sqlmap/lib/utils/wafbypass.py

156 lines
5.5 KiB
Python

#!/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