Minor patch

This commit is contained in:
Miroslav Štampar
2026-08-08 01:07:07 +02:00
parent aa8d9b9841
commit 7d2c33cd32
6 changed files with 252 additions and 4 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.8.15"
VERSION = "1.10.8.16"
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)
+4 -1
View File
@@ -22,6 +22,7 @@ from lib.utils.nonsql import InconclusiveError
from lib.utils.nonsql import INCONCLUSIVE_MARK
from lib.utils.nonsql import userDecision
from lib.utils.nonsql import resolveBit
from lib.utils.nonsql import stripReflection
from lib.utils.nonsql import sqlErrorPresent
from lib.utils.nonsql import blockedStatus
from lib.utils.nonsql import ratio as _ratio
@@ -179,7 +180,9 @@ def _send(place, parameter, value):
# true/false differential without a single expression being evaluated - that reported plain
# reflective search pages as injectable. On a blind target the payload is not in the page, so
# this is a no-op.
return removeReflectiveValues(page, value, suppressWarning=True) or ""
# Two layers on purpose: sqlmap's scan-wide heuristic (which can switch itself off) AND a plain
# deterministic strip that cannot. See stripReflection().
return stripReflection(removeReflectiveValues(page, value, suppressWarning=True), value) or ""
except Exception as ex:
logger.debug("LDAP probe request failed: %s" % getUnicode(ex))
return None
+11 -1
View File
@@ -23,7 +23,9 @@ from lib.utils.nonsql import ratio as _ratio
from lib.utils.nonsql import userOracleActive
from lib.utils.nonsql import InconclusiveError
from lib.utils.nonsql import INCONCLUSIVE_MARK
from thirdparty import six
from lib.utils.nonsql import resolveBit
from lib.utils.nonsql import stripReflection
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
@@ -372,7 +374,15 @@ def _send(place, parameter, segment=None, jsonValue=_UNSET):
# differential - and the wildcard always-true check - without a single operator being interpreted.
# That reported a plain reflective search page as a "Lucene query_string-compatible back-end". On a
# blind target the payload is not in the page, so this is a no-op.
return removeReflectiveValues(page, payload, suppressWarning=True) or ""
# Two layers on purpose: sqlmap's scan-wide heuristic (which can switch itself off) AND a plain
# deterministic strip that cannot. See stripReflection().
# Strip the VALUE as well as the whole parameter string: `payload` here is "q=*", while the page
# echoes just "*", so removing only the former leaves the reflection in place.
page = removeReflectiveValues(page, payload, suppressWarning=True)
for _reflected in (payload, segment, jsonValue if jsonValue is not _UNSET else None):
if isinstance(_reflected, six.string_types) and _reflected:
page = stripReflection(page, _reflected)
return page or ""
def _isError(page):
# a server-error status, a recognizable NoSQL error body, OR a recognized SQL/DBMS error marks a
+4 -1
View File
@@ -24,6 +24,7 @@ from lib.utils.nonsql import INCONCLUSIVE_MARK
from lib.utils.nonsql import userDecision
from lib.utils.nonsql import InconclusiveError
from lib.utils.nonsql import resolveBit
from lib.utils.nonsql import stripReflection
from lib.utils.nonsql import sqlErrorPresent
from lib.utils.nonsql import blockedStatus
from lib.utils.nonsql import ratio as _ratio
@@ -165,7 +166,9 @@ def _send(place, parameter, value):
# true/false differential, and even the XPath-only confirm battery, without a single expression
# ever being evaluated. That reported XSLT (and plain reflective) endpoints as XPath-injectable.
# On a genuinely blind target the payload is not in the page, so this is a no-op.
page = removeReflectiveValues(page, value, suppressWarning=True)
# Two layers on purpose: sqlmap's scan-wide heuristic (which can switch itself off) AND a plain
# deterministic strip that cannot. See stripReflection().
page = stripReflection(removeReflectiveValues(page, value, suppressWarning=True), value)
# A transport failure or a BLOCKED/ERROR status (5xx, 403/429 WAF/rate-limit) is NOT a usable
# oracle sample: returning "" for it would let a one-sided failure fake a true/false divergence
# (an empty body cannot be told apart from a dead connection). Signal it as None -> the boolean
+40
View File
@@ -14,7 +14,11 @@ in one place rather than drifting across six files.
import difflib
import re
from lib.core.common import urldecode
from lib.core.common import urlencode
from lib.core.convert import getUnicode
from lib.core.data import conf
from lib.core.settings import REFLECTED_VALUE_MARKER
from lib.core.settings import UPPER_RATIO_BOUND
from lib.parse.html import htmlParser
@@ -38,6 +42,42 @@ def ratio(first, second):
return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio()
def stripReflection(page, payload):
"""
Remove the payload from the page before any two responses are compared.
An endpoint that merely ECHOES the parameter returns a different page for every different payload, so
a true/false differential is satisfied without a single expression, filter or operator ever being
interpreted. That is not injection, and it is how plain reflective search pages were reported as
XPath / LDAP / NoSQL injectable.
Deliberately NOT lib.core.common.removeReflectiveValues: that one is a scan-wide heuristic which
switches ITSELF OFF after REFLECTIVE_MISS_THRESHOLD misses, after a regex timeout, and during
heuristic mode - so a detection guard built on it silently stops guarding mid-scan. This is a plain,
deterministic substring removal with no global state and no failure mode. The two are complementary,
and the engines apply both.
The raw, URL-decoded and URL-encoded forms are all removed: a payload travels encoded, and an
application may echo whichever of the three it happened to hold.
"""
if not page or not payload:
return page
retVal = getUnicode(page)
forms = set()
for form in (payload, urldecode(payload, convall=True), urlencode(payload, safe="")):
try:
forms.add(getUnicode(form))
except Exception:
pass
# longest first, so a shorter form cannot chop a longer one into unremovable pieces
for form in sorted(filter(None, forms), key=len, reverse=True):
if form in retVal:
retVal = retVal.replace(form, REFLECTED_VALUE_MARKER)
return retVal
def blockedStatus(code):
"""True when an HTTP status means the response is blocked/errored (a 5xx, or a WAF/rate-limit
403/429) and so is not a usable oracle sample. `_send()` implementations return None for these
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Adversarial coverage for reflection removal.
Two independent mechanisms exist and the non-SQL engines rely on BOTH:
lib.core.common.removeReflectiveValues - the scan-wide heuristic. Powerful (it reassembles a payload
that the page broke apart) but it has global state and
SWITCHES ITSELF OFF: after REFLECTIVE_MISS_THRESHOLD misses,
after a regex timeout, and during heuristic mode.
lib.utils.nonsql.stripReflection - a plain deterministic removal with no global state, added
precisely because a detection guard must not stop guarding
halfway through a scan.
The tests below pin the behaviour of each, and - more importantly - pin the blind spots of the first one,
so nobody builds another guard on it without knowing where it does nothing.
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import removeReflectiveValues
from lib.core.data import kb
from lib.core.settings import REFLECTED_VALUE_MARKER
from lib.core.settings import REFLECTIVE_MISS_THRESHOLD
from lib.utils.nonsql import stripReflection
PAYLOAD = u"x') or true() or ('"
def _reset():
kb.reflectiveMechanism = True
kb.heuristicMode = False
kb.reflectiveCounters = {"HIT": 0, "MISS": 0}
def _removed(content, payload):
out = removeReflectiveValues(content, payload, suppressWarning=True)
return out != content and REFLECTED_VALUE_MARKER in (out or "")
class RemoveReflectiveValuesTest(unittest.TestCase):
def setUp(self):
_reset()
def test_plain_reflection_is_removed(self):
self.assertTrue(_removed(u"you searched for: %s" % PAYLOAD, PAYLOAD))
def test_reflection_survives_case_change(self):
self.assertTrue(_removed(u"YOU SEARCHED FOR: %s" % PAYLOAD.upper(), PAYLOAD))
def test_reflection_split_by_markup_is_removed(self):
"""The heuristic's real strength: it reassembles a payload the page broke apart."""
self.assertTrue(_removed(u"search: x') or <b>true()</b> or ('", PAYLOAD))
def test_html_encoded_reflection_is_removed(self):
self.assertTrue(_removed(u"you searched for: x&#39;) or true() or (&#39;", PAYLOAD))
def test_absent_payload_leaves_content_untouched(self):
content = u"nothing to see here"
self.assertEqual(removeReflectiveValues(content, PAYLOAD, suppressWarning=True), content)
def test_empty_inputs_are_safe(self):
self.assertEqual(removeReflectiveValues(None, PAYLOAD), None)
self.assertEqual(removeReflectiveValues(u"abc", None), u"abc")
self.assertEqual(removeReflectiveValues(u"", u""), u"")
def test_word_only_payload_cannot_explode_the_page(self):
"""A payload of pure word characters yields an empty needle; replacing on it would insert the
marker between every character."""
out = removeReflectiveValues(u"aaa bbb aaa", u"aaa", suppressWarning=True)
self.assertNotIn("%s%s" % (REFLECTED_VALUE_MARKER, REFLECTED_VALUE_MARKER), out or "")
class RemoveReflectiveValuesBlindSpotTest(unittest.TestCase):
"""These document where the scan-wide heuristic does NOTHING. They are the reason a second,
deterministic guard exists - not bugs to be 'fixed' here."""
def setUp(self):
_reset()
def test_blind_spot_purely_alphanumeric_payload(self):
"""filterStringValue() leaves such a payload unchanged, so the whole routine short-circuits."""
self.assertFalse(_removed(u"you searched for: abcdefghij", u"abcdefghij"))
def test_blind_spot_bytes_content(self):
"""It requires text. A byte string is returned untouched - identical behaviour on py2 and py3,
and the reason every caller must hand it a decoded page."""
self.assertFalse(_removed(b"you searched for: x", u"x"))
def test_blind_spot_disabled_mechanism(self):
"""It disables itself on a regex timeout and after REFLECTIVE_MISS_THRESHOLD misses."""
kb.reflectiveMechanism = False
self.assertFalse(_removed(u"you searched for: %s" % PAYLOAD, PAYLOAD))
def test_blind_spot_heuristic_mode(self):
kb.heuristicMode = True
self.assertFalse(_removed("you searched for: %s" % PAYLOAD, PAYLOAD))
def test_miss_threshold_is_finite(self):
"""A guard built only on this stops guarding after this many non-reflective responses."""
self.assertTrue(0 < REFLECTIVE_MISS_THRESHOLD < 1000)
class StripReflectionTest(unittest.TestCase):
"""The deterministic guard: no global state, no timeout, no self-disabling."""
def setUp(self):
_reset()
def test_removes_the_payload(self):
out = stripReflection(u"you searched for: %s" % PAYLOAD, PAYLOAD)
self.assertNotIn(PAYLOAD, out)
self.assertIn(REFLECTED_VALUE_MARKER, out)
def test_removes_the_url_encoded_form(self):
out = stripReflection(u"you searched for: x%27%29%20or%20true%28%29", u"x') or true()")
self.assertIn(REFLECTED_VALUE_MARKER, out)
def test_two_different_payloads_collapse_to_the_same_page(self):
"""This is the whole point: a pure echo endpoint must stop looking like a boolean oracle."""
true_page = stripReflection(u"you searched for: x') or true() or ('", u"x') or true() or ('")
false_page = stripReflection(u"you searched for: x') and false() and ('", u"x') and false() and ('")
self.assertEqual(true_page, false_page)
def test_a_real_differential_is_preserved(self):
"""A genuine oracle differs in the APPLICATION's output, not in the echoed payload, so stripping
must not erase it."""
true_page = stripReflection(u"results: luther, fluffy, wu [%s]" % PAYLOAD, PAYLOAD)
false_page = stripReflection(u"results: none [%s]" % PAYLOAD, PAYLOAD)
self.assertNotEqual(true_page, false_page)
def test_does_not_depend_on_global_state(self):
kb.reflectiveMechanism = False
kb.heuristicMode = True
kb.reflectiveCounters = {"HIT": 0, "MISS": REFLECTIVE_MISS_THRESHOLD * 10}
self.assertIn(REFLECTED_VALUE_MARKER, stripReflection(u"echo: %s" % PAYLOAD, PAYLOAD))
def test_covers_the_alphanumeric_blind_spot(self):
self.assertIn(REFLECTED_VALUE_MARKER, stripReflection(u"you searched for: abcdefghij", u"abcdefghij"))
def test_empty_and_missing_inputs_are_safe(self):
self.assertEqual(stripReflection(None, PAYLOAD), None)
self.assertEqual(stripReflection(u"abc", None), u"abc")
self.assertEqual(stripReflection(u"abc", u""), u"abc")
self.assertEqual(stripReflection(u"", PAYLOAD), u"")
def test_absent_payload_leaves_content_identical(self):
content = u"nothing to see here"
self.assertEqual(stripReflection(content, PAYLOAD), content)
def test_every_occurrence_is_removed(self):
out = stripReflection(u"%s middle %s" % (PAYLOAD, PAYLOAD), PAYLOAD)
self.assertNotIn(PAYLOAD, out)
self.assertEqual(out.count(REFLECTED_VALUE_MARKER), 2)
class EnginesUseBothGuardsTest(unittest.TestCase):
"""Pins the wiring. A guard that only one engine applies is how this class of false positive spread
across three shipped engines unnoticed."""
def _source(self, *parts):
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", *parts)) as f:
return f.read()
def test_reflective_engines_apply_both(self):
for engine in ("xpath", "ldap", "nosql"):
source = self._source("lib", "techniques", engine, "inject.py")
self.assertIn("removeReflectiveValues", source, engine)
self.assertIn("stripReflection", source, engine)
def test_xslt_proves_evaluation_instead(self):
"""--xslt needs no reflection filter: its sentinel only exists once concat() has run, so an echo
can never satisfy it. That is the stronger design."""
source = self._source("lib", "techniques", "xslt", "inject.py")
self.assertIn("def _captured", source)
self.assertIn("reflected verbatim", source)
if __name__ == "__main__":
unittest.main()