|
51 | 51 | _PARSE = re.compile(r"IDX=(\d+) CMP(!=|=|>)(\d+)") |
52 | 52 | _SECRET = "Str0ng!" |
53 | 53 | _STRING = "luther" |
54 | | -_TRUE_BODY = "<html><body>welcome %s, here is your dashboard with 12 private items</body></html>" % _STRING |
55 | | -_FALSE_BODY = "<html><body>invalid credentials, no such record, please retry</body></html>" |
| 54 | +# realistic-size bodies (shared nav/footer boilerplate) so the "resembles neither model" anomaly guard |
| 55 | +# behaves as on a real page: benign dynamic noise is proportionally tiny (stays a match), while a junk |
| 56 | +# interstitial/maintenance/empty body clearly matches neither |
| 57 | +_BOILER = "<html><head><title>Acme Portal</title></head><body><nav>home about contact help terms privacy account</nav><div class=main>" * 8 |
| 58 | +_FOOT = "</div><footer>(c) Acme Corp - all rights reserved - support@acme.example - v4.2</footer></body></html>" * 8 |
| 59 | +_TRUE_BODY = _BOILER + "welcome %s, dashboard: orders profile settings billing (12 items)" % _STRING + _FOOT |
| 60 | +_FALSE_BODY = _BOILER + "invalid credentials, no such record found, please retry" + _FOOT |
| 61 | +_INTERSTITIAL = "<html><body>Just a moment... checking your browser before access (DDoS protection)</body></html>" |
56 | 62 | _STRESS = os.environ.get("SQLMAP_JITTER_STRESS") |
57 | 63 |
|
58 | 64 |
|
@@ -97,7 +103,7 @@ class _BooleanJitterBase(unittest.TestCase): |
97 | 103 | _KB = ("negativeLogic", "nullConnection", "errorIsNone", "pageTemplate", "matchRatio", "heavilyDynamic", |
98 | 104 | "pageStructurallyStable", "skipSeqMatcher", "pageEncoding", "partRun", "safeCharEncode", |
99 | 105 | "bruteMode", "fileReadMode", "disableShiftTable", "prependFlag", "timeless", "counters", |
100 | | - "originalCode", "originalPage") |
| 106 | + "originalCode", "originalPage", "trueTemplate", "falseTemplate", "dynamicMarkings") |
101 | 107 |
|
102 | 108 | def setUp(self): |
103 | 109 | self._saved_conf = {k: conf.get(k) for k in self._CONF} |
@@ -129,6 +135,8 @@ def _configure(self): |
129 | 135 | kb.partRun = None; kb.safeCharEncode = False; kb.bruteMode = False; kb.fileReadMode = False |
130 | 136 | kb.disableShiftTable = False; kb.prependFlag = False; kb.timeless = None; kb.counters = {} |
131 | 137 | kb.originalCode = None; kb.originalPage = None |
| 138 | + # calibrated reference bodies for the same-code anomaly guard (Fix B); no learned dynamic markings |
| 139 | + kb.trueTemplate = _TRUE_BODY; kb.falseTemplate = _FALSE_BODY; kb.dynamicMarkings = [] |
132 | 140 | kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: _vector()} |
133 | 141 | setTechnique(PAYLOAD.TECHNIQUE.BOOLEAN) |
134 | 142 | kb.data.processChar = None |
@@ -224,6 +232,36 @@ def respond(payload, cond): |
224 | 232 | self._configure() |
225 | 233 | self.assertEqual(self._extract(respond), _SECRET) |
226 | 234 |
|
| 235 | + def test_anomaly_classifier_flags_only_junk(self): |
| 236 | + # Fix B core: a response resembling NEITHER calibrated model is flagged; the models themselves |
| 237 | + # and a benign dynamic variant are not. Deterministic, no network. |
| 238 | + self._configure() |
| 239 | + self.assertFalse(inf._resemblesNeitherModel(_TRUE_BODY)) |
| 240 | + self.assertFalse(inf._resemblesNeitherModel(_FALSE_BODY)) |
| 241 | + self.assertFalse(inf._resemblesNeitherModel(_TRUE_BODY.replace("dashboard", "dashboard <b>7 new</b> tok=abc123"))) |
| 242 | + for junk in (_INTERSTITIAL, "", "<html><h1>502 Bad Gateway</h1></html>", _TRUE_BODY[:60]): |
| 243 | + self.assertTrue(inf._resemblesNeitherModel(junk), msg="must flag junk %r" % junk[:40]) |
| 244 | + |
| 245 | + def test_same_code_body_jitter_is_ridden_out(self): |
| 246 | + # Fix B guard: a transient same-HTTP-code junk page that resembles NEITHER model makes a |
| 247 | + # character mis-resolve to a wrong (valid) value; the anomaly guard triggers validateChar to |
| 248 | + # re-extract it. The junk here carries the --string token (so it reads True and pushes the char |
| 249 | + # HIGH -> a wrong valid char, the case validateChar covers), and is unlike both models -> flagged. |
| 250 | + junk = "<html><body>notice: %s service temporarily degraded, retry</body></html>" % _STRING |
| 251 | + poisoned = {"n": 0} |
| 252 | + |
| 253 | + def respond(payload, cond): |
| 254 | + m = _PARSE.search(payload) |
| 255 | + idx = int(m.group(1)) if m else 0 |
| 256 | + if idx == 4 and "!=" not in payload and poisoned["n"] < 3: |
| 257 | + poisoned["n"] += 1 |
| 258 | + return junk, 200, "text/html" |
| 259 | + return (_TRUE_BODY if cond else _FALSE_BODY), 200, "text/html" |
| 260 | + |
| 261 | + self._configure() |
| 262 | + self.assertTrue(inf._resemblesNeitherModel(junk)) # precondition: the junk IS anomalous |
| 263 | + self.assertEqual(self._extract(respond), _SECRET) |
| 264 | + |
227 | 265 |
|
228 | 266 | @unittest.skipUnless(_STRESS, "creative boolean-jitter sweep is opt-in (set SQLMAP_JITTER_STRESS=1)") |
229 | 267 | class TestBooleanJitterSweep(_BooleanJitterBase): |
|
0 commit comments