Skip to content

Commit 2afa924

Browse files
committed
Adding new unit test
1 parent 0630556 commit 2afa924

2 files changed

Lines changed: 213 additions & 1 deletion

File tree

lib/core/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.7.238"
23+
VERSION = "1.10.7.239"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

tests/test_jitter_stress.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
5+
See the file 'LICENSE' for copying permission
6+
7+
Adversarial JITTER stress harness for time-based blind extraction.
8+
9+
Drives the REAL bisection() + REAL wasLastResponseDelayed() against a mock oracle that returns a
10+
simulated RESPONSE DURATION (base + jitter + timeSec-if-condition-true) instead of a boolean - so
11+
the actual statistical delay-decision and its re-validation run under controlled network jitter,
12+
with NO real sleeping (thousands of extractions per second, fully deterministic per seed).
13+
14+
Two tiers:
15+
* TestJitterRegression - ALWAYS runs. Low/mild jitter MUST extract perfectly. A real regression
16+
guard for the time-based decision stack (deterministic, fast, non-flaky).
17+
* TestJitterStressSweep - OPT-IN (set env SQLMAP_JITTER_STRESS=1). The adversarial sweeps that map
18+
the failure surface (Gaussian sigma, heavy-tailed spikes). Informational
19+
+ loose bounds only; kept out of normal CI to avoid slowness/flakiness.
20+
21+
Run the sweep on demand: SQLMAP_JITTER_STRESS=1 python -m unittest tests.test_jitter_stress -v
22+
"""
23+
24+
import os
25+
import random
26+
import re
27+
import sys
28+
import unittest
29+
30+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31+
from _testutils import bootstrap, set_dbms, reset_dbms
32+
bootstrap()
33+
34+
from lib.core.data import conf, kb
35+
from lib.core.common import getCurrentThreadData, setTechnique
36+
from lib.core.datatype import AttribDict
37+
from lib.core.enums import ADJUST_TIME_DELAY, PAYLOAD
38+
from lib.request.connect import Connect
39+
import lib.techniques.blind.inference as inf
40+
41+
_TEMPLATE = "EXPR=%s IDX=%d CMP>%d"
42+
_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)")
43+
_TIMESEC = 5.0
44+
_BASE = 0.10 # base (non-delay) round-trip latency, seconds
45+
_STRESS = os.environ.get("SQLMAP_JITTER_STRESS")
46+
47+
48+
def _timeVector():
49+
d = AttribDict()
50+
d.payload = _TEMPLATE; d.where = 1; d.vector = _TEMPLATE
51+
d.comment = ""; d.templatePayload = None; d.matchRatio = None
52+
d.trueCode = None; d.falseCode = None
53+
return d
54+
55+
56+
class _JitterBase(unittest.TestCase):
57+
_CONF = ("threads", "api", "verbose", "direct", "disableStats", "timeSec", "predictOutput",
58+
"hexConvert", "charset", "firstChar", "lastChar")
59+
_KB = ("responseTimeMode", "adjustTimeDelay", "laggingChecked", "partRun", "safeCharEncode",
60+
"bruteMode", "fileReadMode", "disableShiftTable", "prependFlag", "originalTimeDelay",
61+
"counters", "responseTimes")
62+
63+
def setUp(self):
64+
self._saved_conf = {k: conf.get(k) for k in self._CONF}
65+
self._saved_kb = {k: kb.get(k) for k in self._KB}
66+
self._saved_inj = kb.injection.data
67+
self._saved_qp = Connect.queryPage
68+
self._saved_technique = getCurrentThreadData().technique
69+
70+
def tearDown(self):
71+
for k, v in self._saved_conf.items():
72+
conf[k] = v
73+
for k, v in self._saved_kb.items():
74+
kb[k] = v
75+
kb.injection.data = self._saved_inj
76+
Connect.queryPage = self._saved_qp
77+
inf.Request.queryPage = self._saved_qp
78+
setTechnique(self._saved_technique) # setTechnique() sets a thread-local; restore so it can't leak into other modules
79+
80+
def _configure(self, baselineJitter, rng, nBaseline=30):
81+
set_dbms("MySQL")
82+
conf.threads = 1; conf.api = False; conf.verbose = 0; conf.direct = False
83+
conf.disableStats = False; conf.timeSec = _TIMESEC; conf.predictOutput = False
84+
conf.hexConvert = False; conf.charset = None; conf.firstChar = None; conf.lastChar = None
85+
kb.responseTimeMode = None
86+
kb.adjustTimeDelay = ADJUST_TIME_DELAY.DISABLE # never prompt / never mutate timeSec
87+
kb.laggingChecked = True
88+
kb.partRun = None; kb.safeCharEncode = False; kb.bruteMode = False
89+
kb.fileReadMode = False; kb.disableShiftTable = False; kb.prependFlag = False
90+
kb.originalTimeDelay = _TIMESEC; kb.counters = {}
91+
kb.injection.data = {PAYLOAD.TECHNIQUE.TIME: _timeVector()}
92+
setTechnique(PAYLOAD.TECHNIQUE.TIME)
93+
# jitter is always ADDITIVE (network delays only slow a response, never speed it below base),
94+
# so the baseline is right-skewed with a floor at base - like real kb.responseTimes, and with
95+
# no fake point-mass at 0 that a clamp (max(0.0, ..)) would create and that would skew stats
96+
kb.responseTimes = {None: [_BASE + abs(baselineJitter(rng)) for _ in range(nBaseline)]}
97+
kb.data.processChar = None
98+
99+
def _extract(self, secret, jitter, rng):
100+
from lib.core.common import wasLastResponseDelayed
101+
102+
def oracle(payload=None, timeBasedCompare=False, **kwargs):
103+
td = getCurrentThreadData()
104+
m = _PARSE.search(payload or "")
105+
if not m:
106+
td.lastQueryDuration = _BASE + abs(jitter(rng))
107+
return False
108+
idx, op, thr = int(m.group(1)), m.group(2), int(m.group(3))
109+
ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0
110+
cond = (ch > thr) if op == ">" else (ch == thr)
111+
if "NOT(" in payload:
112+
cond = not cond
113+
td.lastQueryDuration = _BASE + abs(jitter(rng)) + (_TIMESEC if cond else 0.0)
114+
return wasLastResponseDelayed() if timeBasedCompare else cond
115+
116+
Connect.queryPage = staticmethod(oracle)
117+
inf.Request.queryPage = staticmethod(oracle) # Note: staticmethod on BOTH (py2 makes a bare function an unbound method)
118+
td = getCurrentThreadData()
119+
td.shared.value = ""; td.shared.index = [0]; td.shared.start = 0; td.shared.count = 0
120+
_, value = inf.bisection(_TEMPLATE, "SELECT secret", length=len(secret), charsetType=None)
121+
return value
122+
123+
def _rate(self, secret, jitter, trials=40, seed0=1000):
124+
ok = 0
125+
for t in range(trials):
126+
rng = random.Random(seed0 + t)
127+
self._configure(jitter, rng)
128+
try:
129+
ok += (self._extract(secret, jitter, rng) == secret)
130+
except Exception:
131+
pass
132+
return ok, trials
133+
134+
135+
def _gaussian(sigma):
136+
return lambda rng: rng.gauss(0, sigma)
137+
138+
139+
def _spike(sigma, p, mag):
140+
def f(rng):
141+
v = rng.gauss(0, sigma)
142+
if rng.random() < p:
143+
v += mag
144+
return v
145+
return f
146+
147+
148+
class TestJitterRegression(_JitterBase):
149+
"""Always-on, deterministic, non-flaky: under low/mild jitter (7*sigma well below timeSec and no
150+
heavy tail) the time-based stack MUST reconstruct the value exactly, every seed."""
151+
152+
SECRET = "Str0ng!"
153+
154+
def test_no_jitter_is_perfect(self):
155+
ok, n = self._rate(self.SECRET, _gaussian(0.0))
156+
self.assertEqual(ok, n, "time-based extraction must be flawless with zero jitter (%d/%d)" % (ok, n))
157+
158+
def test_mild_gaussian_is_perfect(self):
159+
# sigma=0.3 -> false bits at base+|N(0,0.3)| (<~1s) stay well under the threshold, << timeSec=5
160+
ok, n = self._rate(self.SECRET, _gaussian(0.3))
161+
self.assertEqual(ok, n, "mild gaussian jitter must not corrupt extraction (%d/%d)" % (ok, n))
162+
163+
def test_baseline_spike_does_not_hide_a_genuine_delay(self):
164+
# A single latency spike captured in the response-time model must not raise the delay
165+
# threshold (avg + 7*stdev) so high that a real timeSec delay is missed. Deterministic.
166+
from lib.core.common import wasLastResponseDelayed, average, stdev
167+
from lib.core.settings import TIME_STDEV_COEFF
168+
169+
set_dbms("MySQL")
170+
conf.direct = False; conf.disableStats = False; conf.timeSec = _TIMESEC
171+
kb.adjustTimeDelay = ADJUST_TIME_DELAY.DISABLE
172+
kb.responseTimeMode = None
173+
bulk = [0.15, 0.25] * 15 # clean model, small non-zero stdev
174+
kb.responseTimes = {None: bulk + [8.0]} # one 8s spike poisons the baseline
175+
td = getCurrentThreadData()
176+
td.lastQueryDuration = _BASE + _TIMESEC # a genuine time-based delay (~5.1s)
177+
178+
raw = kb.responseTimes[None] # the un-trimmed model WOULD miss it (fix is load-bearing)
179+
self.assertLess(td.lastQueryDuration, average(raw) + TIME_STDEV_COEFF * stdev(raw))
180+
self.assertTrue(wasLastResponseDelayed()) # with spike-trimming the delay is recognized
181+
182+
183+
@unittest.skipUnless(_STRESS, "adversarial jitter sweep is opt-in (set SQLMAP_JITTER_STRESS=1)")
184+
class TestJitterStressSweep(_JitterBase):
185+
"""Opt-in failure-surface map. Prints correctness vs jitter and asserts only loose, non-flaky
186+
invariants (clean case perfect, degradation is monotone-ish). Use to evaluate hardening changes."""
187+
188+
SECRET = "Str0ng!"
189+
190+
def test_gaussian_sweep(self):
191+
print("\n[jitter] Gaussian sigma sweep (timeSec=%.0f, base=%.2f):" % (_TIMESEC, _BASE))
192+
for sigma in (0.0, 0.3, 0.5, 0.7, 0.9, 1.2):
193+
ok, n = self._rate(self.SECRET, _gaussian(sigma))
194+
print(" sigma=%.2fs -> %d/%d (%3.0f%%)" % (sigma, ok, n, 100.0 * ok / n))
195+
if sigma == 0.0:
196+
self.assertEqual(ok, n)
197+
198+
def test_heavy_tailed_spike_sweep(self):
199+
print("\n[jitter] Heavy-tailed spike sweep (base sigma=0.2, spike=+8s):")
200+
for p in (0.0, 0.01, 0.03, 0.05, 0.10):
201+
ok, n = self._rate(self.SECRET, _spike(0.2, p, 8.0))
202+
print(" spike_p=%.2f -> %d/%d (%3.0f%%)" % (p, ok, n, 100.0 * ok / n))
203+
if p == 0.0:
204+
self.assertEqual(ok, n)
205+
206+
207+
if __name__ == "__main__":
208+
unittest.main(verbosity=2)
209+
210+
211+
def tearDownModule():
212+
reset_dbms()

0 commit comments

Comments
 (0)