Couple of fixes

This commit is contained in:
Miroslav Štampar 2026-08-17 22:20:42 +02:00
parent 77ee160875
commit 848ba68ccf
11 changed files with 53 additions and 11 deletions

View file

@ -105,7 +105,7 @@ def _selftest():
esp.discover()
got = esp.extract("(SELECT name FROM users WHERE id=1)")
assert got == "Admin-42", "quorum extraction (seed %d) failed: %r" % (seed, got)
print(" quorum=6 under 20%% noisy oracle (12%% err + 8%% lies) -> 'Admin-42' across 4 seeds")
print(" quorum=6 under 20% noisy oracle (12% err + 8% lies) -> 'Admin-42' across 4 seeds")
# -- integrity guards (peer-review round 4) --------------------------------
# empty/NULL are falsey; a bounded prefix (incl. limit=0) is incomplete+truncated

View file

@ -2198,7 +2198,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.chars = AttribDict()
kb.chars.delimiter = randomStr(length=6, lowercase=True)
# NOTE: markers have to be mutually distinct (e.g. equal start/stop makes the delimited output ambiguous, while equal replacement markers make _errorReplaceChars() restore the wrong character)
# NOTE: markers have to be mutually distinct (e.g. equal start/stop makes the delimited output ambiguous, while equal replacement markers make _errorReplaceChars() restore the wrong character). Also, none of the inner letters may be the boundary character itself, as that makes a marker contain a shorter one (e.g. 'qzqxq' carrying 'qzq')
_ = set()
while len(_) < 2:
_.add(randomStr(length=3, alphabet=KB_CHARS_LOW_FREQUENCY_ALPHABET))
@ -2207,6 +2207,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
_ = set()
while len(_) < 4:
_.add(randomStr(length=1, lowercase=True))
_.discard(KB_CHARS_BOUNDARY_CHAR)
kb.chars.at, kb.chars.space, kb.chars.dollar, kb.chars.hash_ = ("%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, __, KB_CHARS_BOUNDARY_CHAR) for __ in _)
kb.checkWafMode = False

View file

@ -114,6 +114,7 @@ optDict = {
"notString": "string",
"regexp": "string",
"code": "integer",
"lengths": "boolean",
"smart": "boolean",
"textOnly": "boolean",
"titles": "boolean",

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.44"
VERSION = "1.10.8.45"
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)
@ -612,7 +612,7 @@ MYSQL_UNION_VALUE_CAST = "CONVERT(%s USING utf8mb4) COLLATE utf8mb4_bin"
# Row count at/above which keyset (seek) pagination is used automatically for table dumps when a usable integer-key cursor exists (smaller tables keep the plain LIMIT/OFFSET path; '--keyset' forces it regardless of size)
KEYSET_MIN_ROWS = 1000
# Number of consecutive Huffman (set-membership) character attempts allowed to decline/escape without a single validated success before the technique latches itself off (safety against trimmed/blocked long IN() payloads)
# Number of Huffman (set-membership) character attempts made before their escape ratio is judged; at/above it, escapes reaching half of all attempts latch the technique off (safety against trimmed/blocked long IN() payloads)
HUFFMAN_PROBE_LIMIT = 8
# Cold-start (prior) weights for the order-0 Huffman model used in adaptive blind retrieval. Gently
@ -1776,8 +1776,8 @@ BRUTE_DOC_ROOT_TARGET_MARK = "%TARGET%"
# Character used as a boundary in kb.chars (preferably less frequent letter)
KB_CHARS_BOUNDARY_CHAR = 'q'
# Letters of lower frequency used in kb.chars
KB_CHARS_LOW_FREQUENCY_ALPHABET = "zqxjkvbp"
# Letters of lower frequency used in kb.chars (NOTE: without the boundary character itself, so that no marker can contain a shorter one)
KB_CHARS_LOW_FREQUENCY_ALPHABET = "zxjkvbp"
# Printable bytes
PRINTABLE_BYTES = set(bytes(string.printable, "ascii") if six.PY3 else string.printable)

View file

@ -84,8 +84,8 @@ from lib.utils.safe2bin import safecharencode
from lib.utils.xrange import xrange
from thirdparty import six
# Sentinel returned by the opt-in Huffman retrieval (--huffman) meaning "this character is
# outside the ASCII model (e.g. multi-byte/Unicode) - defer to the classic bisection".
# Sentinel returned by the (default-on, '--no-huffman') Huffman retrieval meaning "this character
# is outside the ASCII model (e.g. multi-byte/Unicode) - defer to the classic bisection".
_HUFFMAN_FALLBACK = object()
# Cache of character-level Markov priors keyed by (order, scale, dbms); built once per process

View file

@ -591,6 +591,13 @@ def _confirm(profile, expression, value, length):
# 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:
# a NULL value has no length, so the comparison above is NULL (i.e. not selected) no matter
# what was read - exactly how a channel that drops rows fails it. One extra probe tells the
# two apart, so a column with NULLs in it does not retire an otherwise working channel
if _ask(profile, "(%s) IS NULL" % expression) is True:
_debug("value is NULL, so its length cannot be confirmed")
return None
_debug("length of the extracted value did not confirm")
return False
elif length != len(value):

View file

@ -257,7 +257,8 @@ def _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, count
caps. K is halved adaptively if a chunk response still gets truncated. Returns a BigArray of
rows, or None to let the caller fall back to the regular per-row UNION path.
Same DBMS coverage as the single-shot JSON-agg (per-DBMS aggregate + windowing); others -> None.
Covers the single-shot JSON-agg back-ends that also have a windowing form here (i.e. all of them
except Oracle and MSSQL, whose aggregates are built differently); others -> None.
"""
dbms = Backend.getIdentifiedDbms()

View file

@ -21,6 +21,9 @@ def tamper(payload, **kwargs):
"""
Replaces (MsSQL) statement separator ';' with a blank character
Requirement:
* Microsoft SQL Server
Notes:
* Useful to bypass filters/WAFs blocking the ';' character, as
Transact-SQL does not require any separator between statements

View file

@ -32,8 +32,8 @@ def tamper(payload, **kwargs):
* Useful to bypass web application firewalls, as the resulting payload contains neither
the SELECT nor the FROM keyword. Verified against ModSecurity v3 with the OWASP CRS
(paranoia level 1, blocking mode), where the plain counterpart scores 20 anomaly
points and is blocked, while the rewritten payload scores 0 and is answered with
HTTP 200
points and is blocked, while the rewritten payload drops to 5 (rule 942360 alone,
see below) or to 0 when chained with tamper script 'odbcbrace'
* The rule doing most of the work there is 942270 '(?i)union.*?select.*?from', which
needs all three keywords in that order. TABLE <table> is a complete query block on
its own (sql_yacc.yy query_primary has exactly three alternatives: SELECT, VALUES and

View file

@ -58,6 +58,19 @@ class KbCharsDistinctTest(unittest.TestCase):
self.assertEqual(len(set(drawn)), len(MARKERS),
msg="colliding kb.chars markers on round %d: %s" % (i, dict(zip(MARKERS, drawn))))
def test_markers_never_contain_each_other(self):
# whole-string distinctness is not enough: the boundary character wrapping every marker used
# to be drawn for the inner letters as well, so a start marker could render as 'qzqxq', which
# carries the perfectly legal replacement marker 'qzq' (and 'qxq') inside it
for i in range(ROUNDS):
_setKnowledgeBaseAttributes()
drawn = [getattr(kb.chars, _) for _ in MARKERS]
for one in drawn:
for other in drawn:
if one is not other:
self.assertNotIn(other, one,
msg="kb.chars marker %r contains %r on round %d" % (one, other, i))
def test_markers_keep_their_shape(self):
# the fix must not change the on-the-wire length of a payload
for _ in range(ROUNDS // 100):

View file

@ -30,6 +30,7 @@ bootstrap()
from lib.core.data import conf, kb, queries
from lib.core.enums import DBMS
from lib.core.enums import PAYLOAD
from lib.core.settings import MULTIBIT_MAX_FAILURES
from lib.core.settings import MULTIBIT_NARROW
from lib.core.settings import MULTIBIT_WIDEN
from plugins.dbms.sqlite import SQLiteMap # registers the SQLite escaper used by the read-back
@ -236,6 +237,21 @@ class MultibitTest(unittest.TestCase):
good = target.secret("plain ascii value that must still come back")
self.assertEqual(self._read(target, good, len(good)), good, "condemned after %r" % first)
def test_null_values_do_not_condemn_the_channel(self):
# a NULL column has no length, so the read-back's length probe comes back unselected no matter
# what the channel did - i.e. exactly the way a channel dropping rows fails it. That is the
# data's doing, and a handful of NULLs in one dump must not retire a working channel
first = "first value is perfectly readable ascii"
target = _Target(first)
self.assertEqual(self._read(target, first, len(first)), first)
for _ in range(MULTIBIT_MAX_FAILURES + 1):
target.secret(None)
self.assertIsNone(self._read(target, ""))
good = target.secret("and the channel is still fine afterwards")
self.assertEqual(self._read(target, good, len(good)), good)
def test_unreadable_value_mid_run_keeps_the_channel(self):
target = _Target("first value is perfectly readable ascii")
self.assertEqual(self._read(target, "first value is perfectly readable ascii", 39), "first value is perfectly readable ascii")