Skip to content
Z3tra
All write-ups
4 min readPersonal labmedium

From a blind SQL injection to a shell, one bit at a time

A lab walkthrough: finding a boolean-blind injection with no visible output, turning it into data exfiltration, then into file write, then into code execution.

This is a walkthrough against a deliberately vulnerable application in my own lab. The whole point of writing it up is the reasoning — the dead ends included — not the specific payloads, which are the least transferable part.

Recon: understand it first

Before touching a single parameter I mapped what the application does. A product catalogue with a search box, a login, and an admin panel referenced in the JavaScript bundle but not linked anywhere in the UI. The search box takes a query and returns matching products — or, interestingly, returns nothing at all for certain inputs rather than an error.

That "nothing at all" is the tell. An application that behaves differently for malformed input without showing an error is often making a decision based on a query it will not let you see.

Confirming the injection

No error messages, no reflected output. That rules out the easy paths and points at boolean-blind: the response does not contain the data, but it does change shape depending on whether a condition is true.

Establish the two states

A query that returns results, and one that returns none. Here, a valid search term gave a product grid; a term matching nothing gave an empty state. Two distinguishable responses is all a blind injection needs.

Inject a condition that is always true

Appending a condition that evaluates true left the results unchanged. Appending one that evaluates false collapsed them to the empty state. The response was now a one-bit oracle answering any yes/no question I could phrase in SQL.

Automate the oracle

Asking one bit at a time by hand is unbearable. A short script turns a yes/no oracle into arbitrary reads by binary-searching each character.

import requests
 
BASE = "http://lab.local/search"
 
def oracle(condition: str) -> bool:
    # True when the injected condition holds — detected by whether the
    # results grid is present in the response.
    payload = f"widget' AND ({condition})-- -"
    r = requests.get(BASE, params={"q": payload})
    return "product-card" in r.text
 
def extract_char(query: str, index: int) -> str:
    # Binary search over the printable ASCII range: 7 requests per character
    # instead of 95.
    low, high = 32, 126
    while low < high:
        mid = (low + high) // 2
        if oracle(f"ascii(substring(({query}),{index},1)) > {mid}"):
            low = mid + 1
        else:
            high = mid
    return chr(low)

Seven requests per character rather than ninety-five. On a lab that is the difference between a coffee and an afternoon.

Escalating: reading beyond the database

Extracting table contents proved the injection, but the goal was the box, not the data. The database user turned out to have file privileges — the kind of misconfiguration that is depressingly common and exactly what a lab is built to teach.

That gave two new primitives: reading files off the host, and writing them.

-- Read: confirm file access with something harmless first.
' UNION SELECT load_file('/etc/hostname')-- -

Reading confirmed file access. Writing was the actual escalation — dropping a small file into a directory the web server would execute.

From file write to a shell

With a write primitive and a web-executable directory, the last step is mechanical: write a minimal handler, request it, and you have command execution in the context of the web server. From there the lab's intended path was ordinary local enumeration to a more privileged user.

What the lab was actually teaching

The injection was the entry, but the lesson was the chain — each link a separate misconfiguration that would have been harmless alone:

  1. A query built by concatenation instead of parameters.
  2. A database user with file privileges it never needed.
  3. A web root writable by that same user.
  4. A server willing to execute what it found there.

Fix the first and there is no injection. Fix the second and the injection cannot touch the filesystem. Fix the third and it cannot write where it matters. Any single correct decision breaks the whole chain — which is the entire argument for defence in depth, made concrete.

Related

Write-up3 min

The JWT that trusted its own header

Personal lab

A lab write-up on an authentication bypass through JWT algorithm confusion — the classic 'alg: none' and its slightly less obvious RS256-to-HS256 cousin.

  • Write-ups
  • Web Security
  • Authentication
Research4 min

Encrypting content is easy. Hiding who talks to whom is not.

Notes from designing Orbyte: why metadata is often more sensitive than message content, and the spectrum of defences between 'we encrypt messages' and actual metadata resistance.

  • Research
  • Web Security
  • Cryptography
Article3 min

Understand the application before you test it

Scanners find what they were told to look for. The bugs that matter live in the gap between what an application believes about itself and what it actually enforces.

  • Web Security
  • OWASP
  • Methodology