mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-28 12:31:00 +00:00
Couple of improvements
This commit is contained in:
parent
8a458fc8d0
commit
8de9c5899d
18 changed files with 108 additions and 51 deletions
|
|
@ -94,6 +94,7 @@ from lib.core.settings import SUHOSIN_MAX_VALUE_LENGTH
|
|||
from lib.core.settings import SUPPORTED_DBMS
|
||||
from lib.core.settings import UPPER_RATIO_BOUND
|
||||
from lib.core.settings import URI_HTTP_HEADER
|
||||
from lib.core.settings import WAF_BLOCK_HTTP_CODES
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.core.unescaper import unescaper
|
||||
from lib.request.connect import Connect as Request
|
||||
|
|
@ -588,6 +589,18 @@ def checkSqlInjection(place, parameter, value):
|
|||
break
|
||||
|
||||
if injectable:
|
||||
# WAF/IPS block-artifact guard: a TRUE condition (the always-true payload that
|
||||
# mimics a legitimate request) coming back with a blocked HTTP status (e.g. 403)
|
||||
# while the FALSE condition passes (2xx) is the WAF answering, not the database.
|
||||
# A real boolean injection's TRUE condition reproduces the normal page, so this
|
||||
# status-code asymmetry is the classic false positive - refuse it here.
|
||||
if not kb.negativeLogic and trueCode in WAF_BLOCK_HTTP_CODES and (falseCode or 0) < 400 and (kb.heuristicCode or 200) < 400:
|
||||
warnMsg = "%sparameter '%s' TRUE/FALSE responses differ only by a blocked HTTP %d vs %d status, " % ("%s " % paramType if paramType != parameter else "", parameter, trueCode, falseCode)
|
||||
warnMsg += "which is characteristic of a WAF/IPS block rather than a SQL injection; skipping as a likely false positive"
|
||||
logger.warning(warnMsg)
|
||||
injectable = False
|
||||
continue
|
||||
|
||||
if kb.pageStable and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)):
|
||||
if all((falseCode, trueCode)) and falseCode != trueCode and trueCode != kb.heuristicCode:
|
||||
suggestion = conf.code = trueCode
|
||||
|
|
|
|||
|
|
@ -817,7 +817,7 @@ def start():
|
|||
try:
|
||||
action()
|
||||
finally:
|
||||
if conf.prove:
|
||||
if conf.proof:
|
||||
from lib.utils.prove import proveExploitation
|
||||
proveExploitation()
|
||||
|
||||
|
|
|
|||
|
|
@ -827,7 +827,7 @@ class Agent(object):
|
|||
|
||||
def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None):
|
||||
"""
|
||||
Take in input an query (pseudo query) string and return its
|
||||
Take in input a query (pseudo query) string and return its
|
||||
processed UNION ALL SELECT query.
|
||||
|
||||
Examples:
|
||||
|
|
|
|||
|
|
@ -210,9 +210,9 @@ class BigArray(list):
|
|||
except (OSError, IOError) as ex:
|
||||
errMsg = "exception occurred while storing data "
|
||||
errMsg += "to a temporary file ('%s'). Please " % ex
|
||||
errMsg += "make sure that there is enough disk space left. If problem persists, "
|
||||
errMsg += "make sure that there is enough disk space left. If the problem persists, "
|
||||
errMsg += "try to set environment variable 'TEMP' to a location "
|
||||
errMsg += "writeable by the current user"
|
||||
errMsg += "writable by the current user"
|
||||
raise SqlmapSystemException(errMsg)
|
||||
|
||||
def _checkcache(self, index):
|
||||
|
|
|
|||
|
|
@ -2761,7 +2761,7 @@ def getPartRun(alias=True):
|
|||
|
||||
def longestCommonPrefix(*sequences):
|
||||
"""
|
||||
Returns longest common prefix occuring in given sequences
|
||||
Returns longest common prefix occurring in given sequences
|
||||
|
||||
# Reference: http://boredzo.org/blog/archives/2007-01-06/longest-common-prefix-in-python-2
|
||||
|
||||
|
|
@ -3158,7 +3158,7 @@ def getPublicTypeMembers(type_, onlyValues=False):
|
|||
|
||||
def enumValueToNameLookup(type_, value_):
|
||||
"""
|
||||
Returns name of a enum member with a given value
|
||||
Returns name of an enum member with a given value
|
||||
|
||||
>>> enumValueToNameLookup(SORT_ORDER, 100)
|
||||
'LAST'
|
||||
|
|
|
|||
|
|
@ -608,7 +608,7 @@ def _setMetasploit():
|
|||
else:
|
||||
warnMsg = "the provided Metasploit Framework path "
|
||||
warnMsg += "'%s' is not valid. The cause could " % conf.msfPath
|
||||
warnMsg += "be that the path does not exists or that one "
|
||||
warnMsg += "be that the path does not exist or that one "
|
||||
warnMsg += "or more of the needed Metasploit executables "
|
||||
warnMsg += "within msfcli, msfconsole, msfencode and "
|
||||
warnMsg += "msfpayload do not exist"
|
||||
|
|
@ -1675,9 +1675,9 @@ def _createTemporaryDirectory():
|
|||
except Exception as ex:
|
||||
warnMsg = "there has been a problem while accessing "
|
||||
warnMsg += "system's temporary directory location(s) ('%s'). Please " % getSafeExString(ex)
|
||||
warnMsg += "make sure that there is enough disk space left. If problem persists, "
|
||||
warnMsg += "make sure that there is enough disk space left. If the problem persists, "
|
||||
warnMsg += "try to set environment variable 'TEMP' to a location "
|
||||
warnMsg += "writeable by the current user"
|
||||
warnMsg += "writable by the current user"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
if "sqlmap" not in (tempfile.tempdir or "") or conf.tmpDir and tempfile.tempdir == conf.tmpDir:
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ optDict = {
|
|||
"prefix": "string",
|
||||
"suffix": "string",
|
||||
"tamper": "string",
|
||||
"prove": "boolean",
|
||||
"proof": "boolean",
|
||||
},
|
||||
|
||||
"Detection": {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from lib.core.enums import OS
|
|||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.6.122"
|
||||
VERSION = "1.10.6.123"
|
||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||
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)
|
||||
|
|
@ -54,6 +54,11 @@ IPS_WAF_CHECK_RATIO = 0.5
|
|||
# Timeout used in heuristic check for WAF/IPS protected targets
|
||||
IPS_WAF_CHECK_TIMEOUT = 10
|
||||
|
||||
# HTTP status codes a WAF/IPS typically returns when it blocks a request. Used to reject a boolean
|
||||
# "injection" whose only TRUE/FALSE difference is the always-true payload being blocked (a status-code
|
||||
# false positive) rather than the back-end actually answering.
|
||||
WAF_BLOCK_HTTP_CODES = (403, 406, 429, 451, 501, 503)
|
||||
|
||||
# Candidate tamper scripts for automatic WAF-bypass, ordered by empirical WAF-bypass value
|
||||
# (structural token-substitution first, camouflage last; per identYwaf data). The back-end DBMS
|
||||
# is not pre-filtered here: semantics-preservation is verified at runtime by re-running detection
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ def vulnTest():
|
|||
("-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=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 --prove", ("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.")), # --prove: 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 --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
|
||||
("-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")),
|
||||
("-c <config>", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")),
|
||||
("-l <log> --flush-session --keep-alive --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")),
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ def cmdLineParser(argv=None):
|
|||
injection.add_argument("--tamper", dest="tamper",
|
||||
help="Use given script(s) for tampering injection data")
|
||||
|
||||
injection.add_argument("--prove", dest="prove", action="store_true",
|
||||
injection.add_argument("--proof", dest="proof", action="store_true",
|
||||
help="Prove exploitation of the detected injection point(s)")
|
||||
|
||||
# Detection options
|
||||
|
|
|
|||
|
|
@ -162,8 +162,8 @@ def _goInferenceFields(expression, expressionFields, expressionFieldsList, paylo
|
|||
|
||||
def _goInferenceProxy(expression, fromUser=False, batch=False, unpack=True, charsetType=None, firstChar=None, lastChar=None, dump=False):
|
||||
"""
|
||||
Retrieve the output of a SQL query characted by character taking
|
||||
advantage of an blind SQL injection vulnerability on the affected
|
||||
Retrieve the output of a SQL query character by character taking
|
||||
advantage of a blind SQL injection vulnerability on the affected
|
||||
parameter through a bisection algorithm.
|
||||
"""
|
||||
|
||||
|
|
@ -209,9 +209,11 @@ def _goInferenceProxy(expression, fromUser=False, batch=False, unpack=True, char
|
|||
test = False
|
||||
|
||||
if test:
|
||||
# Count the number of SQL query entries output
|
||||
countFirstField = queries[Backend.getIdentifiedDbms()].count.query % expressionFieldsList[0]
|
||||
countedExpression = expression.replace(expressionFields, countFirstField, 1)
|
||||
# Count the number of SQL query entries output. NOTE: COUNT(*) (row count), not
|
||||
# COUNT(<first field>) - the latter excludes NULLs and would drop NULL-valued rows from
|
||||
# the dump (e.g. dumping a single column whose value is NULL on some rows).
|
||||
countField = queries[Backend.getIdentifiedDbms()].count.query % '*'
|
||||
countedExpression = expression.replace(expressionFields, countField, 1)
|
||||
|
||||
if " ORDER BY " in countedExpression.upper():
|
||||
_ = countedExpression.upper().rindex(" ORDER BY ")
|
||||
|
|
|
|||
|
|
@ -326,8 +326,10 @@ def errorUse(expression, dump=False):
|
|||
expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump)
|
||||
|
||||
if limitCond:
|
||||
# Count the number of SQL query entries output
|
||||
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1)
|
||||
# Count the number of SQL query entries output. NOTE: always COUNT(*) (row count); a single
|
||||
# field must NOT use COUNT(field) as that excludes NULLs and would drop NULL-valued rows from
|
||||
# the dump (e.g. a column whose value is NULL on some rows).
|
||||
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1)
|
||||
|
||||
if " ORDER BY " in countedExpression.upper():
|
||||
_ = countedExpression.upper().rindex(" ORDER BY ")
|
||||
|
|
|
|||
|
|
@ -295,8 +295,10 @@ def unionUse(expression, unpack=True, dump=False):
|
|||
expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump)
|
||||
|
||||
if limitCond:
|
||||
# Count the number of SQL query entries output
|
||||
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1)
|
||||
# Count the number of SQL query entries output. NOTE: always COUNT(*) (row count); a single
|
||||
# field must NOT use COUNT(field) as that excludes NULLs and would drop NULL-valued rows from
|
||||
# the dump (e.g. a column whose value is NULL on some rows).
|
||||
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1)
|
||||
|
||||
if " ORDER BY " in countedExpression.upper():
|
||||
_ = countedExpression.upper().rindex(" ORDER BY ")
|
||||
|
|
|
|||
|
|
@ -104,12 +104,16 @@ def _signalArtifacts(expression):
|
|||
return None, None
|
||||
|
||||
|
||||
def _proveBoolean(injection):
|
||||
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.
|
||||
"""
|
||||
|
||||
retVal = []
|
||||
|
|
@ -128,6 +132,10 @@ def _proveBoolean(injection):
|
|||
trueCode, trueTitle = _signalArtifacts("%d=%d" % (n, n))
|
||||
falseCode, falseTitle = _signalArtifacts("%d=%d" % (n, n + 1))
|
||||
|
||||
if signal is not None:
|
||||
signal["codeBased"] = bool(injection.conf.code)
|
||||
signal["trueCode"], signal["falseCode"] = trueCode, falseCode
|
||||
|
||||
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:
|
||||
|
|
@ -277,7 +285,7 @@ def _retrieveProof():
|
|||
def proveExploitation():
|
||||
"""
|
||||
Renders a report-grade, best-effort demonstration of exploitation for the confirmed injection point
|
||||
(option '--prove'), in the same style as sqlmap's injection-point summary so it reads naturally: the
|
||||
(option '--proof'), in the same style as sqlmap's injection-point summary so it reads naturally: the
|
||||
target URL and the confirmed injection point (parameter / type / title / payload), then the strongest
|
||||
proof first - an actual value read out of the back-end (drilling from the plain read to a more evasive
|
||||
one so a WAF/IPS does not stop it) - backed by a deterministic boolean differential (rendered with the
|
||||
|
|
@ -290,11 +298,12 @@ def proveExploitation():
|
|||
|
||||
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)
|
||||
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)
|
||||
|
|
@ -330,16 +339,40 @@ def proveExploitation():
|
|||
if sdata.payload:
|
||||
payload = urldecode(agent.adjustLateValues(sdata.payload), unsafe="&", spaceplus=(injection.place != PLACE.GET and kb.postSpaceToPlus))
|
||||
fields.append(_field("Payload", payload))
|
||||
if proof:
|
||||
fields.append(_field("Proof", proof))
|
||||
if rungs:
|
||||
# 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)
|
||||
|
||||
if proven:
|
||||
if proof:
|
||||
fields.append(_field("Proof", proof))
|
||||
for label, text in rungs:
|
||||
fields.append(_field(label, text))
|
||||
header = "sqlmap proved exploitation of the following injection point"
|
||||
else:
|
||||
fields.append(_field("Retrieved", "(no value could be read back; the proof above still confirms exploitation)"))
|
||||
if proof:
|
||||
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 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)
|
||||
header = "sqlmap proved exploitation of the following injection point"
|
||||
conf.dumper.string(header, data)
|
||||
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue