#!/usr/bin/env python """ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission Offline, deterministic tests for the XXE injection engine. Pure helpers are exercised directly; detection tiers run against a mocked _send() so reflected/error/echo oracles can be simulated without a live target; and crafted payloads are parsed with real lxml to prove they are well-formed and actually expand the injected entity. """ import os import re import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _testutils import bootstrap bootstrap() import lib.techniques.xxe.inject as xxe from lib.core.data import conf from lib.core.data import kb class TestLooksXmlAndClean(unittest.TestCase): def test_looks_xml(self): self.assertTrue(xxe._looksXml("x")) self.assertTrue(xxe._looksXml(" ")) self.assertFalse(xxe._looksXml("id=1&name=x")) self.assertFalse(xxe._looksXml("{\"a\": 1}")) self.assertFalse(xxe._looksXml("")) def test_clean_body_strips_marks_and_bom(self): conf.data = u"\ufeffluther%s" % (kb.customInjectionMark or "*") cleaned = xxe._cleanBody() self.assertFalse(cleaned.startswith(u"\ufeff")) self.assertNotIn(kb.customInjectionMark or "*", cleaned) self.assertTrue(cleaned.startswith("")) class TestRootName(unittest.TestCase): def test_plain(self): self.assertEqual(xxe._rootName("x"), "user") def test_with_prolog_and_comment(self): self.assertEqual(xxe._rootName("x"), "order") def test_namespaced(self): self.assertEqual(xxe._rootName(''), "soap:Envelope") def test_existing_doctype_skipped(self): self.assertEqual(xxe._rootName(''), "user") class TestBuildDoctype(unittest.TestCase): SUBSET = '' def test_no_doctype_prepended(self): out = xxe._buildDoctype("x", "r", self.SUBSET) self.assertIn("x", "r", self.SUBSET) self.assertLess(out.index("]>x", "r", self.SUBSET) self.assertEqual(out.count("x', "r", self.SUBSET) self.assertEqual(out.count("' inside a quoted entity value must not fool the internal-subset splice out = xxe._buildDoctype('y">]>z', "r", self.SUBSET) self.assertEqual(out.count("z', "r", self.SUBSET) self.assertEqual(out.count("onetwo

", "&e;") self.assertEqual(out.count("&e;"), 2) self.assertNotIn("one", out) self.assertNotIn("two", out) def test_attributes_only_when_requested(self): text = 'luther' self.assertNotIn('id="&e;"', xxe._placeRef(text, "&e;")) # attrs off by default self.assertIn('id="&e;"', xxe._placeRef(text, "&e;", attrs=True)) # attrs on def test_xmlns_preserved(self): out = xxe._placeRef('x', "&e;", attrs=True) self.assertIn('xmlns:soap="ns"', out) # namespace decl untouched def test_self_closing_fallback(self): out = xxe._placeRef("", "&e;") self.assertIn("&e;", out) self.assertIn("", out) def test_empty_element_fallback(self): out = xxe._placeRef("", "&e;") self.assertIn("&e;", out) class TestGuards(unittest.TestCase): def test_echoed(self): self.assertTrue(xxe._echoed("... xxemarkzzzzother", "&e;") self.assertIn("&e;", out) self.assertIn("other", out) # other node left intact self.assertNotIn("xxemarkzzzz", out) def test_clean_body_sets_marker_on_user_marks(self): conf.data = "luther%s" % (kb.customInjectionMark or "*") kb.processUserMarks = True try: cleaned = xxe._cleanBody() self.assertIsNotNone(xxe._MARKER) self.assertIn(xxe._MARKER, cleaned) finally: kb.processUserMarks = False xxe._MARKER = None class TestReportMethod(unittest.TestCase): def test_report_uses_conf_method(self): captured = [] class _Dumper(object): def singleString(self, data, content_type=None): captured.append(data) old_dumper, old_method, old_beep = conf.get("dumper"), conf.get("method"), conf.get("beep") conf.dumper, conf.method, conf.beep = _Dumper(), "PUT", False try: xxe._report("Title", "Payload") finally: conf.dumper, conf.method, conf.beep = old_dumper, old_method, old_beep self.assertIn("PUT XML body", captured[0]) class TestOobBase64Capture(unittest.TestCase): def test_path_capture_survives_plus_slash_equals(self): import base64 from lib.core.convert import getText, decodeBase64 marker = "mk12345678" raw = b">>>\xff\xfe some + / = data ==" blob = getText(base64.b64encode(raw)) self.assertTrue(any(c in blob for c in "+/=")) # ensure the risky chars are present url = "http://webhook.site/tok/%s/%s" % (marker, blob) # base64 in the PATH m = re.search(r"/%s/([A-Za-z0-9+/=]+)" % re.escape(marker), url) self.assertIsNotNone(m) self.assertEqual(m.group(1), blob) self.assertEqual(decodeBase64(m.group(1)), raw) class TestDetectionMocked(unittest.TestCase): def setUp(self): self._send = xxe._send xxe.SENTINEL = "sentineltoken1" def tearDown(self): xxe._send = self._send def test_internal_reflected_positive(self): xxe._send = lambda body: "Hello, %s! (parsed)" % xxe.SENTINEL payload, _ = xxe._tryInternal("luther", "u", baseline="Hello, luther!") self.assertIsNotNone(payload) def test_internal_echo_rejected(self): # endpoint mirrors the raw body back (never parses) -> must NOT be a hit xxe._send = lambda body: "You sent: %s" % body payload, _ = xxe._tryInternal("luther", "u", baseline="You sent: luther") self.assertIsNone(payload) def test_internal_baseline_contains_sentinel_rejected(self): xxe._send = lambda body: "Hello, %s!" % xxe.SENTINEL payload, _ = xxe._tryInternal("luther", "u", baseline="already %s here" % xxe.SENTINEL) self.assertIsNone(payload) def test_error_based_positive(self): xxe._send = lambda body: 'XML error: failed to load external entity "file:///%s/nonexistent"' % xxe.SENTINEL payload, page = xxe._tryError("x", "u") self.assertIsNotNone(payload) self.assertIsNotNone(xxe._fingerprint(page)) def test_error_based_echo_rejected(self): xxe._send = lambda body: "You sent: %s" % body # echoes DOCTYPE/ENTITY -> _echoed guard payload, _ = xxe._tryError("x", "u") self.assertIsNone(payload) def test_error_exfil_extraction_base64(self): import base64 from lib.core.convert import getText secret = getText(base64.b64encode(b"root:x:0:0:root:/root:/bin/sh")) def mock(body): m = re.search(r'file:///(\w+)/%file;', body) or re.search(r'file:///(\w+)/%file;', body) marker = m.group(1) if m else "zzz" return 'failed to load "file:///%s/%s"' % (marker, secret) xxe._send = mock conf.fileRead = "/etc/passwd" try: content, name = xxe._tryErrorExfil("x", "u") finally: conf.fileRead = None self.assertEqual(name, "/etc/passwd") self.assertIn("root:x:0:0", content or "") class TestRealXmlPayloads(unittest.TestCase): """Prove crafted payloads are well-formed and actually expand the entity.""" @staticmethod def _expand(payload): try: from lxml import etree except ImportError: raise unittest.SkipTest("lxml not available") parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=True, huge_tree=False) doc = etree.fromstring(payload.encode("utf-8"), parser) return "".join(doc.itertext()) def test_internal_entity_expands(self): xxe.SENTINEL = "realxmlsentinel" ent = "abcd" subset = '' % (ent, xxe.SENTINEL) payload = xxe._placeRef(xxe._buildDoctype("luther", "u", subset), "&%s;" % ent) self.assertIn(xxe.SENTINEL, self._expand(payload)) def test_internal_entity_expands_with_existing_doctype(self): xxe.SENTINEL = "realxmlsentinel2" ent = "efgh" subset = '' % (ent, xxe.SENTINEL) base = ']>luther' payload = xxe._placeRef(xxe._buildDoctype(base, "u", subset), "&%s;" % ent) self.assertIn(xxe.SENTINEL, self._expand(payload)) def test_attribute_entity_expands(self): xxe.SENTINEL = "attrsentinel" ent = "ijkl" subset = '' % (ent, xxe.SENTINEL) payload = xxe._placeRef(xxe._buildDoctype('x', "u", subset), "&%s;" % ent, attrs=True) self.assertIn(xxe.SENTINEL, self._expand(payload)) if __name__ == "__main__": unittest.main()