Repository navigation
Expand file tree
/
Copy pathnonsql.py
More file actions
188 lines (151 loc) · 9.1 KB
/
Copy pathnonsql.py
File metadata and controls
188 lines (151 loc) · 9.1 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
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Shared detection primitives for the non-SQL injection techniques (--nosql, --xpath, --ldap, --hql,
--ssti, --graphql, --xxe). Each of those engines historically carried its own copy of the same
response-comparison, error/blocked-status filtering, blind-bit classification and user-oracle logic;
this module is the single home for that shared machinery so the behavior is uniform and reviewable
in one place rather than drifting across six files.
"""
import difflib
import re
from lib.core.common import urldecode
from lib.core.common import urlencode
from lib.core.convert import getUnicode
from lib.core.data import conf
from lib.core.settings import REFLECTED_VALUE_MARKER
from lib.core.settings import UPPER_RATIO_BOUND
from lib.parse.html import htmlParser
# Minimum similarity margin by which a blind-extraction response must lean toward the confirmed TRUE
# model over the FALSE model before a bit is accepted as true (else ambiguous -> false). Deliberately
# generous: a small (e.g. 5%) margin lets a noisy page fabricate values one character at a time.
EXTRACT_MATCH_MARGIN = 0.2
# HTTP statuses that mean the response is BLOCKED (WAF / rate-limit); together with 5xx these must
# never be fed to a boolean oracle as if they were application content.
BLOCKED_HTTP_CODES = frozenset((403, 429))
# generic SQL/DBMS error marker (mirrors lib/parse/html.py's own generic check), used alongside the
# DBMS-specific errors.xml signatures that htmlParser() recognizes
_SQL_ERROR_REGEX = re.compile(r"(?i)SQL (warning|error|syntax)")
def ratio(first, second):
"""Content-similarity ratio shared by every non-SQL detector (difflib quick_ratio over the two
response bodies) - one implementation instead of six identical copies."""
return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio()
def stripReflection(page, payload):
"""
Remove the payload from the page before any two responses are compared.
An endpoint that merely ECHOES the parameter returns a different page for every different payload, so
a true/false differential is satisfied without a single expression, filter or operator ever being
interpreted. That is not injection, and it is how plain reflective search pages were reported as
XPath / LDAP / NoSQL injectable.
Deliberately NOT lib.core.common.removeReflectiveValues: that one is a scan-wide heuristic which
switches ITSELF OFF after REFLECTIVE_MISS_THRESHOLD misses, after a regex timeout, and during
heuristic mode - so a detection guard built on it silently stops guarding mid-scan. This is a plain,
deterministic substring removal with no global state and no failure mode. The two are complementary,
and the engines apply both.
The raw, URL-decoded and URL-encoded forms are all removed: a payload travels encoded, and an
application may echo whichever of the three it happened to hold.
"""
if not page or not payload:
return page
retVal = getUnicode(page)
forms = set()
for form in (payload, urldecode(payload, convall=True), urlencode(payload, safe="")):
try:
forms.add(getUnicode(form))
except Exception:
pass
# longest first, so a shorter form cannot chop a longer one into unremovable pieces
for form in sorted(filter(None, forms), key=len, reverse=True):
if form in retVal:
retVal = retVal.replace(form, REFLECTED_VALUE_MARKER)
return retVal
def blockedStatus(code):
"""True when an HTTP status means the response is blocked/errored (a 5xx, or a WAF/rate-limit
403/429) and so is not a usable oracle sample. `_send()` implementations return None for these
(and for transport exceptions) so the boolean routines, which reject None, can never decide on
a non-answer."""
return bool(code) and (code >= 500 or code in BLOCKED_HTTP_CODES)
def sqlErrorPresent(page):
"""True when the response carries a recognized SQL/DBMS error - either a DBMS-specific signature
from sqlmap's errors.xml (via htmlParser) or the generic 'SQL warning/error/syntax' marker. The
non-SQL detectors treat such a page as NOT a valid boolean template, so a payload that merely
trips a back-end SQL syntax error cannot fake a true/false divergence and get a plainly SQL-
injectable parameter mis-reported as NoSQL / XPath / LDAP / HQL."""
page = page or ""
return bool(htmlParser(page)) or bool(_SQL_ERROR_REGEX.search(page))
# Visible placeholder for a single recovered cell/attribute whose extraction was INCONCLUSIVE (the
# oracle stayed ambiguous after retries). Rendered in dumps in place of the value so a failed cell is
# never silently shown as a genuine empty string - `None` from an extractor means "unknown", `""` means
# "really empty", and they must stay distinguishable in the output.
INCONCLUSIVE_MARK = "<inconclusive>"
class InconclusiveError(Exception):
"""Raised by resolveBit(abort=True) when a bit stays INCONCLUSIVE after retries. Per-value
extractors catch it to ABORT the current value (return what was recovered so far, marked
incomplete) instead of substituting a semantic False - which would corrupt a length, pick the
wrong half of a bisection, or truncate enumeration."""
class Decision(object):
"""Tri(+)-state blind-inference outcome. INCONCLUSIVE is deliberately DISTINCT from FALSE: an
ambiguous comparison (equally close to both models, close to neither, or a transport/blocked
anomaly) must be retried/aborted, NOT silently read as a semantic false - which would shorten a
value, pick the wrong half of a bisection or truncate enumeration."""
TRUE = "TRUE"
FALSE = "FALSE"
INCONCLUSIVE = "INCONCLUSIVE"
def decide(page, trueModel, falseModel, margin=EXTRACT_MATCH_MARGIN):
"""Classify a blind-inference response against the two calibrated models, returning a Decision.
TRUE when it resembles the confirmed TRUE model (identical, or clearly closer to it than to the
FALSE model by `margin`); FALSE when it resembles the FALSE model; INCONCLUSIVE when it leans to
neither (so the caller can retry or abort rather than guess)."""
if page is None:
return Decision.INCONCLUSIVE
simTrue, simFalse = ratio(trueModel, page), ratio(falseModel, page)
if simTrue >= UPPER_RATIO_BOUND and simTrue >= simFalse:
return Decision.TRUE
if simFalse >= UPPER_RATIO_BOUND and simFalse >= simTrue:
return Decision.FALSE
if (simTrue - simFalse) >= margin:
return Decision.TRUE
if (simFalse - simTrue) >= margin:
return Decision.FALSE
return Decision.INCONCLUSIVE
def resolveBit(page, trueModel, falseModel, resend, retries=2, margin=EXTRACT_MATCH_MARGIN, abort=True):
"""Resolve one blind bit to True/False. On an INCONCLUSIVE first read, RE-SEND (fresh, cache-
bypassing) up to `retries` times to ride out transient jitter before deciding. `resend` is a
0-arg callable returning a fresh page (or None on error/block). If a bit stays INCONCLUSIVE after
the retries: raise InconclusiveError when `abort` (the caller aborts the CURRENT VALUE rather than
corrupt it), else return False."""
d = decide(page, trueModel, falseModel, margin)
tries = 0
while d is Decision.INCONCLUSIVE and tries < retries:
page = resend()
if page is None:
break
d = decide(page, trueModel, falseModel, margin)
tries += 1
if d is Decision.INCONCLUSIVE and abort:
raise InconclusiveError()
return d is Decision.TRUE
def leansTrue(page, trueModel, falseModel, margin=EXTRACT_MATCH_MARGIN):
"""Boolean shorthand for `decide(...) is Decision.TRUE` (kept for callers that don't retry).
A page indistinguishable from the FALSE model, or ambiguous, is NOT true - so a dynamic token, a
changed error page, a WAF/rate-limit body or a transient exception can never fabricate a bit."""
return decide(page, trueModel, falseModel, margin) is Decision.TRUE
def userOracleActive():
"""True when the user supplied an explicit true/false response signal (--string / --not-string /
--regexp) that the non-SQL techniques should honor instead of relying on raw page similarity."""
return bool(getattr(conf, "string", None) or getattr(conf, "notString", None) or getattr(conf, "regexp", None))
def userDecision(page):
"""Classify a response with the user's explicit oracle (--string / --not-string / --regexp),
returning True/False, or None when no override is set (caller falls back to content comparison).
Page-only: HTTP-code overrides (--code) stay per-engine, where the status line is available.
This routes the non-SQL boolean detectors through sqlmap's documented detection overrides - the
same knobs the SQL engine honors - rather than discarding them for a fixed similarity ratio."""
page = page or ""
if getattr(conf, "string", None):
return conf.string in page
if getattr(conf, "notString", None):
return conf.notString not in page
if getattr(conf, "regexp", None):
return re.search(conf.regexp, page) is not None
return None