mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-12 02:22:55 +00:00
Improved mechanism for --proof
This commit is contained in:
@@ -20,7 +20,7 @@ from lib.core.enums import OS
|
|||||||
from thirdparty import six
|
from thirdparty import six
|
||||||
|
|
||||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||||
VERSION = "1.10.8.9"
|
VERSION = "1.10.8.10"
|
||||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||||
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
||||||
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ def vulnTest(tests=None, label="vuln"):
|
|||||||
("-u <url> --data=\"security_level=3\" -p id --flush-session --technique=B", ("bypassed the WAF/IPS by using tamper script", "Type: boolean-based blind")), # automatic WAF-bypass: SQL-tamper dimension at a stricter signature threshold
|
("-u <url> --data=\"security_level=3\" -p id --flush-session --technique=B", ("bypassed the WAF/IPS by using tamper script", "Type: boolean-based blind")), # automatic WAF-bypass: SQL-tamper dimension at a stricter signature threshold
|
||||||
("-u <url> --data=\"security_level=4\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass against a libinjection-class WAF: tampers cannot help, only the non-scanner User-Agent does
|
("-u <url> --data=\"security_level=4\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass against a libinjection-class WAF: tampers cannot help, only the non-scanner User-Agent does
|
||||||
("-u <url> --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat
|
("-u <url> --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat
|
||||||
("-u <url> -p id --flush-session --technique=B --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof
|
("-u <url> -p id --flush-session --technique=B --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "drawn at random after the scan started", "boolean-based blind:", "PASS confirmed", "Read-back: back-end DBMS banner", "TRUE (", "5/5", "separated by:", "PROVEN - the back-end executed injected SQL")), # --proof: every claim is an experiment with a control - an unpredictable product the back-end must compute (per technique), a real datum read back, and the TRUE/FALSE differential with its wire facts
|
||||||
("-u <base> --mine-params --flush-session --technique=B", ("mining for hidden GET parameters", "found hidden parameter 'id'", "held back parameter(s) that break the base request", "Parameter: id (GET)", "Type: boolean-based blind")), # --mine-params: discover an injectable parameter absent from a bare URL, hold back the raw-SQL sink that would shadow it, then confirm the injection on the mined 'id'
|
("-u <base> --mine-params --flush-session --technique=B", ("mining for hidden GET parameters", "found hidden parameter 'id'", "held back parameter(s) that break the base request", "Parameter: id (GET)", "Type: boolean-based blind")), # --mine-params: discover an injectable parameter absent from a bare URL, hold back the raw-SQL sink that would shadow it, then confirm the injection on the mined 'id'
|
||||||
("-u \"<base>ratelimit?id=1\" --flush-session --technique=B", ("target appears to be rate-limiting", "Parameter: id (GET)", "Type: boolean-based blind")), # adaptive rate-limit handling: the endpoint answers 429 with 'Retry-After' first, so detection only succeeds if sqlmap honors the backoff, throttles, and retries rather than treating 429 as a hard block
|
("-u \"<base>ratelimit?id=1\" --flush-session --technique=B", ("target appears to be rate-limiting", "Parameter: id (GET)", "Type: boolean-based blind")), # adaptive rate-limit handling: the endpoint answers 429 with 'Retry-After' first, so detection only succeeds if sqlmap honors the backoff, throttles, and retries rather than treating 429 as a hard block
|
||||||
("-r <request> --flush-session -v 5 --test-skip=\"heavy\" --save=<config>", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")),
|
("-r <request> --flush-session -v 5 --test-skip=\"heavy\" --save=<config>", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")),
|
||||||
|
|||||||
+327
-237
@@ -6,9 +6,13 @@ See the file 'LICENSE' for copying permission
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
from lib.core.common import Backend
|
from lib.core.common import Backend
|
||||||
from lib.core.common import average
|
from lib.core.common import average
|
||||||
|
from lib.core.common import getCurrentThreadData
|
||||||
|
from lib.core.common import getSafeExString
|
||||||
|
from lib.core.common import getUnicode
|
||||||
from lib.core.common import openFile
|
from lib.core.common import openFile
|
||||||
from lib.core.common import randomInt
|
from lib.core.common import randomInt
|
||||||
from lib.core.common import stdev
|
from lib.core.common import stdev
|
||||||
@@ -25,17 +29,36 @@ from lib.core.enums import PAYLOAD
|
|||||||
from lib.core.enums import PLACE
|
from lib.core.enums import PLACE
|
||||||
from lib.core.settings import INFERENCE_MARKER
|
from lib.core.settings import INFERENCE_MARKER
|
||||||
from lib.core.settings import SLEEP_TIME_MARKER
|
from lib.core.settings import SLEEP_TIME_MARKER
|
||||||
from lib.request.inject import getValue
|
|
||||||
|
|
||||||
# how many times a true/false condition is re-evaluated to demonstrate repeatability (kills false positives)
|
# how many times the differential control is repeated, to show it is stable rather than a coincidence
|
||||||
PROVE_REPETITIONS = 5
|
PROVE_REPETITIONS = 5
|
||||||
|
|
||||||
|
# characters of a datum read through an INFERENTIAL technique (one request per bit), so a time-based point
|
||||||
|
# does not spend minutes reading a banner it has already proven it can read
|
||||||
|
INFERENTIAL_DATUM_CHARS = 12
|
||||||
|
|
||||||
# comparison knobs that decide true/false at request time (lib/request/comparison.py reads these globals,
|
# comparison knobs that decide true/false at request time (lib/request/comparison.py reads these globals,
|
||||||
# not injection.conf); they must be re-pointed at the injection being proven or the oracle returns None
|
# not injection.conf); they must be re-pointed at the injection being proven or the oracle returns None
|
||||||
_COMPARISON_ATTRS = ("string", "notString", "regexp", "code", "textOnly", "titles")
|
_COMPARISON_ATTRS = ("string", "notString", "regexp", "code", "textOnly", "titles")
|
||||||
|
|
||||||
# width the field labels are padded to, so the values line up in a clean column
|
_LABEL_WIDTH = 10
|
||||||
_LABEL_WIDTH = 9
|
|
||||||
|
# getValue() gates, so each experiment runs through ONE named technique and the evidence can be attributed
|
||||||
|
# to it. Without this the report would credit whatever technique getValue() happened to pick as fastest.
|
||||||
|
_GATES = {
|
||||||
|
PAYLOAD.TECHNIQUE.UNION: {"union": True, "error": False, "blind": False, "time": False},
|
||||||
|
PAYLOAD.TECHNIQUE.ERROR: {"union": False, "error": True, "blind": False, "time": False},
|
||||||
|
PAYLOAD.TECHNIQUE.QUERY: {"union": False, "error": True, "blind": False, "time": False},
|
||||||
|
PAYLOAD.TECHNIQUE.BOOLEAN: {"union": False, "error": False, "blind": True, "time": False},
|
||||||
|
PAYLOAD.TECHNIQUE.TIME: {"union": False, "error": False, "blind": False, "time": True},
|
||||||
|
PAYLOAD.TECHNIQUE.STACKED: {"union": False, "error": False, "blind": False, "time": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
# techniques that return the value inside the response body; the rest infer it one bit at a time
|
||||||
|
_INBAND = (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)
|
||||||
|
|
||||||
|
# order the experiments run in: cheapest and most demonstrative first
|
||||||
|
_ORDER = (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY, PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED)
|
||||||
|
|
||||||
|
|
||||||
def _field(label, value):
|
def _field(label, value):
|
||||||
@@ -75,118 +98,191 @@ def _restoreInjection(saved):
|
|||||||
setattr(conf, attr, value)
|
setattr(conf, attr, value)
|
||||||
|
|
||||||
|
|
||||||
def _booleanOracle(expression):
|
def _exchange():
|
||||||
"""
|
"""
|
||||||
Evaluates a boolean expression strictly through the boolean (inferential) technique. UNION/error are
|
The wire facts of the request that was just sent: (request line, HTTP code, response bytes, seconds).
|
||||||
forced off on purpose: for a multi-technique injection getValue() would try those first, and a WAF/IPS
|
An evidence line without them is an assertion; with them the reader can check the work.
|
||||||
that blocks their function-heavy payloads makes them return None, which (with expectingNone) short-
|
|
||||||
circuits the whole call before the boolean technique is ever reached - the real cause of a 0/0 reading.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return getValue(expression, expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY, suppressOutput=True, expectingNone=True, union=False, error=False, time=False)
|
threadData = getCurrentThreadData()
|
||||||
|
parts = (threadData.lastRequestMsg or "").replace("\r\n", "\n").split("\n")
|
||||||
|
line = urldecode(parts[1].strip(), convall=True) if len(parts) > 1 else ""
|
||||||
|
return line, threadData.lastCode, len(threadData.lastPage or ""), threadData.lastQueryDuration
|
||||||
|
|
||||||
|
|
||||||
def _signalArtifacts(expression):
|
def _techniques(injection):
|
||||||
|
return [_ for _ in _ORDER if _ in injection.data]
|
||||||
|
|
||||||
|
|
||||||
|
def _name(stype):
|
||||||
|
return PAYLOAD.SQLINJECTION.get(stype) or "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _outcome(ok, detail, exchange):
|
||||||
|
"""One evidence row: 'PASS <detail> HTTP 200, 402 bytes, 0.045s' plus the request line beneath it."""
|
||||||
|
|
||||||
|
line, code, length, duration = exchange
|
||||||
|
facts = []
|
||||||
|
if code is not None:
|
||||||
|
facts.append("HTTP %s" % code)
|
||||||
|
if length:
|
||||||
|
facts.append("%d bytes" % length)
|
||||||
|
if duration:
|
||||||
|
facts.append("%.3fs" % duration)
|
||||||
|
retVal = ["%-4s %s%s" % ("PASS" if ok else "FAIL", detail, (" [%s]" % ", ".join(facts)) if facts else "")]
|
||||||
|
if line:
|
||||||
|
retVal.append(" %s" % line)
|
||||||
|
return retVal
|
||||||
|
|
||||||
|
|
||||||
|
def _challenge(injection, a, b):
|
||||||
"""
|
"""
|
||||||
Evaluates 'expression' through the boolean oracle and reads back the (HTTP code, page <title>) of the
|
The decisive experiment, run once through EVERY confirmed technique.
|
||||||
response it produced (queryPage stores both in thread data), so the boolean proof can quote the actual
|
|
||||||
TRUE/FALSE codes and titles rather than a generic flag. Returns (None, None) on any error.
|
The back-end is asked for a*b, where both operands were drawn at random after the scan started. That
|
||||||
|
product exists in no page, cache, log or reflection, and no amount of pattern matching in front of the
|
||||||
|
application can produce it - only something that evaluates SQL can. An in-band technique must return
|
||||||
|
the product itself; an inferential one must answer TRUE to 'a*b=product' AND FALSE to 'a*b=product+1',
|
||||||
|
which costs two requests instead of reading the digits back one bit at a time.
|
||||||
|
|
||||||
|
Running it per technique is the point of the report: on a filtered target it shows exactly which
|
||||||
|
channels the protection closed and which one still carries data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from lib.core.common import extractRegexResult, getCurrentThreadData
|
from lib.request import inject
|
||||||
from lib.core.settings import HTML_TITLE_REGEX
|
|
||||||
|
|
||||||
try:
|
|
||||||
_booleanOracle(expression)
|
|
||||||
threadData = getCurrentThreadData()
|
|
||||||
return threadData.lastCode, (extractRegexResult(HTML_TITLE_REGEX, threadData.lastPage or "") or "").strip()
|
|
||||||
except Exception:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
def _proveBoolean(injection, signal=None):
|
|
||||||
"""
|
|
||||||
Demonstrates deterministic boolean control, rendered with the distinguishing signal sqlmap already
|
|
||||||
auto-selected (--string / --code / --title), repeated to show it is stable (not a fluke). The signal
|
|
||||||
line quotes the actual distinguishing artifact: the matched string, the two HTTP codes, or the two
|
|
||||||
page titles - so a reader sees exactly what tells TRUE from FALSE.
|
|
||||||
|
|
||||||
When a mutable 'signal' dict is supplied it is filled with the distinguishing artifact (code-based?
|
|
||||||
and the TRUE/FALSE HTTP codes) so the caller can tell a genuine signal from a blocked-response (WAF)
|
|
||||||
artifact - a TRUE condition that yields an HTTP 4xx is a block, not a database answer.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
expected = a * b
|
||||||
retVal = []
|
retVal = []
|
||||||
n = randomInt()
|
|
||||||
|
|
||||||
trues = sum(1 for _ in range(PROVE_REPETITIONS) if _booleanOracle("%d=%d" % (n, n)))
|
for stype in _techniques(injection):
|
||||||
falses = sum(1 for _ in range(PROVE_REPETITIONS) if _booleanOracle("%d=%d" % (n, n + 1)) is False)
|
gate = _GATES[stype]
|
||||||
|
try:
|
||||||
line = "condition %d=%d returns TRUE (%d/%d) while %d=%d returns FALSE (%d/%d)" % (n, n, trues, PROVE_REPETITIONS, n, n + 1, falses, PROVE_REPETITIONS)
|
if stype in _INBAND:
|
||||||
if trues == PROVE_REPETITIONS and falses == PROVE_REPETITIONS:
|
value = inject.getValue("%d*%d" % (a, b), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS, resumeValue=False, suppressOutput=True, **gate)
|
||||||
line += ", repeatably" # only claim repeatability when every repetition agreed
|
exchange = _exchange()
|
||||||
retVal.append(line)
|
ok = value is not None and ("%s" % value).strip() == str(expected)
|
||||||
|
detail = "returned %s" % value if value is not None else "no value returned"
|
||||||
trueCode = trueTitle = falseCode = falseTitle = None
|
else:
|
||||||
if injection.conf.code or injection.conf.titles: # fetch the real artifacts only when the signal needs them
|
hit = inject.getValue("%d*%d=%d" % (a, b, expected), expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY, resumeValue=False, suppressOutput=True, expectingNone=True, **gate)
|
||||||
trueCode, trueTitle = _signalArtifacts("%d=%d" % (n, n))
|
exchange = _exchange() # quote the TRUE probe: on a time-based point that is where the delay shows
|
||||||
falseCode, falseTitle = _signalArtifacts("%d=%d" % (n, n + 1))
|
miss = inject.getValue("%d*%d=%d" % (a, b, expected + 1), expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY, resumeValue=False, suppressOutput=True, expectingNone=True, **gate)
|
||||||
|
ok = bool(hit) and miss is False
|
||||||
if signal is not None:
|
detail = "confirmed %d, rejected %d" % (expected, expected + 1) if ok else "inconclusive (%s/%s)" % (hit, miss)
|
||||||
signal["codeBased"] = bool(injection.conf.code)
|
retVal.append((stype, ok, _outcome(ok, detail, exchange)))
|
||||||
signal["trueCode"], signal["falseCode"] = trueCode, falseCode
|
except Exception as ex:
|
||||||
|
retVal.append((stype, False, ["FAIL %s" % getSafeExString(ex)]))
|
||||||
if injection.conf.string:
|
|
||||||
retVal.append("the response contains %s only when the condition is TRUE" % repr(injection.conf.string).lstrip('u'))
|
|
||||||
elif injection.conf.notString:
|
|
||||||
retVal.append("the response contains %s only when the condition is FALSE" % repr(injection.conf.notString).lstrip('u'))
|
|
||||||
elif injection.conf.code:
|
|
||||||
if trueCode and falseCode and trueCode != falseCode:
|
|
||||||
retVal.append("the response returns HTTP %s when the condition is TRUE and HTTP %s when it is FALSE" % (trueCode, falseCode))
|
|
||||||
else:
|
|
||||||
retVal.append("the response returns HTTP %s only when the condition is TRUE (a different code otherwise)" % injection.conf.code)
|
|
||||||
elif injection.conf.titles:
|
|
||||||
if trueTitle and falseTitle and trueTitle != falseTitle:
|
|
||||||
retVal.append("the page title is %s when the condition is TRUE and %s when it is FALSE" % (repr(trueTitle).lstrip('u'), repr(falseTitle).lstrip('u')))
|
|
||||||
else:
|
|
||||||
retVal.append("the page <title> differs between the TRUE and FALSE responses")
|
|
||||||
else:
|
|
||||||
retVal.append("the TRUE response matches the original page while the FALSE one differs (content similarity)")
|
|
||||||
|
|
||||||
return retVal
|
return retVal
|
||||||
|
|
||||||
|
|
||||||
def _proveTime(injection):
|
def _datumQuery(stype):
|
||||||
"""
|
"""
|
||||||
Demonstrates time-based blind in plain IT language (jitter / latency / controlled delay), keeping the
|
A real datum to read out of the back-end, and its label. Inferential techniques pay one request per
|
||||||
statistics under the hood. Where the payload uses a parameterizable delay (SLEEP(n)/pg_sleep(n)/WAITFOR),
|
bit, so their datum is bounded with the DBMS' own SUBSTRING template instead of a whole banner.
|
||||||
it sweeps the injected delay (0 / T / 2T seconds) and shows the response time tracks it ~1:1 - a controlled
|
"""
|
||||||
delay that network latency or a slow page cannot reproduce. Otherwise (heavy-query delays) it falls back to
|
|
||||||
a baseline-vs-jitter statement.
|
dbms = Backend.getIdentifiedDbms()
|
||||||
|
entry = queries.get(dbms) if dbms else None
|
||||||
|
if entry is None:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
for attr, label in (("banner", "back-end DBMS banner"), ("current_db", "current database"), ("current_user", "current database user")):
|
||||||
|
query = getattr(getattr(entry, attr, None), "query", None)
|
||||||
|
if not query:
|
||||||
|
continue
|
||||||
|
if stype in _INBAND:
|
||||||
|
return query, label
|
||||||
|
template = getattr(getattr(entry, "substring", None), "query", None)
|
||||||
|
if template:
|
||||||
|
return template % (query, 1, INFERENTIAL_DATUM_CHARS), "%s (first %d characters)" % (label, INFERENTIAL_DATUM_CHARS)
|
||||||
|
return query, label
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _datum(passing):
|
||||||
|
"""
|
||||||
|
Reads a real value out of the back-end through a technique the challenge already proved, and reports
|
||||||
|
which one returned it. The challenge proves execution; this proves data egress.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from lib.request import inject
|
||||||
|
|
||||||
|
for stype in passing:
|
||||||
|
query, label = _datumQuery(stype)
|
||||||
|
if not query:
|
||||||
|
continue
|
||||||
|
started = kb.requestCounter
|
||||||
|
try:
|
||||||
|
value = unArrayizeValue(inject.getValue(query, safeCharEncode=False, suppressOutput=True, resumeValue=False, **_GATES[stype]))
|
||||||
|
except Exception:
|
||||||
|
value = None
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
line, code, length, duration = _exchange()
|
||||||
|
head = "%s = %s [%s]" % (label, repr(getUnicode(value)).lstrip('u'), _name(stype))
|
||||||
|
if stype in _INBAND:
|
||||||
|
head += " [HTTP %s, %d bytes, %.3fs]" % (code, length, duration or 0.0)
|
||||||
|
return [_field("Read-back", [head, " %s" % line] if line else [head])]
|
||||||
|
# inferred one bit at a time: a single request line would not represent the exchange
|
||||||
|
return [_field("Read-back", [head, " recovered bit by bit over %d requests" % (kb.requestCounter - started)])]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _booleanControl(injection):
|
||||||
|
"""
|
||||||
|
The TRUE/FALSE differential, quoted with the artifact that separates them AND with the response facts
|
||||||
|
behind it, so the reader can see what the oracle actually looked at.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from lib.request.inject import getValue
|
||||||
|
|
||||||
|
def _ask(expression):
|
||||||
|
result = getValue(expression, expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY, suppressOutput=True, expectingNone=True, union=False, error=False, time=False)
|
||||||
|
_line, code, length, _duration = _exchange()
|
||||||
|
return result, code, length
|
||||||
|
|
||||||
|
n = randomInt()
|
||||||
|
trues = falses = 0
|
||||||
|
trueCode = falseCode = trueLength = falseLength = None
|
||||||
|
|
||||||
|
for _ in range(PROVE_REPETITIONS):
|
||||||
|
result, trueCode, trueLength = _ask("%d=%d" % (n, n))
|
||||||
|
trues += bool(result)
|
||||||
|
result, falseCode, falseLength = _ask("%d=%d" % (n, n + 1))
|
||||||
|
falses += result is False
|
||||||
|
|
||||||
|
retVal = ["TRUE (%d=%d): %d/%d [HTTP %s, %s bytes]" % (n, n, trues, PROVE_REPETITIONS, trueCode, trueLength),
|
||||||
|
"FALSE (%d=%d): %d/%d [HTTP %s, %s bytes]" % (n, n + 1, falses, PROVE_REPETITIONS, falseCode, falseLength)]
|
||||||
|
|
||||||
|
if injection.conf.string:
|
||||||
|
retVal.append("separated by: the response contains %s only when TRUE" % repr(injection.conf.string).lstrip('u'))
|
||||||
|
elif injection.conf.notString:
|
||||||
|
retVal.append("separated by: the response contains %s only when FALSE" % repr(injection.conf.notString).lstrip('u'))
|
||||||
|
elif injection.conf.code:
|
||||||
|
retVal.append("separated by: the HTTP status code")
|
||||||
|
elif injection.conf.titles:
|
||||||
|
retVal.append("separated by: the page title")
|
||||||
|
else:
|
||||||
|
retVal.append("separated by: response content similarity")
|
||||||
|
|
||||||
|
# a TRUE condition answered by a 4xx is a block, not a database answer - the caller needs to know
|
||||||
|
return retVal, (bool(injection.conf.code) and (trueCode or 0) >= 400)
|
||||||
|
|
||||||
|
|
||||||
|
def _timeControl(injection, stype):
|
||||||
|
"""
|
||||||
|
Sweeps the injected delay (0 / T / 2T seconds) and shows the response time follows it. The 0s case is
|
||||||
|
the control: a slow application or a congested network cannot switch itself off on command.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from lib.core.agent import agent
|
from lib.core.agent import agent
|
||||||
from lib.core.common import getCurrentThreadData, popValue, pushValue
|
from lib.core.common import popValue, pushValue
|
||||||
from lib.request.connect import Connect as Request
|
from lib.request.connect import Connect as Request
|
||||||
|
|
||||||
retVal = []
|
|
||||||
stype = PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED
|
|
||||||
vector = (injection.data.get(stype) or {}).get("vector")
|
vector = (injection.data.get(stype) or {}).get("vector")
|
||||||
|
|
||||||
def _baselineStatement():
|
|
||||||
baseline = kb.responseTimes.get(kb.responseTimeMode) or []
|
|
||||||
if len(baseline) >= 2:
|
|
||||||
return "a TRUE condition delays the response well beyond the target's normal latency ~%.3fs (jitter ~%.3fs), repeatably" % (average(baseline), stdev(baseline))
|
|
||||||
return "a TRUE condition delays the response well beyond the target's normal latency and jitter, repeatably"
|
|
||||||
|
|
||||||
if not (vector and SLEEP_TIME_MARKER in vector):
|
|
||||||
retVal.append(_baselineStatement())
|
|
||||||
return retVal
|
|
||||||
|
|
||||||
n = randomInt()
|
|
||||||
base = conf.timeSec or 5
|
|
||||||
measurements = []
|
|
||||||
|
|
||||||
benign = []
|
benign = []
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
try:
|
try:
|
||||||
@@ -194,6 +290,17 @@ def _proveTime(injection):
|
|||||||
benign.append(getCurrentThreadData().lastQueryDuration)
|
benign.append(getCurrentThreadData().lastQueryDuration)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
baseAvg = average(benign) if benign else 0.0
|
||||||
|
baseStd = stdev(benign) if len(benign) >= 2 else 0.0
|
||||||
|
|
||||||
|
if not (vector and SLEEP_TIME_MARKER in vector):
|
||||||
|
# a heavy-query delay carries no parameterizable seconds, so there is nothing to sweep
|
||||||
|
return ["a TRUE condition delays the response well beyond the normal ~%.3fs (jitter ~%.3fs)" % (baseAvg, baseStd)]
|
||||||
|
|
||||||
|
n = randomInt()
|
||||||
|
base = conf.timeSec or 5
|
||||||
|
measurements = []
|
||||||
|
|
||||||
for k in (0, base, 2 * base):
|
for k in (0, base, 2 * base):
|
||||||
pushValue(conf.timeSec)
|
pushValue(conf.timeSec)
|
||||||
conf.timeSec = k
|
conf.timeSec = k
|
||||||
@@ -207,179 +314,162 @@ def _proveTime(injection):
|
|||||||
conf.timeSec = popValue()
|
conf.timeSec = popValue()
|
||||||
|
|
||||||
if any(d is None for _, d in measurements):
|
if any(d is None for _, d in measurements):
|
||||||
retVal.append(_baselineStatement())
|
return ["a TRUE condition delays the response well beyond the normal ~%.3fs (jitter ~%.3fs)" % (baseAvg, baseStd)]
|
||||||
return retVal
|
|
||||||
|
|
||||||
d0, dT, d2T = (measurements[0][1], measurements[1][1], measurements[2][1])
|
d0, dT, d2T = (measurements[0][1], measurements[1][1], measurements[2][1])
|
||||||
baseAvg = average(benign) if benign else d0
|
retVal = ["unmodified request: %.3fs (jitter ~%.3fs)" % (baseAvg, baseStd),
|
||||||
baseStd = stdev(benign) if len(benign) >= 2 else 0.0
|
"injected delay: %s" % " ".join("%ds -> %.2fs" % (k, d) for k, d in measurements)]
|
||||||
|
|
||||||
# only claim 1:1 scaling if the measurements actually track the injected seconds: 0s stays near baseline,
|
|
||||||
# Ts ~ T, 2Ts ~ 2T, monotonic. A heavy-query delay (e.g. SQLite RANDOMBLOB) also rides [SLEEPTIME] but
|
|
||||||
# does NOT scale linearly, so it must NOT be rendered as 1:1 (its sweep is noisy / non-monotonic)
|
|
||||||
linear = d0 < max(0.5, base * 0.5) and abs(dT - base) <= base * 0.5 and abs(d2T - 2 * base) <= base * 0.6 and d2T > dT
|
|
||||||
|
|
||||||
if linear:
|
|
||||||
retVal.append("normal response ~%.3fs (jitter ~%.3fs); injected delay %s" % (baseAvg, baseStd, " ".join("%ds -> %.2fs" % (k, d) for k, d in measurements)))
|
|
||||||
retVal.append("the response slows ~1:1 with the injected delay - a controlled delay that network latency or a slow page cannot reproduce (the 0s case returns at normal speed)")
|
|
||||||
else:
|
|
||||||
retVal.append("a TRUE condition makes the response take ~%.2fs versus ~%.3fs normal (jitter ~%.3fs), repeatably" % (max(dT, d2T), baseAvg, baseStd))
|
|
||||||
retVal.append("a FALSE condition returns at normal speed - a sustained delay neither network latency nor a slow page reproduces")
|
|
||||||
|
|
||||||
|
# only claim 1:1 scaling when the measurements really track the injected seconds. A heavy-query delay
|
||||||
|
# also rides [SLEEPTIME] but does not scale linearly, so it must not be rendered as a controlled delay.
|
||||||
|
if d0 < max(0.5, base * 0.5) and abs(dT - base) <= base * 0.5 and abs(d2T - 2 * base) <= base * 0.6 and d2T > dT:
|
||||||
|
retVal.append("the delay follows the injected value ~1:1, and 0s returns at normal speed")
|
||||||
return retVal
|
return retVal
|
||||||
|
|
||||||
|
|
||||||
def _retrieveProof():
|
# response codes a protection returns when it drops a request, rather than the application answering
|
||||||
|
_BLOCKED_CODES = (403, 406, 419, 429, 501, 503)
|
||||||
|
|
||||||
|
|
||||||
|
def _evasion():
|
||||||
"""
|
"""
|
||||||
Reads values back through the injection to prove it - DBMS-agnostic, weakest-to-strongest:
|
What the proof had to get through. On a filtered target this is the part that matters: the evidence
|
||||||
|
above is worth much more when the report also states that a protection was in the path and what was
|
||||||
1. a random arithmetic product (e.g. 48391*60128): every SQL engine evaluates it, it needs no
|
needed to carry data past it.
|
||||||
table/function/FROM (valid even on Oracle), so its WAF surface is tiny - yet the operands are
|
|
||||||
random, so reading the exact product back proves the back-end actually executed injected SQL
|
|
||||||
(not a reflected constant);
|
|
||||||
2. the DBMS banner: a real datum the application never returns on its own (the strongest proof).
|
|
||||||
|
|
||||||
Whatever evasion the run already adopted (tamper scripts) applies here too - this is not tied to any one
|
|
||||||
DBMS or tamper. Returns a list of (label, text) rungs; both, one, or none may be present.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from lib.request import inject
|
|
||||||
|
|
||||||
retVal = []
|
retVal = []
|
||||||
|
if kb.identifiedWafs:
|
||||||
a, b = randomInt(4), randomInt(4) # 4-digit operands: product stays < 2^31 so it never overflows a 32-bit INT (e.g. PostgreSQL int4), yet is unguessable
|
retVal.append("protection identified: %s" % ", ".join(sorted(kb.identifiedWafs)))
|
||||||
try:
|
elif kb.wafBypass is not None:
|
||||||
result = inject.getValue("%d*%d" % (a, b), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS, resumeValue=False, suppressOutput=True)
|
retVal.append("protection detected in front of the application (not fingerprinted)")
|
||||||
except Exception:
|
if kb.wafBypass:
|
||||||
result = None
|
retVal.append("automatic bypass applied: non-scanner User-Agent and browser-like headers")
|
||||||
if result is not None and ("%s" % result).strip() == str(a * b):
|
names = ", ".join(sorted(_.__name__.rsplit('.', 1)[-1] for _ in (kb.tamperFunctions or [])))
|
||||||
retVal.append(("Computed", "%d*%d = %d returned by the back-end - it executed the injected SQL (works on any DBMS)" % (a, b, a * b)))
|
if names or conf.tamper:
|
||||||
|
retVal.append("tamper scripts in effect: %s" % (names or conf.tamper))
|
||||||
label = value = None
|
blocked = ", ".join("%d x%d" % (code, count) for code, count in sorted((kb.httpErrorCodes or {}).items()) if code in _BLOCKED_CODES)
|
||||||
for requested, candidate, lbl in ( # reuse a value the user's own switches already pulled
|
if blocked:
|
||||||
(conf.getBanner, getattr(kb.data, "banner", None), "back-end DBMS banner"),
|
retVal.append("responses refused by the protection during the run: %s" % blocked)
|
||||||
(conf.getCurrentUser, getattr(kb.data, "currentUser", None), "current database user"),
|
if kb.droppingRequests:
|
||||||
(conf.getCurrentDb, getattr(kb.data, "currentDb", None), "current database"),
|
retVal.append("the target dropped or reset requests during the scan (retried)")
|
||||||
):
|
if conf.delay:
|
||||||
if requested and candidate:
|
retVal.append("requests were delayed by %.2fs each" % conf.delay)
|
||||||
label, value = lbl, unArrayizeValue(candidate)
|
|
||||||
break
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
dbms = Backend.getIdentifiedDbms()
|
|
||||||
banner = getattr(queries.get(dbms), "banner", None) if dbms else None
|
|
||||||
query = getattr(banner, "query", None) if banner else None
|
|
||||||
if query:
|
|
||||||
try:
|
|
||||||
value = unArrayizeValue(inject.getValue(query, safeCharEncode=False, suppressOutput=True))
|
|
||||||
label = "back-end DBMS banner"
|
|
||||||
except Exception:
|
|
||||||
value = None
|
|
||||||
|
|
||||||
if value:
|
|
||||||
retVal.append(("Retrieved", "%s %s - a real value read out of the back-end (the strongest proof)" % (label, repr(value).lstrip('u'))))
|
|
||||||
|
|
||||||
return retVal
|
return retVal
|
||||||
|
|
||||||
|
|
||||||
|
def _proveInjection(injection):
|
||||||
|
"""
|
||||||
|
Runs every experiment for one injection point and renders its block. Returns (fields, proven).
|
||||||
|
"""
|
||||||
|
|
||||||
|
saved = _activateInjection(injection)
|
||||||
|
started = kb.requestCounter
|
||||||
|
|
||||||
|
try:
|
||||||
|
a, b = randomInt(4), randomInt(4) # 4-digit operands: the product stays inside a 32-bit INT on every DBMS, yet is unguessable
|
||||||
|
rows = _challenge(injection, a, b)
|
||||||
|
passing = [stype for stype, ok, _ in rows if ok]
|
||||||
|
|
||||||
|
blocked = None
|
||||||
|
control = []
|
||||||
|
stype = passing[0] if passing else (_techniques(injection) or [None])[0]
|
||||||
|
|
||||||
|
if PAYLOAD.TECHNIQUE.BOOLEAN in injection.data:
|
||||||
|
control, blocked = _booleanControl(injection)
|
||||||
|
controlLabel = "boolean differential"
|
||||||
|
elif PAYLOAD.TECHNIQUE.TIME in injection.data or PAYLOAD.TECHNIQUE.STACKED in injection.data:
|
||||||
|
control = _timeControl(injection, PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED)
|
||||||
|
controlLabel = "timing control"
|
||||||
|
else:
|
||||||
|
controlLabel = None
|
||||||
|
|
||||||
|
readback = _datum(passing)
|
||||||
|
finally:
|
||||||
|
_restoreInjection(saved)
|
||||||
|
|
||||||
|
paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else injection.place
|
||||||
|
fields = [_field("Parameter", "%s (%s)" % (injection.parameter, paramType)),
|
||||||
|
_field("Techniques", ", ".join(_name(_) for _ in _techniques(injection)) or "none")]
|
||||||
|
|
||||||
|
challenge = ["the back-end must compute %d*%d = %d, drawn at random after the scan started" % (a, b, a * b),
|
||||||
|
"(the product is in no page, cache or reflection - only something that evaluates SQL can return it)"]
|
||||||
|
for stype, _ok, lines in rows:
|
||||||
|
challenge.append("%s:" % _name(stype))
|
||||||
|
challenge.extend(" %s" % _ for _ in lines)
|
||||||
|
fields.append(_field("Challenge", challenge))
|
||||||
|
|
||||||
|
fields.extend(readback)
|
||||||
|
|
||||||
|
if control:
|
||||||
|
fields.append(_field("Control", ["%s" % controlLabel] + [" %s" % _ for _ in control]))
|
||||||
|
|
||||||
|
evasion = _evasion()
|
||||||
|
if evasion:
|
||||||
|
fields.append(_field("Evasion", evasion))
|
||||||
|
|
||||||
|
proven = bool(passing)
|
||||||
|
if proven:
|
||||||
|
through = " through the protection in front of the application" if (kb.identifiedWafs or kb.wafBypass) else ""
|
||||||
|
verdict = ["PROVEN - the back-end executed injected SQL and returned the result%s" % through,
|
||||||
|
"channels that carry data: %s" % ", ".join(_name(_) for _ in passing)]
|
||||||
|
failed = [_name(stype) for stype, ok, _ in rows if not ok]
|
||||||
|
if failed:
|
||||||
|
verdict.append("channels that did NOT answer: %s" % ", ".join(failed))
|
||||||
|
else:
|
||||||
|
verdict = ["NOT PROVEN - no technique returned the computed value"]
|
||||||
|
if blocked:
|
||||||
|
verdict.append("a TRUE condition answers with an HTTP error - that is a block, not a database answer")
|
||||||
|
if kb.identifiedWafs or kb.droppingRequests or blocked:
|
||||||
|
verdict.append("a protection is interfering, so this may be a real injection whose data channel is blocked")
|
||||||
|
verdict.append("=> re-test without the protection, or with '--tamper', then prove again")
|
||||||
|
else:
|
||||||
|
verdict.append("the reported injection point reproduces a differential but cannot execute SQL")
|
||||||
|
verdict.append("=> treat it as a FALSE POSITIVE unless a side effect proves otherwise (e.g. '--os-shell')")
|
||||||
|
|
||||||
|
verdict.append("%d requests spent on this proof" % (kb.requestCounter - started))
|
||||||
|
fields.append(_field("Verdict", verdict))
|
||||||
|
|
||||||
|
return fields, proven
|
||||||
|
|
||||||
|
|
||||||
def proveExploitation():
|
def proveExploitation():
|
||||||
"""
|
"""
|
||||||
Renders a report-grade, best-effort demonstration of exploitation for the confirmed injection point
|
Renders a verifiable demonstration of exploitation for every confirmed injection point (switch
|
||||||
(option '--proof'), in the same style as sqlmap's injection-point summary so it reads naturally: the
|
'--proof'). It does not restate what detection reported: each claim is an experiment with a control,
|
||||||
target URL and the confirmed injection point (parameter / type / title / payload), then the strongest
|
an unpredictable expected value, and the request line, HTTP status, response size and timing that
|
||||||
proof first - an actual value read out of the back-end (drilling from the plain read to a more evasive
|
produced it - and every claim is attributed to the technique that produced it. Written to stdout and
|
||||||
one so a WAF/IPS does not stop it) - backed by a deterministic boolean differential (rendered with the
|
to '<output>/proof.txt'.
|
||||||
distinguishing --string/--code/--title signal) or a statistical time-based demonstration. Written both
|
|
||||||
to stdout and to '<output>/proof.txt'.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not kb.injections or not any(getattr(_, "place", None) for _ in kb.injections):
|
injections = [_ for _ in (kb.injections or []) if getattr(_, "place", None)]
|
||||||
|
if not injections:
|
||||||
return
|
return
|
||||||
|
|
||||||
injection = kb.injection if getattr(kb.injection, "place", None) else kb.injections[0]
|
|
||||||
|
|
||||||
signal = {}
|
|
||||||
saved = _activateInjection(injection)
|
|
||||||
try:
|
|
||||||
if PAYLOAD.TECHNIQUE.BOOLEAN in injection.data:
|
|
||||||
stype = PAYLOAD.TECHNIQUE.BOOLEAN
|
|
||||||
proof = _proveBoolean(injection, signal)
|
|
||||||
elif PAYLOAD.TECHNIQUE.TIME in injection.data or PAYLOAD.TECHNIQUE.STACKED in injection.data:
|
|
||||||
stype = PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED
|
|
||||||
proof = _proveTime(injection)
|
|
||||||
elif PAYLOAD.TECHNIQUE.ERROR in injection.data:
|
|
||||||
stype = PAYLOAD.TECHNIQUE.ERROR
|
|
||||||
proof = ["the back-end error message returns the requested value directly"]
|
|
||||||
elif PAYLOAD.TECHNIQUE.UNION in injection.data:
|
|
||||||
stype = PAYLOAD.TECHNIQUE.UNION
|
|
||||||
proof = ["the requested value is rendered inside the application response"]
|
|
||||||
else:
|
|
||||||
stype = next(iter(injection.data), None)
|
|
||||||
proof = []
|
|
||||||
|
|
||||||
rungs = _retrieveProof()
|
|
||||||
finally:
|
|
||||||
_restoreInjection(saved)
|
|
||||||
|
|
||||||
from lib.core.agent import agent
|
|
||||||
|
|
||||||
target = conf.url or ""
|
target = conf.url or ""
|
||||||
if conf.parameters.get(PLACE.GET) and "?" not in target: # spell out the full GET target, not just the path
|
if conf.parameters.get(PLACE.GET) and "?" not in target: # spell out the full GET target, not just the path
|
||||||
target += "?%s" % conf.parameters[PLACE.GET]
|
target += "?%s" % conf.parameters[PLACE.GET]
|
||||||
|
|
||||||
paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else injection.place
|
|
||||||
sdata = injection.data.get(stype)
|
|
||||||
|
|
||||||
fields = [_field("Target", target)]
|
fields = [_field("Target", target)]
|
||||||
if conf.parameters.get(PLACE.POST):
|
if conf.parameters.get(PLACE.POST):
|
||||||
fields.append(_field("Data", conf.parameters[PLACE.POST]))
|
fields.append(_field("Data", conf.parameters[PLACE.POST]))
|
||||||
fields.append(_field("Parameter", "%s (%s)" % (injection.parameter, paramType)))
|
if Backend.getIdentifiedDbms():
|
||||||
if sdata is not None:
|
fields.append(_field("Back-end", Backend.getIdentifiedDbms()))
|
||||||
fields.append(_field("Technique", PAYLOAD.SQLINJECTION[stype]))
|
fields.append(_field("Verified", time.strftime("%Y-%m-%d %H:%M:%S")))
|
||||||
if sdata.payload:
|
|
||||||
payload = urldecode(agent.adjustLateValues(sdata.payload), unsafe="&", spaceplus=(injection.place != PLACE.GET and kb.postSpaceToPlus))
|
|
||||||
fields.append(_field("Payload", payload))
|
|
||||||
# Reading a value back out of the back-end is the GATE, not a bonus: it is the only thing that
|
|
||||||
# distinguishes a real injection from a differential that merely correlates with the payload. A
|
|
||||||
# WAF/IPS that answers blocked payloads with a distinct HTTP status (e.g. 403 when TRUE, 200 when
|
|
||||||
# FALSE) reproduces a perfect, repeatable boolean differential WITHOUT any SQL ever executing - so
|
|
||||||
# the differential alone is exactly the signal detection already (mis)read. If nothing could be read
|
|
||||||
# back, exploitation is NOT proven; say so plainly instead of echoing the detection verdict.
|
|
||||||
proven = bool(rungs)
|
|
||||||
|
|
||||||
# whether ANY confirmed technique here can return data inline; a stacked-query-only point cannot, so a
|
proven = 0
|
||||||
# failed read-back below is expected there and must NOT be spun into a "false positive" verdict
|
for injection in injections:
|
||||||
canReadBack = any(_ in injection.data for _ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.TIME))
|
block, ok = _proveInjection(injection)
|
||||||
|
proven += int(ok)
|
||||||
|
fields.append("")
|
||||||
|
fields.extend(block)
|
||||||
|
|
||||||
if proven:
|
if proven == len(injections):
|
||||||
if proof:
|
header = "sqlmap proved exploitation of the following injection point(s)"
|
||||||
fields.append(_field("Proof", proof))
|
elif proven:
|
||||||
for label, text in rungs:
|
header = "sqlmap proved exploitation of %d of %d reported injection point(s)" % (proven, len(injections))
|
||||||
fields.append(_field(label, text))
|
|
||||||
header = "sqlmap proved exploitation of the following injection point"
|
|
||||||
else:
|
else:
|
||||||
if proof:
|
header = "sqlmap could NOT prove exploitation of the reported injection point(s)"
|
||||||
fields.append(_field("Observed", proof)) # the differential is observed, but unconfirmed
|
|
||||||
suspectWaf = bool(signal.get("codeBased")) and (signal.get("trueCode") or 0) >= 400
|
|
||||||
wafInterfering = suspectWaf or kb.droppingRequests or bool(kb.identifiedWafs)
|
|
||||||
verdict = ["no value could be read back through the injection (tried a random arithmetic product and the DBMS banner)"]
|
|
||||||
if suspectWaf:
|
|
||||||
verdict.append("the TRUE/FALSE difference is only an HTTP %s (blocked) response - characteristic of a WAF/IPS, not a database answer" % signal.get("trueCode"))
|
|
||||||
if not canReadBack:
|
|
||||||
# e.g. stacked-query-only: no confirmed technique returns data inline, so a value cannot be read
|
|
||||||
# back here - that is expected by design and is NOT evidence of a false positive
|
|
||||||
verdict.append("this injection point exposes no data-returning channel (e.g. stacked queries), so a value cannot be read back inline - expected here, not a false positive")
|
|
||||||
verdict.append("=> confirm exploitation through a side effect instead (e.g. '--os-shell', or '--sql-query' run with '--technique=S')")
|
|
||||||
elif wafInterfering:
|
|
||||||
# behind a WAF, an unconfirmed read-back is ambiguous: a genuine injection whose data-retrieval
|
|
||||||
# payloads are being blocked looks the same as a pure WAF artifact - so don't assert "false
|
|
||||||
# positive", point the user at the way to disambiguate instead
|
|
||||||
verdict.append("a WAF/IPS is interfering: this may be a real injection whose data-retrieval is blocked, or a false positive")
|
|
||||||
verdict.append("=> exploitation is NOT proven; re-test directly (no WAF) or with --tamper, then re-prove")
|
|
||||||
else:
|
|
||||||
verdict.append("=> exploitation is NOT proven; the reported injection is likely a FALSE POSITIVE")
|
|
||||||
fields.append(_field("Verdict", verdict))
|
|
||||||
header = "sqlmap could NOT prove exploitation of the reported injection point"
|
|
||||||
|
|
||||||
data = "\n".join(fields)
|
data = "\n".join(fields)
|
||||||
conf.dumper.string(header, data)
|
conf.dumper.string(header, data)
|
||||||
|
|||||||
Reference in New Issue
Block a user