mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-28 12:31:00 +00:00
293 lines
11 KiB
Python
293 lines
11 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
|
See the file 'LICENSE' for copying permission
|
|
|
|
Edge cases / control-flow branches of the blind-SQLi inference engine
|
|
(lib/techniques/blind/inference.py) plus the pure UNION configuration helper
|
|
(lib/techniques/union/use.py configUnion).
|
|
|
|
Complements tests/test_inference_engine.py (which covers the happy-path char-by-char
|
|
extraction). Here we drive the REAL bisection() / queryOutputLength() against a mock
|
|
oracle (Request.queryPage replaced by a parser of our own parseable payload template)
|
|
to exercise the branches the engine test does not reach:
|
|
|
|
* trivial returns: payload is None, length == 0
|
|
* --first-char / --last-char range limiting (both via the function args and via
|
|
conf.firstChar / conf.lastChar)
|
|
* --hex output decoding of the assembled value
|
|
* kb.data.processChar post-processing hook
|
|
* session resume from HashDB: a fully cached value, and a PARTIAL_VALUE_MARKER
|
|
partial value that bisection continues from (against a REAL temp SQLite HashDB)
|
|
* queryOutputLength() forging + DIGITS-charset length retrieval
|
|
|
|
No network, no live target, no real DBMS - exactly like the sibling engine test.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from _testutils import bootstrap, set_dbms
|
|
bootstrap()
|
|
|
|
from lib.core.data import conf, kb
|
|
from lib.core.common import decodeDbmsHexValue
|
|
from lib.core.common import getCurrentThreadData
|
|
from lib.core.common import hashDBWrite
|
|
from lib.core.enums import CHARSET_TYPE
|
|
from lib.core.exception import SqlmapSyntaxException
|
|
from lib.core.settings import PARTIAL_VALUE_MARKER
|
|
from lib.request.connect import Connect
|
|
from lib.utils.hashdb import HashDB
|
|
import lib.techniques.blind.inference as inf
|
|
import lib.techniques.union.use as uu
|
|
|
|
# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the
|
|
# greater-char marker (swapped to '=' on the final equality check). A parseable template
|
|
# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret.
|
|
TEMPLATE = "EXPR=%s IDX=%d CMP>%d"
|
|
_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)")
|
|
|
|
# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path
|
|
_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False,
|
|
"charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False,
|
|
"repair": False, "flushSession": None, "freshQueries": None, "hashDB": None}
|
|
_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False,
|
|
"disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False,
|
|
"resumeValues": True, "inferenceMode": False}
|
|
|
|
|
|
class _InferenceCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self._saved_conf = {k: conf.get(k) for k in _CONF}
|
|
self._saved_kb = {k: kb.get(k) for k in _KB}
|
|
self._saved_qp = Connect.queryPage
|
|
self._saved_processChar = kb.data.get("processChar")
|
|
for k, v in _CONF.items():
|
|
conf[k] = v
|
|
for k, v in _KB.items():
|
|
kb[k] = v
|
|
kb.data.processChar = None
|
|
set_dbms("MySQL")
|
|
|
|
def tearDown(self):
|
|
for k, v in self._saved_conf.items():
|
|
conf[k] = v
|
|
for k, v in self._saved_kb.items():
|
|
kb[k] = v
|
|
kb.data.processChar = self._saved_processChar
|
|
Connect.queryPage = self._saved_qp
|
|
inf.Request.queryPage = self._saved_qp
|
|
|
|
def _install_oracle(self, secret):
|
|
def oracle(payload=None, *args, **kwargs):
|
|
m = _PARSE.search(payload)
|
|
idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3))
|
|
ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0
|
|
return (ch > threshold) if op == ">" else (ch == threshold)
|
|
|
|
Connect.queryPage = staticmethod(oracle)
|
|
inf.Request.queryPage = staticmethod(oracle)
|
|
|
|
@staticmethod
|
|
def _reset_thread():
|
|
td = getCurrentThreadData()
|
|
td.shared.value = ""
|
|
td.shared.index = [0]
|
|
td.shared.start = 0
|
|
td.shared.count = 0
|
|
|
|
def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs):
|
|
self._install_oracle(secret)
|
|
self._reset_thread()
|
|
if length is None:
|
|
length = len(secret)
|
|
return inf.bisection(TEMPLATE, expression, length=length, **kwargs)
|
|
|
|
|
|
class TestTrivialReturns(_InferenceCase):
|
|
def test_none_payload(self):
|
|
# payload is None -> (0, None) without ever touching the oracle
|
|
self.assertEqual(inf.bisection(None, "SELECT x"), (0, None))
|
|
|
|
def test_zero_length(self):
|
|
# length == 0 -> (0, "") short-circuit
|
|
self._install_oracle("ignored")
|
|
self._reset_thread()
|
|
self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, ""))
|
|
|
|
|
|
class TestRangeLimiting(_InferenceCase):
|
|
SECRET = "ABCDEFGH"
|
|
|
|
def test_first_char_arg(self):
|
|
# firstChar=3 -> start from the 3rd character (1-based) -> drop "AB"
|
|
_, value = self._bisect(self.SECRET, firstChar=3)
|
|
self.assertEqual(value, "CDEFGH")
|
|
|
|
def test_last_char_arg(self):
|
|
# lastChar=4 -> stop after the 4th character
|
|
_, value = self._bisect(self.SECRET, lastChar=4)
|
|
self.assertEqual(value, "ABCD")
|
|
|
|
def test_conf_first_char(self):
|
|
conf.firstChar = 4
|
|
_, value = self._bisect(self.SECRET)
|
|
self.assertEqual(value, "DEFGH")
|
|
|
|
def test_conf_last_char(self):
|
|
conf.lastChar = 3
|
|
_, value = self._bisect(self.SECRET)
|
|
self.assertEqual(value, "ABC")
|
|
|
|
def test_first_and_last_window(self):
|
|
# combined window: chars 3..6 inclusive -> "CDEF"
|
|
_, value = self._bisect(self.SECRET, firstChar=3, lastChar=6)
|
|
self.assertEqual(value, "CDEF")
|
|
|
|
|
|
class TestHexConvert(_InferenceCase):
|
|
def test_hex_output_decoded(self):
|
|
# --hex: the retrieved value is a hex string the engine decodes on the way out
|
|
conf.hexConvert = True
|
|
hexed = "48656C6C6F" # "Hello"
|
|
_, value = self._bisect(hexed)
|
|
self.assertEqual(value, "Hello")
|
|
self.assertEqual(value, decodeDbmsHexValue(hexed))
|
|
|
|
|
|
class TestProcessCharHook(_InferenceCase):
|
|
def test_process_char_applied_to_each_char(self):
|
|
# kb.data.processChar transforms every assembled character
|
|
kb.data.processChar = lambda c: c.upper()
|
|
_, value = self._bisect("abcde")
|
|
self.assertEqual(value, "ABCDE")
|
|
|
|
|
|
class TestResumeFromHashDB(_InferenceCase):
|
|
"""bisection() consults the session store first (hashDBRetrieve(checkConf=True)).
|
|
Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py)."""
|
|
|
|
def setUp(self):
|
|
_InferenceCase.setUp(self)
|
|
fd, self.path = tempfile.mkstemp(suffix=".sqlite")
|
|
os.close(fd)
|
|
os.remove(self.path) # HashDB creates it lazily
|
|
conf.hashDB = HashDB(self.path)
|
|
# hashDBRetrieve/Write key off these
|
|
self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port"))
|
|
conf.hostname = "test.invalid"
|
|
conf.path = "/"
|
|
conf.port = 80
|
|
|
|
def tearDown(self):
|
|
conf.hostname, conf.path, conf.port = self._saved_loc
|
|
try:
|
|
conf.hashDB.closeAll()
|
|
except Exception:
|
|
pass
|
|
if os.path.exists(self.path):
|
|
os.remove(self.path)
|
|
_InferenceCase.tearDown(self)
|
|
|
|
def test_full_value_resumed(self):
|
|
# a complete cached value short-circuits the whole bisection (0 queries)
|
|
hashDBWrite("SELECT cached", "RESUMED")
|
|
conf.hashDB.flush()
|
|
count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7)
|
|
self.assertEqual(value, "RESUMED")
|
|
self.assertEqual(count, 0)
|
|
|
|
def test_partial_value_continued(self):
|
|
# a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix
|
|
# and extracts only the remaining characters
|
|
kb.inferenceMode = True # partial markers are honored only in inference mode
|
|
hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER)
|
|
conf.hashDB.flush()
|
|
count, value = self._bisect("ABCDE", expression="SELECT partial", length=5)
|
|
self.assertEqual(value, "ABCDE")
|
|
self.assertGreater(count, 0) # it did real work for "CDE"
|
|
|
|
|
|
class TestQueryOutputLength(_InferenceCase):
|
|
def test_length_retrieved(self):
|
|
# queryOutputLength forges a LENGTH() expression and runs bisection with the
|
|
# DIGITS charset; the mock "secret" is the textual length itself
|
|
self._install_oracle("42")
|
|
self._reset_thread()
|
|
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42)
|
|
|
|
def test_length_single_digit(self):
|
|
self._install_oracle("7")
|
|
self._reset_thread()
|
|
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7)
|
|
|
|
def test_digits_charset_extracts_number(self):
|
|
# direct bisection with the DIGITS charset (queryOutputLength's inner call)
|
|
_, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS)
|
|
self.assertEqual(value, "2026")
|
|
|
|
|
|
class TestConfigUnion(unittest.TestCase):
|
|
"""lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols."""
|
|
|
|
_CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50}
|
|
|
|
def setUp(self):
|
|
self._saved = {k: conf.get(k) for k in self._CONF}
|
|
self._saved_uchar = kb.get("uChar")
|
|
for k, v in self._CONF.items():
|
|
conf[k] = v
|
|
|
|
def tearDown(self):
|
|
for k, v in self._saved.items():
|
|
conf[k] = v
|
|
kb.uChar = self._saved_uchar
|
|
|
|
def test_char_and_range(self):
|
|
uu.configUnion(char="NULL", columns="2-6")
|
|
self.assertEqual(kb.uChar, "NULL")
|
|
self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6))
|
|
|
|
def test_single_column(self):
|
|
uu.configUnion(char="NULL", columns="4")
|
|
self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4))
|
|
|
|
def test_uchar_substitution_quoted(self):
|
|
# conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template
|
|
conf.uChar = "test"
|
|
uu.configUnion(char="x[CHAR]x", columns="1")
|
|
self.assertEqual(kb.uChar, "x'test'x")
|
|
|
|
def test_uchar_substitution_digit(self):
|
|
# a digit conf.uChar is substituted unquoted
|
|
conf.uChar = "88"
|
|
uu.configUnion(char="[CHAR]", columns="1")
|
|
self.assertEqual(kb.uChar, "88")
|
|
|
|
def test_conf_ucols_overrides_columns_arg(self):
|
|
# conf.uCols takes precedence over the columns argument
|
|
conf.uCols = "3-9"
|
|
uu.configUnion(char="NULL", columns="1-2")
|
|
self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9))
|
|
|
|
def test_non_integer_range_raises(self):
|
|
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc")
|
|
|
|
def test_inverted_range_raises(self):
|
|
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2")
|
|
|
|
def test_non_string_char_ignored(self):
|
|
# a non-string char leaves kb.uChar untouched (early return)
|
|
kb.uChar = "SENTINEL"
|
|
uu.configUnion(char=None, columns="1")
|
|
self.assertEqual(kb.uChar, "SENTINEL")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|