sqlmap/tests/test_inference.py
Miroslav Štampar cb20a446ae
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run
Update of unit tests
2026-06-28 14:28:42 +02:00

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)