mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-08-04 14:55:40 +00:00
200 lines
7.1 KiB
Python
200 lines
7.1 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
|
See the file 'LICENSE' for copying permission
|
|
"""
|
|
|
|
import difflib
|
|
|
|
from lib.core.compat import xrange
|
|
from lib.core.common import getFileItems
|
|
from lib.core.common import paramToDict
|
|
from lib.core.common import randomStr
|
|
from lib.core.common import singleTimeWarnMessage
|
|
from lib.core.data import conf
|
|
from lib.core.data import logger
|
|
from lib.core.data import paths
|
|
from lib.core.enums import HTTPMETHOD
|
|
from lib.core.enums import PLACE
|
|
from lib.core.settings import DIFF_TOLERANCE
|
|
from lib.core.settings import MAX_DIFFLIB_SEQUENCE_LENGTH
|
|
from lib.core.settings import PARAMETER_MINING_BUCKET_SIZE
|
|
from lib.request.connect import Connect as Request
|
|
|
|
# Benign, broadly-stable value used both to check whether a discovered parameter is safe to add and
|
|
# as its seed during testing (a random value would error out an integer/id context and defeat inference)
|
|
PROBE_VALUE = "1"
|
|
|
|
def _canary():
|
|
return randomStr(10, lowercase=True)
|
|
|
|
def _fetch(get):
|
|
"""
|
|
Requests the target with the given GET query string, returning a (page, HTTP code) pair.
|
|
"""
|
|
|
|
try:
|
|
page, _, code = Request.getPage(get=get or None, silent=True, raise404=False)
|
|
except Exception:
|
|
page, code = None, None
|
|
|
|
return (page or ""), code
|
|
|
|
def _ratio(first, second):
|
|
"""
|
|
Similarity of two response bodies, mirroring the core page comparison (see comparison.py): an
|
|
exact match, a length ratio for oversized bodies, otherwise difflib's quick_ratio().
|
|
"""
|
|
|
|
if not first or not second:
|
|
return 0.0
|
|
|
|
if first == second:
|
|
return 1.0
|
|
|
|
if any(len(_) > MAX_DIFFLIB_SEQUENCE_LENGTH for _ in (first, second)):
|
|
ratio = 1.0 * len(first) / len(second)
|
|
return ratio if ratio <= 1 else 1.0 / ratio
|
|
|
|
return difflib.SequenceMatcher(None, first, second).quick_ratio()
|
|
|
|
def _differs(page, base, floor):
|
|
"""
|
|
True when 'page' departs from the baseline by more than the target's own dynamic jitter ('floor').
|
|
"""
|
|
|
|
return bool(page) and _ratio(page, base) < floor - DIFF_TOLERANCE
|
|
|
|
def _confirm(baseGet, name, base, floor):
|
|
"""
|
|
Confirms a single candidate in isolation with two differently-valued probes. Returns 'reflected'
|
|
when a value is echoed back (caught here even if a shared bucket hid it behind a preceding
|
|
parameter), 'behavioral' when both probes resemble each other yet depart from the baseline (its
|
|
presence, not its value, matters), otherwise None.
|
|
"""
|
|
|
|
def _get(value):
|
|
pair = "%s=%s" % (name, value)
|
|
return "%s&%s" % (baseGet, pair) if baseGet else pair
|
|
|
|
canaries = (_canary(), _canary())
|
|
first, second = _fetch(_get(canaries[0]))[0], _fetch(_get(canaries[1]))[0]
|
|
|
|
if not (first and second):
|
|
return None
|
|
|
|
if (canaries[0] in first or canaries[1] in second) and not any(_ in base for _ in canaries):
|
|
return "reflected"
|
|
|
|
if _ratio(first, second) < floor - DIFF_TOLERANCE: # value-dependent yet not reflected -> unreliable
|
|
return None
|
|
|
|
if _differs(first, base, floor) and _differs(second, base, floor):
|
|
return "behavioral"
|
|
|
|
return None
|
|
|
|
def _chunks(sequence, size):
|
|
for i in xrange(0, len(sequence), size):
|
|
yield sequence[i:i + size]
|
|
|
|
def _discover(candidates, baseGet, base, floor):
|
|
"""
|
|
Probes candidate names in buckets (one shared request per bucket, each name carrying its own
|
|
random canary) and returns the confirmed ones as (name, reason) pairs. Reflection is resolved
|
|
straight from the bucket response; the rest are confirmed individually only when the bucket
|
|
actually moved the response, so a target that ignores every candidate stays cheap.
|
|
"""
|
|
|
|
found = []
|
|
|
|
for chunk in _chunks(candidates, PARAMETER_MINING_BUCKET_SIZE):
|
|
canaries = dict((name, _canary()) for name in chunk)
|
|
query = "&".join("%s=%s" % (name, canaries[name]) for name in chunk)
|
|
page = _fetch("%s&%s" % (baseGet, query) if baseGet else query)[0]
|
|
|
|
if not page:
|
|
continue
|
|
|
|
pending = []
|
|
for name in chunk:
|
|
if canaries[name] in page and canaries[name] not in base:
|
|
found.append((name, "reflected"))
|
|
else:
|
|
pending.append(name)
|
|
|
|
if pending and _differs(page, base, floor):
|
|
for name in pending:
|
|
reason = _confirm(baseGet, name, base, floor)
|
|
if reason:
|
|
found.append((name, reason))
|
|
|
|
return found
|
|
|
|
def _commit(found, baseGet, baseCode):
|
|
"""
|
|
Adds the discovered parameters (seeded with PROBE_VALUE) to the GET test scope. One that turns
|
|
the request into a server error the baseline did not have would shadow and corrupt the testing
|
|
of every sibling parameter, so it is reported and held back rather than degrading detection.
|
|
"""
|
|
|
|
safe, disruptive = [], []
|
|
|
|
for name, _ in found:
|
|
pair = "%s=%s" % (name, PROBE_VALUE)
|
|
code = _fetch("%s&%s" % (baseGet, pair) if baseGet else pair)[1]
|
|
if code is not None and code >= 500 and not (baseCode is not None and baseCode >= 500):
|
|
disruptive.append(name)
|
|
else:
|
|
safe.append(name)
|
|
|
|
if disruptive:
|
|
logger.warning("held back parameter(s) that break the base request with a test value (test them explicitly with '-p'): %s" % ", ".join("'%s'" % _ for _ in disruptive))
|
|
|
|
if not safe:
|
|
return
|
|
|
|
logger.info("adding %d discovered parameter(s) to the test scope: %s" % (len(safe), ", ".join("'%s'" % _ for _ in safe)))
|
|
|
|
additions = "&".join("%s=%s" % (name, PROBE_VALUE) for name in safe)
|
|
conf.parameters[PLACE.GET] = "%s&%s" % (baseGet, additions) if baseGet else additions
|
|
conf.paramDict[PLACE.GET] = paramToDict(PLACE.GET, conf.parameters[PLACE.GET])
|
|
|
|
def mineParameters():
|
|
"""
|
|
Discovers hidden (unlinked) GET parameters the target still processes and queues the confirmed
|
|
ones for the regular injection tests, using two independent oracles (value reflection and a
|
|
behavioral side effect on the response).
|
|
"""
|
|
|
|
if conf.data or (conf.method and conf.method != HTTPMETHOD.GET):
|
|
singleTimeWarnMessage("'--mine-params' currently supports GET parameters only")
|
|
return
|
|
|
|
baseGet = conf.parameters.get(PLACE.GET) or ""
|
|
existing = set(conf.paramDict.get(PLACE.GET) or {})
|
|
candidates = [_ for _ in getFileItems(paths.COMMON_PARAMETERS, unique=True) if _ and _ not in existing]
|
|
|
|
if not candidates:
|
|
return
|
|
|
|
logger.info("mining for hidden GET parameters (%d candidate name(s))" % len(candidates))
|
|
|
|
base, baseCode = _fetch(baseGet)
|
|
if not base:
|
|
singleTimeWarnMessage("could not obtain a baseline response, skipping parameter mining")
|
|
return
|
|
|
|
floor = _ratio(base, _fetch(baseGet)[0]) # the target's own between-request jitter
|
|
|
|
found = _discover(candidates, baseGet, base, floor)
|
|
|
|
for name, reason in found:
|
|
logger.info("found hidden parameter '%s' (%s)" % (name, reason))
|
|
|
|
if not found:
|
|
logger.info("no hidden parameters found")
|
|
return
|
|
|
|
_commit(found, baseGet, baseCode)
|