@@ -656,20 +656,23 @@ def xxeScan():
656656 logger .info ("testing XXE injection on the XML request body (root element: '%s')" % rootName )
657657
658658 baseline = _send (xml )
659- found = False
659+ found = False # an actual impact/oracle (file read, error-based, XInclude, blind)
660+ expansionSeen = False # reflected DTD/internal-entity processing (weaker; must not stop the search)
660661
661662 # T2: in-band reflected DTD/internal-entity expansion. This proves the parser
662- # processes entities; it is NOT yet external-entity file-read impact - so the
663- # finding is worded conservatively and escalated only if an actual read follows.
663+ # 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.
664666 payload , page = _tryInternal (xml , rootName , baseline )
665667 if payload :
666- found = True
668+ expansionSeen = True
667669 logger .info ("the XML body processes DTD/internal entities (in-band reflection confirmed)" )
668670 _report ("In-band DTD/internal entity expansion" , payload )
669671
670672 if conf .get ("fileRead" ):
671673 content = _tryInbandFileRead (xml , rootName , conf .fileRead )
672674 if content :
675+ found = True
673676 logger .info ("in-band XXE file-read impact confirmed for '%s'" % conf .fileRead )
674677 _report ("In-band file read ('%s')" % conf .fileRead , "<in-band reflected read of '%s'>" % conf .fileRead )
675678 _dumpFileRead (conf .fileRead , content )
@@ -680,21 +683,24 @@ def xxeScan():
680683 snippet = _tryPhpFilter (xml , rootName , baseline )
681684 systemId = "php://filter" if snippet else None
682685 if systemId :
686+ found = True
683687 logger .info ("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId )
684688 _report ("In-band file-read impact (external entity '%s')" % systemId , "<external-entity read of a benign file for impact>" )
685689
686- # T3: error-based (works where entities are not reflected but errors leak)
690+ # T3: error-based (works where entities are not reflected but errors leak). A
691+ # redundant detection channel once in-band reflection was already seen, so it is
692+ # skipped then - the file-read *impact* tiers below still run to try to upgrade.
687693 errorChannel = False
688- if not found :
694+ if not found and not expansionSeen :
689695 payload , page = _tryError (xml , rootName )
690696 if payload :
691697 found = errorChannel = True
692698 backend = _fingerprint (page ) or "Generic XML"
693699 logger .info ("the XML body is vulnerable to XXE injection (error-based, back-end parser: '%s')" % backend )
694700 _report ("Error-based (parameter entity, back-end: '%s')" % backend , payload )
695701
696- # T3b: no-egress error-based via local-DTD repurposing
697- if not found :
702+ # T3b: no-egress error-based via local-DTD repurposing (detection; skip once reflected)
703+ if not found and not expansionSeen :
698704 payload , page = _tryLocalDtd (xml , rootName )
699705 if payload :
700706 found = errorChannel = True
@@ -721,27 +727,32 @@ def xxeScan():
721727 logger .info ("the XML body is vulnerable to XInclude file read ('%s'): '%s'" % (systemId , snippet ))
722728 _report ("XInclude file read ('%s')" % systemId , payload )
723729
724- # T5: WAF-evasion fallbacks (UTF-16 re-encoding, PUBLIC-for-SYSTEM)
725- if not found :
730+ # T5: WAF-evasion fallbacks (UTF-16 re-encoding, PUBLIC-for-SYSTEM). The UTF-16
731+ # variant re-detects internal-entity reflection, so it is redundant (and mislabels
732+ # as 'evasion') once reflection was already seen - skip it then.
733+ if not found and not expansionSeen :
726734 title , payload = _tryEvasions (xml , rootName , baseline )
727735 if title :
728736 found = True
729737 logger .info ("the XML body is vulnerable to XXE injection (%s)" % title .lower ())
730738 _report (title , payload )
731739
732- # T6: time-based blind (no collector, no third party) - external entity to a non-routable host
733- if not found :
740+ # T6: time-based blind (no collector, no third party) - external entity to a non-routable host.
741+ # Skipped once in-band reflection worked: the target is demonstrably not blind, so the (slow)
742+ # blind tiers add nothing and would needlessly stall.
743+ if not found and not expansionSeen :
734744 logger .debug ("attempting time-based blind XXE (external entity to a non-routable host); this can be slow" )
735745 payload = _tryTimeBlind (xml , rootName )
736746 if payload :
737747 found = True
738748 logger .info ("the XML body is vulnerable to XXE injection (time-based blind, external entity resolution reaches out-of-band)" )
739749 _report ("Time-based blind (external entity to non-routable host)" , payload )
740750
741- # T7: out-of-band tiers - THIRD PARTY, so only on explicit consent (default NO).
751+ # T7: out-of-band tiers - THIRD PARTY, so only on explicit consent (default NO). Also blind-only
752+ # (skipped when in-band reflection already worked, so a non-blind target never triggers the prompt).
742753 # Low-impact callback confirmation is the default; actual file exfiltration is
743754 # attempted only when the user explicitly asked for a file via '--file-read'.
744- if not found and _oobConsent ():
755+ if not found and not expansionSeen and _oobConsent ():
745756 if conf .get ("fileRead" ):
746757 exfil = _tryOobExfil (xml , rootName )
747758 if exfil and (exfil ["content" ] or exfil ["detected" ]):
@@ -762,6 +773,12 @@ def xxeScan():
762773 _report ("Out-of-band blind (collector callback: %s)" % protocol , payload )
763774
764775 if not found :
776+ if expansionSeen :
777+ # in-band entity processing is real, but no external-entity/blind oracle was reachable
778+ # (typically external entities disabled) - report honestly rather than overstate impact
779+ logger .info ("DTD/internal entity processing is enabled, but no external-entity file-read or blind XXE oracle was established" )
780+ logger .info ("XXE scan complete" )
781+ return
765782 # Reachable-but-not-exploitable diagnostics: distinguish a hardened parser
766783 # from a merely non-reflecting one so the user knows why it did not fire.
767784 probe = _send (_buildDoctype (xml , rootName , '<!ENTITY %% p SYSTEM "file:///%s">%%p;' % SENTINEL ))
0 commit comments