This one didn't.

No error messages. No stack traces. No SQL syntax warnings.

Just a response that took longer than it should have.

During an authorized security assessment, I identified a time-based blind SQL injection vulnerability in a backend API endpoint. Automated tooling missed it entirely on the first pass — not because the vulnerability was sophisticated, but because the application processed input in a way the tool didn't account for.

Once that was understood, everything changed.

The Entry Point: A Backend API Endpoint

The target was a GET-based API endpoint handling parameter validation during a login-related workflow.

GET /api/v2/clogin?apikey=12334&type=check&u=CXTgnn

On the surface it looked unremarkable:

  • Simple GET request
  • Short parameter list
  • Fast, consistent responses — 47ms baseline

Nothing immediately suggested injection. But Burp's active scanner flagged a possible timing anomaly on one of the parameters.

That was enough to dig deeper.

None
Baseline request, 47ms response time.

Shifting the Testing Mindset

The temptation at this point is to hand it straight to sqlmap and move on.

Instead, I asked a different question:

What is the application actually doing with this input before the backend sees it?

Not what the parameter looks like in the request. What happens to it in transit.

That question changed the entire approach.

The First Signal: 35 Seconds of Silence

Before touching any tooling, I validated the timing behaviour manually.

I crafted a simple delay-based payload and injected it directly into the u parameter:

CXTgnn'+(select*from(select(sleep(35)))a)+'

The response came back in 35,054ms.

Baseline was 47ms.

No error. No output. Just a 35-second delay — exactly as instructed.

The backend was executing injected SQL logic. It just wasn't returning any output.

That's the definition of blind SQL injection.

None
URL-decode → base64-decode → plain payload chain
None
Same request with encoded payload. Response time: 35,054ms

Why Naive Automation Would Fail Here

Before running any tooling, I understood why a standard sqlmap run would come up empty.

The u parameter wasn't consumed as raw input. Looking at the request flow revealed a two-step transformation happening before the backend ever saw the value:

  1. The plain value was Base64-encoded
  2. Then URL-encoded
  3. Only then passed to backend logic

Any tool injecting raw SQL payloads directly into u would be sending input the backend never actually received in that form. The payloads would be decoded into something unrecognizable — and the injection would silently fail.

The tool needed to speak the application's language first.

Teaching the Tool How the Application Thinks

The fix was to tell sqlmap exactly how to transform its payloads before sending them.

sqlmap -u "https://[target]/api/v2/clogin?apikey=12334&type=check&u=INJECT_HERE" \
--eval="import urllib.parse, base64; u=urllib.parse.quote(base64.b64encode(u.encode()).decode())" \
--technique=T \
--batch --level=5 --risk=3 \
--dbs

The --eval flag instructs sqlmap to run a Python expression against the parameter value before each request — in this case, Base64-encoding then URL-encoding the payload to match exactly what the application expected.

The behaviour changed immediately.

None
sqlmap run with — eval flag showing the Base64 + URL encoding eval expression and technique=T targeting the u parameter.

Impact: Full Database Enumeration

Despite being entirely blind — no error output, no reflected data, no stack traces — the injection led to:

  • Backend DBMS confirmed: MySQL ≥ 5.0.12
  • 5 databases enumerated
  • 88 tables retrieved from the target database
None
u parameter confirmed vulnerable. MySQL ≥ 5.0.12 identified. 5 databases enumerated including information_schema, mysql, performance_schema, sys, and redacted target database.
None
Second sqlmap run targeting specific database with — tables flag.
None
88 tables retrieved. Table names visible including admin, audit_log_db, chatbot_log, companies, dashboard and more.

All extracted character by character through timing alone.

This wasn't a noisy attack. It was patient, quiet, and reliable.

Why Encoding Isn't Protection

The encoding layer on this parameter likely gave a false sense of security.

From a developer's perspective, a Base64 + URL-encoded value looks nothing like a SQL payload. It doesn't trigger naive input validation. It doesn't match obvious injection signatures.

But encoding is transformation, not sanitization.

The backend decoded the value before passing it to the query layer. At that point, the payload was exactly what it was designed to be.

Encoding delayed discovery. It didn't prevent exploitation.

The Architectural Failure

This came down to two missing controls:

  • No parameterized queries or prepared statements on the backend
  • Input transformation mistaken for input validation

Security tools don't fail because they're weak. They fail when the tester doesn't adapt them to how the application actually processes input.

Once the encoding logic was understood and replicated, the vulnerability that appeared invisible became fully exploitable.

Final Thought

This vulnerability wasn't about finding a clever bypass.

It was about reading the application correctly.

When systems assume that transformation equals protection, blind vulnerabilities don't disappear — they just wait longer to be found.

The 47ms baseline told one story. The 35-second delay told the truth.

None