Repository navigation
MySQL + ORDER BY expression + value-wide character whitelist: boolean blind detection misses it #6145
Description
sqlmap 1.10.9.32 misses the blind injection below, although it takes about three manual requests to confirm it's real.
The injection sits in an ORDER BY expression position, and the parameter value is gated by a value-wide character whitelist - the whole value gets dropped if it contains anything unexpected, it's not per-character filtering:
POST https://TARGET/PATH?SORT_COL=CASE&SORT_DIR=<expr>
The backend concatenates SORT_DIR into order by case <SORT_DIR> <asc|desc> ..., accepts only [A-Za-z0-9_ ,.] and falls back to the default ordering otherwise.
$ python sqlmap.py -u "https://TARGET/PATH?SORT_COL=CASE&SORT_DIR=asc" \
-p SORT_DIR --technique=B --level=5 --risk=1 --batch
[WARNING] GET parameter 'SORT_DIR' does not seem to be injectable
[CRITICAL] all tested parameters do not appear to be injectable.
Same parameter, only the condition changes, and the result set reorders:
SORT_DIR=WHEN p.id BETWEEN 999999 AND 999999 THEN 0 ELSE p.id END asc -> first id = 972 (default)
SORT_DIR=WHEN p.id BETWEEN 974 AND 974 THEN 0 ELSE p.id END asc -> first id = 974 (that row forced to the front)
SORT_DIR=WHEN p.zzqq_nonexist THEN 0 ELSE p.id END asc -> Unknown column 'p.zzqq_nonexist' in 'order clause', with the whole backend SQL in the response
Parameter names below are placeholders (SORT_COL / SORT_DIR); the real ones are framework-specific details I'd rather not publish together with a live host.
What the whitelist lets through
I put each kind of construct into SORT_DIR and checked whether it reached SQL. If the response reorders or raises a SQL error it got through; if the first id is still 8139 and the body is byte-identical to the no-parameter request, the value was dropped whole.
- no function survives:
LENGTH()CHAR_LENGTH()ORD()ASCII()SUBSTRING()LEFT()HEX()CAST()IF()SLEEP()EXTRACTVALUE() - operators do:
BETWEENLIKELIKE BINARYIS NOT NULLNOT BETWEENAND/OR/NOTCASE WHEN ... THEN ... ELSE ... END - literals do:
0x...hex, plain numbers - nothing else does:
=<>><()'";%*-/@#!
So the usable alphabet is really just keywords, hex literals, digits and space/comma/dot, and function names are filtered out as well.
Why it gets missed
Three separate reasons.
1. There is no MySQL ORDER BY boolean vector.
boolean_blind.xml has 11 tests declaring <clause>3</clause>, covering PostgreSQL, MSSQL/Sybase, Oracle, SAP MaxDB and DB2. MySQL/MariaDB has none. The generic vector that can land in ORDER BY is:
,(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN [ORIGVALUE] ELSE 1/(SELECT 0) END))Under this whitelist it is dropped whole - the true and false responses are byte-identical and both fall back to the default order (first id 8139) - so sqlmap only ever sees "no difference".
The framework side already supports the clause: prefixQuery in lib/core/agent.py has a kb.injection.clause == [3] branch, and enums.py defines 3: "ORDER BY" in PAYLOAD.CLAUSE. The mechanism is there, MySQL just has no payload that can survive the position.
2. Every boolean/time vector's inference contains = or parentheses.
Whatever the DBMS and whatever the <clause> says, [INFERENCE] expands to [RANDNUM]=[RANDNUM] or to a parenthesized expression. The MySQL entry in time_blind.xml is:
<vector>AND (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
<clause>1,2,3,8,9</clause>It declares clause 3, but once the parentheses are dropped it is no longer expression-position syntax.
3. The extraction inference template cannot be reworked.
queries.xml has <inference query="ORD(MID((%s),%d,1))>%d"/> for MySQL, and ORD/MID are function names, which are filtered here too. The substitution happens in agent.py around lines 391-408 and reads queries[Backend.getIdentifiedDbms()].inference, i.e. a DBMS-level fixed value that a payload cannot override. In an environment where function names are rejected, string extraction degrades to hex-prefix enumeration (col LIKE BINARY 0x..., works, verified, but ~256 requests per byte instead of the ORD(MID()) template).
Possible fixes
A - this alone would fix detection. Add MySQL ORDER BY vectors to boolean_blind.xml using keywords and hex literals only:
<test>
<title>MySQL >= 5.0 boolean-based blind - ORDER BY clause (keyword-only)</title>
<stype>1</stype>
<level>2</level>
<risk>1</risk>
<clause>3</clause>
<where>1</where>
<vector>CASE WHEN [INFERENCE] THEN 1 ELSE 0 END</vector>
<request>
<payload>CASE WHEN 1 BETWEEN 1 AND 1 THEN 1 ELSE 0 END</payload>
</request>
<response>
<comparison>CASE WHEN 1 NOT BETWEEN 1 AND 1 THEN 1 ELSE 0 END</comparison>
</response>
<details>
<dbms>MySQL</dbms>
<dbms>MariaDB</dbms>
</details>
</test>CASE WHEN <cond> THEN 1 ELSE 0 END is a valid ORDER BY expression, and the two branches reorder the result set (first id 972 vs 974 on this target), which the boolean comparison can observe. NOT BETWEEN stands in for the negation of =, since <> cannot be written with keywords alone.
B - a tamper that rewrites comparators into keyword-only equivalents: x='abc' to x LIKE BINARY 0x616263, x=y to x BETWEEN y AND y, strip redundant parentheses, &&/||/! to AND/OR/NOT. If the payload contains a function call it is returned unchanged, because a rewrite cannot be valid SQL when function names are rejected anyway. 9 offline cases pass locally.
C - needs your call. Making string extraction work in this kind of environment needs either a whitelist-safe [INFERENCE] variant or a way for a payload to declare its own inference template. Right now it is DBMS-level fixed.
Why I don't think this is a one-off
A very common Java scaffolding + pagination helper (pagination-style APIs) concatenates user-supplied column/direction parameters straight into ORDER BY, and plenty of deployments add a character whitelist on top. The clause 3 machinery exists, but with no MySQL payload that survives an expression position, this position is systematically invisible.
Reproduction is the three requests above, just swap the host.
Happy to send a PR with A and B if the direction is acceptable.
Activity
stamparm commentedon Oct 16, 2026
please dont send PRs as the chances to get merged is around 0
this is a narrow case, not a sqlmap bug. also, REWRITING the engine for the single specific case, especially by the outsider, will not be
Update regarding #6145
stamparm commentedon Oct 16, 2026
with
.../PATH?SORT_COL=p.name&SORT_DIR=asc" -p SORT_COL --technique=B --level=3 --batch --tamper=betweenyou should at least be able to get the detection with the latest revision:as for the enumeration, there are too much constraints in your case
stamparm commentedon Oct 16, 2026
p.s. nonetheless, couple of nice fixes came inherently with the latest revision (because of your narrow case), so thank you for the report