-
-
Notifications
You must be signed in to change notification settings - Fork 6.3k
Expand file tree
/
Copy pathtest_inference_engine.py
More file actions
213 lines (171 loc) · 9.54 KB
/
Copy pathtest_inference_engine.py
File metadata and controls
213 lines (171 loc) · 9.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
The blind-SQLi extraction engine (lib/techniques/blind/inference.py bisection).
This is the actual algorithm that pulls data out one character at a time over a
boolean/blind oracle - the heart of sqlmap. It is normally network-coupled, so
here we drive the REAL bisection() against a mock oracle: Request.queryPage is
replaced with a function that decodes the forged payload (we control the payload
template, so it is trivially parseable) and answers the comparison against a
known secret. If bisection's binary search, charset narrowing, or value assembly
regress, these go red - without a live target.
Also asserts the search is logarithmic (binary search), not a linear scan of the
character space.
"""
import os
import re
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.data import conf, kb
from lib.core.common import getCurrentThreadData
from lib.request.connect import Connect
import lib.techniques.blind.inference as inf
# bisection does: safeStringFormat(payload, (expression, idx, posValue)); '>' is the
# greater-char marker (swapped to '=' on the final equality check). We pass a parseable
# template so the mock oracle can recover (idx, operator, threshold).
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}
_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False,
"disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False}
class _EngineCase(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")
self._saved_counters = kb.get("counters")
for k, v in _CONF.items():
conf[k] = v
for k, v in _KB.items():
kb[k] = v
kb.data.processChar = None
# getCounter()/kb.counters is a cumulative per-technique query tally; reset it so each test
# measures only its own extraction (bisection returns the absolute counter, not a per-call delta)
kb.counters = {}
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
kb.counters = self._saved_counters
Connect.queryPage = self._saved_qp
inf.Request.queryPage = self._saved_qp
def _extract(self, secret, charsetType=None):
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)
td = getCurrentThreadData()
td.shared.value = ""
td.shared.index = [0]
td.shared.start = 0
td.shared.count = 0
count, value = inf.bisection(TEMPLATE, "SELECT secret", length=len(secret), charsetType=charsetType)
return value, count
class TestBisectionExtraction(_EngineCase):
# NOTE: the alpha / numeric / mixed cases are NOT redundant - getChar has per-class
# "first character" position heuristics (distinct branches for a-z, A-Z and 0-9 at
# inference.py ~331-336), so each character class exercises a different code path.
def test_single_char(self):
value, _ = self._extract("X")
self.assertEqual(value, "X")
def test_alpha(self):
value, _ = self._extract("AdminUser") # exercises the a-z / A-Z heuristic branch
self.assertEqual(value, "AdminUser")
def test_alphanumeric(self):
value, _ = self._extract("admin123")
self.assertEqual(value, "admin123")
def test_with_spaces_and_symbols(self):
value, _ = self._extract("p@ss W0rd!")
self.assertEqual(value, "p@ss W0rd!")
def test_numeric_string(self):
value, _ = self._extract("4815162342") # exercises the 0-9 heuristic branch
self.assertEqual(value, "4815162342")
def test_longer_value(self):
secret = "The quick brown fox 0123456789"
value, _ = self._extract(secret)
self.assertEqual(value, secret)
class TestUnicodeExpansion(_EngineCase):
"""charsetType=None starts with a 0..127 table and gradually expands it (shiftTable) to
reach higher code points. This test exercises the FIRST expansion step (code points
128..1023) via Latin-1 chars, where the per-byte oracle model is exact.
NOTE: kb.disableShiftTable is an INTENTIONAL session-level safety latch (sqlmap author's
design): once expansion runs all the way to the top - only reachable by a code point above
0xFFFFF, or by a misbehaving always-TRUE oracle - it disables further expansion to prevent
runaway / erroneous extraction. That is deliberate, so this test does NOT assert that
expansion survives across such an event.
(Code points >= 256 are retrieved/assembled byte-wise in real runs - decodeIntToUnicode
splits them into a byte sequence - so a simple ord()-based mock oracle only models the
single-byte range; those are out of scope here.)"""
def test_extracts_latin1_via_first_expansion(self):
for s in (u"caf\xe9", u"\xfcber", u"ni\xf1o", u"\xe9\xe8\xea\xeb"):
self.assertEqual(self._extract(s)[0], s, msg="expansion extraction failed for %r" % s)
class TestMysqlMultibyteExpansion(_EngineCase):
"""Regression guard for the shiftTable expansion ceiling (getChar, inference.py ~671).
MySQL's ORD() returns the byte-composite integer of a multibyte UTF-8 character (e.g. CJK
U+4E2D -> UTF-8 E4 B8 AD -> 0xE4B8AD = 14989485), and decodeIntToUnicode reconstructs the
character back from that integer. The gradual unicode expansion must therefore be able to
reach MySQL's full 3-byte ORD range (up to 0xEFBFBF = 15728575). A table whose ceiling
stops below that (as [2, 2, 3, 3, 3] did, ceiling 0xFFFFF = 1048575) silently truncates or
garbles every CJK and non-Latin >= U+0800 value on the most common DBMS - see issue #5171.
Unlike TestUnicodeExpansion (which models a single-byte oracle via ord()), this oracle
models MySQL ORD() as the char's UTF-8 bytes read big-endian, exercising the real high
code-point path end to end."""
def _extract_ord(self, secret):
def mysql_ord(ch):
value = 0
for octet in bytearray(ch.encode("utf-8")):
value = (value << 8) | octet
return value
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 = mysql_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)
td = getCurrentThreadData()
td.shared.value = ""
td.shared.index = [0]
td.shared.start = 0
td.shared.count = 0
count, value = inf.bisection(TEMPLATE, "SELECT secret", length=len(secret), charsetType=None)
return value, count
def test_extracts_3byte_cjk(self):
# U+4E2D/U+6587 are CJK; each returned None (truncation) / '?' under the regressed ceiling
for s in (u"\u4e2d", u"\u4e2d\u6587", u"A\u4e2dZ", u"\u4e2d123"):
self.assertEqual(self._extract_ord(s)[0], s, msg="3-byte extraction failed for %r" % s)
def test_extracts_near_max_3byte(self):
# U+FFFD -> UTF-8 EF BF BD -> ORD 0xEFBFBD, just under the 0xEFBFBF 3-byte ceiling
self.assertEqual(self._extract_ord(u"\ufffd")[0], u"\ufffd")
def test_2byte_still_extracts(self):
# guard: raising the ceiling must not disturb the (unchanged) 2-byte path
self.assertEqual(self._extract_ord(u"caf\xe9")[0], u"caf\xe9")
class TestSearchIsLogarithmic(_EngineCase):
def test_query_count_is_sublinear_in_charset(self):
# GOAL: catch a regression from binary search to a linear/per-codepoint scan.
# Observed cost is ~6-22 queries/char (it varies: the first-char heuristic's benefit
# depends on ambient kb/conf state, so a tighter bound would flake). A linear scan of the
# 128-char ASCII space would be ~128/char (~3840 for 30 chars). Bound at 40/char cleanly
# separates "logarithmic" (passes) from "linearized" (fails) without being flaky.
secret = "x" * 30
_, count = self._extract(secret)
self.assertLess(count, len(secret) * 40,
msg="bisection used %d queries for %d chars (~%.1f/char) - search regressed toward linear?"
% (count, len(secret), count / float(len(secret))))
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules