diff --git a/.gitignore b/.gitignore index 07ca46e6e..7eeed3d31 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ plugins/.DS_Store thirdparty/.DS_Store CLAUDE.md .coverage +.codegraph/ diff --git a/lib/core/settings.py b/lib/core/settings.py index dc8f694e6..0e51e1b0a 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.213" +VERSION = "1.10.7.214" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index 95f618c3d..5a66f4d94 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -268,6 +268,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None finalValue = None retrievedLength = 0 columnKey = None + hexEncoded = False if payload is None: return 0, None @@ -369,6 +370,10 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None # rows for low-cardinality guessing and for its own per-column online Huffman model. columnKey = normalizedExpression(expression) if dump else None + # Whole-value equality shortcuts (low-card guess, litmus) compare a hex-DECODED value against a + # HEX()-wrapped expression -> always miss; skip them when hex-encoding (--hex / --binary-fields). + hexEncoded = bool(conf.hexConvert or kb.binaryField) + # Low-cardinality whole-value guessing: when the distinct values already seen for this column are # few (<= LOW_CARDINALITY_THRESHOLD), confirm the current cell by equality against each of them # (one request on a hit) before per-character extraction - a large win on the enum/flag/status/ @@ -376,7 +381,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None # Especially valuable for TIME-BASED blind: a hit confirms the whole value in a single delayed # request instead of ~7 delays/char x N chars. The repetition gate below ensures it only ever fires # on genuinely low-cardinality columns, so unique identifier names never pay a wasted probe/delay. - if columnKey is not None and not partialValue: + if columnKey is not None and not partialValue and not hexEncoded: # Snapshot the shared cache under the lock (value-parallel workers may mutate it concurrently). with kb.locks.prediction: seen = dict(kb.lowCardCache.get(columnKey) or ()) @@ -1188,7 +1193,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None # known-answer differential so an always-true / flaky / degraded channel that would otherwise dump # SILENT garbage instead raises a one-time "results may be unreliable" warning. First value is always # checked (catch it before a whole bad dump), then every ORACLE_LITMUS_CHECK_EVERY-th. - if (ORACLE_LITMUS_CHECK_EVERY and finalValue and not kb.reliabilityAlarm and not kb.bruteMode + if (ORACLE_LITMUS_CHECK_EVERY and finalValue and not kb.reliabilityAlarm and not kb.bruteMode and not hexEncoded and (columnKey is not None or kb.partRun in NAME_PREDICTION_CONTEXTS)): with kb.locks.prediction: kb.litmusCounter += 1 diff --git a/tests/test_techniques.py b/tests/test_techniques.py index fe7563a44..38815d5de 100644 --- a/tests/test_techniques.py +++ b/tests/test_techniques.py @@ -1522,6 +1522,93 @@ class TestHexConvert(_InferenceCase): self.assertEqual(value, decodeDbmsHexValue(hexed)) +class TestHexEncodedShortcutsGated(_InferenceCase): + """Under --hex / --binary-fields the expression is HEX()-wrapped, so the whole-value equality + shortcuts (low-card guess, oracle litmus) compare a hex-DECODED value against it and always miss - + wasting a probe per cell and tripping a spurious "unreliable" alarm. They must be skipped there. + Spying valueMatchCondition (the guess calls it first, 'continue's on None) and the litmus lets us + see whether each shortcut was reached without a full injection/agent context.""" + + _EXTRA_KB = ("lowCardCache", "dumpCharset", "dumpCharsetStable", "litmusCounter", + "reliabilityAlarm", "huffmanModel", "multiThreadMode", "commonOutputs") + + def setUp(self): + _InferenceCase.setUp(self) + self._saved_extra_kb = {k: kb.get(k) for k in self._EXTRA_KB} + self._saved_noHuffman = conf.get("noHuffman") + self._saved_binaryFields = conf.get("binaryFields") + self._saved_dbms = kb.get("dbms") + self._saved_vmc = inf.valueMatchCondition + self._saved_litmus = inf.oracleReliabilityLitmus + conf.noHuffman = True # keep extraction on the classic '>' bisection path + conf.binaryFields = None + # bisection only builds the nulled/casted (and HEX-wrapped) expression - the step that sets + # kb.binaryField - when Backend.getDbms() is truthy; set_dbms() only forces getIdentifiedDbms() + kb.dbms = "MySQL" + kb.lowCardCache = {} + kb.dumpCharset = {} + kb.dumpCharsetStable = {} + kb.litmusCounter = 0 + kb.reliabilityAlarm = False + kb.huffmanModel = {} + kb.multiThreadMode = False + kb.commonOutputs = None + + def tearDown(self): + inf.valueMatchCondition = self._saved_vmc + inf.oracleReliabilityLitmus = self._saved_litmus + conf.noHuffman = self._saved_noHuffman + conf.binaryFields = self._saved_binaryFields + kb.dbms = self._saved_dbms + for k, v in self._saved_extra_kb.items(): + kb[k] = v + _InferenceCase.tearDown(self) + + _HEXED = "48656C6C6F" # hex of "Hello"; all-ASCII so the shortcuts are applicable + + def _run(self, hexConvert, expression="SELECT secret", binaryField=False): + conf.hexConvert = hexConvert + if binaryField: + conf.binaryFields = [agent.getFields(expression)[6]] # the field bisection will hex-wrap + # arm the low-cardinality cache for this column so the guess WOULD fire if not gated + kb.lowCardCache[inf.normalizedExpression(expression)] = {"Hello": 3} + calls = {"guess": 0, "litmus": 0} + + def vmc_spy(*args, **kwargs): + calls["guess"] += 1 + return None # None -> guess loop 'continue's: no probe, no agent context needed + + def litmus_spy(*args, **kwargs): + calls["litmus"] += 1 + return True + + inf.valueMatchCondition = vmc_spy + inf.oracleReliabilityLitmus = litmus_spy + _, value = self._bisect(self._HEXED, expression=expression, length=len(self._HEXED), dump=True) + return value, calls + + def test_shortcuts_fire_without_hex(self): + # control: on a normal dump both shortcuts are reached (proves the test can actually see them, + # so the skips below are the gate doing its job, not a dead assertion) + value, calls = self._run(hexConvert=False) + self.assertEqual(value, self._HEXED) # no decode without --hex + self.assertGreater(calls["guess"], 0, "low-card guess should run on a normal dump") + self.assertEqual(calls["litmus"], 1, "oracle litmus should run on a normal dump") + + def test_shortcuts_skipped_under_hex(self): + value, calls = self._run(hexConvert=True) + self.assertEqual(value, "Hello") # extraction still correct (decoded on the way out) + self.assertEqual(calls["guess"], 0, "low-card guess must be skipped when the expression is HEX-wrapped") + self.assertEqual(calls["litmus"], 0, "oracle litmus must be skipped when the expression is HEX-wrapped") + self.assertFalse(kb.reliabilityAlarm, "no false 'unreliable' alarm on a correct --hex dump") + + def test_shortcuts_skipped_under_binary_field(self): + value, calls = self._run(hexConvert=False, binaryField=True) + self.assertEqual(value, self._HEXED) + self.assertEqual(calls["guess"], 0, "shortcuts must be skipped for a --binary-fields column") + self.assertEqual(calls["litmus"], 0) + + class TestProcessCharHook(_InferenceCase): def test_process_char_applied_to_each_char(self): # kb.data.processChar transforms every assembled character