66
77Adversarial JITTER stress harness for time-based blind extraction.
88
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).
9+ Drives the REAL bisection() + REAL wasLastResponseDelayed() + REAL validateChar() re-validation
10+ against a mock oracle that returns a simulated RESPONSE DURATION (base + jitter + timeSec-if-condition-
11+ true) instead of a boolean - so the whole time-based decision stack runs under controlled network
12+ jitter, with NO real sleeping (thousands of extractions per second, fully deterministic per seed).
13+ The delimiter-wrapped template is what lets validateChar's per-char '!=' re-check actually fire (it is
14+ sqlmap's main defense against a single spike faking one bit); without it the harness is far too harsh.
1315
1416Two 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 .
17+ * TestJitterRegression - ALWAYS runs. Low/mild jitter MUST extract perfectly, and a spike in the
18+ baseline model MUST NOT hide genuine delays. Deterministic , fast, non-flaky.
19+ * TestJitterStressSweep - OPT-IN (set env SQLMAP_JITTER_STRESS=1). Adversarial sweeps (Gaussian
20+ sigma, heavy-tailed spikes) mapping where extraction finally degrades.
21+ Informational + loose bounds only; kept out of normal CI (slow/noisy) .
2022
2123Run the sweep on demand: SQLMAP_JITTER_STRESS=1 python -m unittest tests.test_jitter_stress -v
2224"""
3537from lib .core .common import getCurrentThreadData , setTechnique
3638from lib .core .datatype import AttribDict
3739from lib .core .enums import ADJUST_TIME_DELAY , PAYLOAD
40+ from lib .core .settings import PAYLOAD_DELIMITER
3841from lib .request .connect import Connect
3942import lib .techniques .blind .inference as inf
4043
41- _TEMPLATE = "EXPR=%s IDX=%d CMP>%d"
42- _PARSE = re .compile (r"IDX=(\d+) CMP(.)(\d+)" )
44+ # The comparison must sit BETWEEN PAYLOAD_DELIMITERs: validateChar (inference.py) rewrites '>' to '!='
45+ # with a regex anchored on the delimiters, and without them that per-char re-validation silently
46+ # no-ops (defeating sqlmap's main per-request-spike defense and making this harness far too pessimistic).
47+ _TEMPLATE = "%sEXPR=%%s IDX=%%d CMP>%%d%s" % (PAYLOAD_DELIMITER , PAYLOAD_DELIMITER )
48+ _PARSE = re .compile (r"IDX=(\d+) CMP(!=|=|>)(\d+)" ) # bisection '>'/'=' plus validateChar's '!='
4349_TIMESEC = 5.0
4450_BASE = 0.10 # base (non-delay) round-trip latency, seconds
4551_STRESS = os .environ .get ("SQLMAP_JITTER_STRESS" )
@@ -107,7 +113,7 @@ def oracle(payload=None, timeBasedCompare=False, **kwargs):
107113 return False
108114 idx , op , thr = int (m .group (1 )), m .group (2 ), int (m .group (3 ))
109115 ch = ord (secret [idx - 1 ]) if 0 <= idx - 1 < len (secret ) else 0
110- cond = (ch > thr ) if op == ">" else (ch == thr )
116+ cond = (ch > thr ) if op == ">" else (ch != thr ) if op == "!=" else ( ch == thr )
111117 if "NOT(" in payload :
112118 cond = not cond
113119 td .lastQueryDuration = _BASE + abs (jitter (rng )) + (_TIMESEC if cond else 0.0 )
@@ -183,11 +189,14 @@ def test_baseline_spike_does_not_hide_a_genuine_delay(self):
183189@unittest .skipUnless (_STRESS , "adversarial jitter sweep is opt-in (set SQLMAP_JITTER_STRESS=1)" )
184190class TestJitterStressSweep (_JitterBase ):
185191 """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."""
192+ invariants (clean case perfect). Use to evaluate hardening changes."""
187193
188194 SECRET = "Str0ng!"
189195
190196 def test_gaussian_sweep (self ):
197+ # Continuous jitter: degrades only once sigma approaches timeSec/7 (7*stdev threshold nears the
198+ # real delay). That is the FUNDAMENTAL limit of the statistic - the answer there is a larger
199+ # timeSec (--time-sec), not a code change; shown here so a regression that degrades it earlier is visible.
191200 print ("\n [jitter] Gaussian sigma sweep (timeSec=%.0f, base=%.2f):" % (_TIMESEC , _BASE ))
192201 for sigma in (0.0 , 0.3 , 0.5 , 0.7 , 0.9 , 1.2 ):
193202 ok , n = self ._rate (self .SECRET , _gaussian (sigma ))
@@ -196,8 +205,11 @@ def test_gaussian_sweep(self):
196205 self .assertEqual (ok , n )
197206
198207 def test_heavy_tailed_spike_sweep (self ):
208+ # One-off +8s spikes: baseline-trim (stripTimeOutliers) keeps the model clean and validateChar's
209+ # '!=' re-check catches a spike that fakes a single bit, so extraction stays ~perfect until an
210+ # absurd spike rate (a fifth of all requests). This is the payoff of both defenses together.
199211 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 ):
212+ for p in (0.0 , 0.01 , 0.03 , 0.05 , 0.10 , 0.20 ):
201213 ok , n = self ._rate (self .SECRET , _spike (0.2 , p , 8.0 ))
202214 print (" spike_p=%.2f -> %d/%d (%3.0f%%)" % (p , ok , n , 100.0 * ok / n ))
203215 if p == 0.0 :
0 commit comments