Fixing some of WAF bypass bugs

This commit is contained in:
Miroslav Štampar
2026-08-17 16:41:49 +02:00
parent dc42edc19f
commit 49a774035f
5 changed files with 37 additions and 8 deletions
+4 -1
View File
@@ -423,7 +423,10 @@ def _doSearch():
conf.googlePage += 1
def _setStdinPipeTargets():
if conf.url:
# Note: an explicit target source takes precedence. Without this, any non-interactive run (CI,
# cron, subprocess) would reroute '-m/-l/-r/-g' targets through the STDIN container, losing both
# their count and their order
if any((conf.url, conf.direct, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.openApiFile)):
return
if isinstance(conf.stdinPipe, _collections.Iterable):
+2 -2
View File
@@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.8.42"
VERSION = "1.10.8.43"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
@@ -88,7 +88,7 @@ WAF_BYPASS_TAMPERS = (
)
# Maximum number of candidate tamper (chains) trialled during automatic WAF-bypass
WAF_BYPASS_MAX_TRIALS = 8
WAF_BYPASS_MAX_TRIALS = len(WAF_BYPASS_TAMPERS)
# Browser-like request headers applied alongside the random (non-scanner) User-Agent during
# automatic WAF bypass: sqlmap's defaults ('Accept: */*', no 'Accept-Language') are themselves a
+4 -1
View File
@@ -1215,7 +1215,10 @@ def cmdLineParser(argv=None):
if args.dummy:
args.url = args.url or DUMMY_URL
if hasattr(sys.stdin, "fileno") and not any((os.isatty(sys.stdin.fileno()), args.api, args.ignoreStdin, "GITHUB_ACTIONS" in os.environ)):
# Note: an explicit target source rules out reading targets from the standard input. Without
# this, any non-interactive run (CI, cron, subprocess) would turn '-d/-u/-m/-l/-r/-g' into a
# 'multiple targets' run reading from a pipe, which also resets per-target options in between
if hasattr(sys.stdin, "fileno") and not any((os.isatty(sys.stdin.fileno()), args.api, args.ignoreStdin, "GITHUB_ACTIONS" in os.environ, args.direct, args.url, args.logFile, args.bulkFile, args.requestFile, args.googleDork, args.configFile, args.openApiFile)):
args.stdinPipe = iter(sys.stdin.readline, None)
else:
args.stdinPipe = None
+1 -1
View File
@@ -120,7 +120,7 @@ class Fingerprint(GenericFingerprint):
fork = FORK.DORIS
elif inject.checkBooleanExpression("@@VERSION_COMMENT LIKE '%StarRocks%'"):
fork = FORK.STARROCKS
elif inject.checkBooleanExpression("GEOGRAPHY_AREA(NULL) IS NULL"): # Note: MemSQL is the only one of the forks without SESSION_USER()
elif inject.checkBooleanExpression("GEOGRAPHY_AREA(NULL) IS NULL"): # Note: GEOGRAPHY_AREA() is MemSQL specific
fork = FORK.MEMSQL
elif inject.checkBooleanExpression("AURORA_VERSION() LIKE '%'"): # Reference: https://aws.amazon.com/premiumsupport/knowledge-center/aurora-version-number/
fork = FORK.AURORA
+26 -3
View File
@@ -7,6 +7,8 @@ See the file 'LICENSE' for copying permission
import re
from lib.core.common import Backend
from lib.core.enums import DBMS
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
@@ -43,7 +45,16 @@ def _unwrapIsnull(query):
break
inner = retVal[match.end():end]
separator = inner.rfind(',')
separator, depth = -1, 0 # the argument separator is the comma at the top level
for index, char in enumerate(inner):
if char == '(':
depth += 1
elif char == ')':
depth -= 1
elif char == ',' and depth == 0:
separator = index
if separator < 1:
break
@@ -75,6 +86,11 @@ def _reshape(payload, opener, tail, build):
pos = pos + match.end()
continue
replacement = build(query, rest)
if replacement is None: # builder declined, leave this occurrence alone
pos = pos + match.end()
continue
retVal = retVal[:start] + replacement + retVal[end + 1 + rest.end():]
pos = start + len(replacement)
return retVal
@@ -134,7 +150,12 @@ def tamper(payload, **kwargs):
def _mysqlSet(query, rest):
# set-membership form of the same read ('... IN (<ordinals>)', used by the Huffman retrieval).
# ORD('') is 0, so a past-the-end position matches the ordinal 0, which is the empty string here
position, ordinals = rest.group(1), [int(_) for _ in rest.group(2).split(',')]
position = rest.group(1)
ordinals = [int(_) for _ in rest.group(2).split(',') if _.strip().isdigit()]
if not ordinals or any(_ > 255 for _ in ordinals): # a byte comparison cannot represent those
return None
query = _unwrapIsnull(query)
members = ",".join("''" if _ == 0 else "0x%02x" % _ for _ in ordinals)
return "BINARY RIGHT(LEFT(%s,%s),(%s<=LENGTH(CONVERT(%s USING ascii)))) IN (%s)" % (query, position, position, query, members)
@@ -150,7 +171,9 @@ def tamper(payload, **kwargs):
comma_tail = r"\s*,\s*(\d+)\s*,\s*1\)\)\s*(>=|<=|>|<|=)\s*(\d+)"
set_tail = r"\s*,\s*(\d+)\s*,\s*1\)\)\s+IN\s*\(([\d,\s]+)\)"
if re.search(r"(?i)IFNULL\(", payload): # also on payloads that are not single-character reads
# also on payloads that are not single-character reads. Gated on MySQL, because IFNULL() is used
# by H2, HSQLDB, Cubrid and others too, and IF() is not a function there
if Backend.getIdentifiedDbms() == DBMS.MYSQL and re.search(r"(?i)IFNULL\(", payload):
payload = _unwrapIsnull(payload)
retVal = _reshape(payload, r"(?i)ORD\(MID\(", set_tail, _mysqlSet)