mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-28 12:31:00 +00:00
156 lines
5.5 KiB
Python
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
|