From f4c4025bdb45b430fd3530ca328a8fa86c723e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Fri, 10 Jul 2026 12:51:25 +0200 Subject: [PATCH] Minor patch --- extra/vulnserver/vulnserver.py | 34 ++++++++++++++++++++++++++++++++ lib/core/settings.py | 2 +- lib/core/testing.py | 9 +++++---- lib/techniques/graphql/inject.py | 34 ++++++++++++++++++++++---------- 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/extra/vulnserver/vulnserver.py b/extra/vulnserver/vulnserver.py index 1a29dbb4f..e1a438864 100644 --- a/extra/vulnserver/vulnserver.py +++ b/extra/vulnserver/vulnserver.py @@ -11,11 +11,13 @@ from __future__ import print_function import base64 import json +import os import random import re import sqlite3 import string import sys +import tempfile import threading import time import traceback @@ -24,6 +26,18 @@ PY3 = sys.version_info >= (3, 0) UNICODE_ENCODING = "utf-8" DEBUG = False +# A benign file with random content/name that the XXE endpoint can disclose via a file:// +# external entity, so '--xxe --file-read' has a target in the vuln-test. Randomized (never a +# static literal) to match sqlmap's below-the-radar convention, so nothing here becomes a +# blacklistable signature. In-process server, so callers read these same values. +XXE_READ_MARKER = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(20)) +XXE_READ_FILE = os.path.join(tempfile.gettempdir(), "%s.txt" % "".join(random.choice(string.ascii_lowercase) for _ in range(12))) +try: + with open(XXE_READ_FILE, "w") as _f: + _f.write(XXE_READ_MARKER + "\n") +except (IOError, OSError): + pass + if PY3: from http.client import FORBIDDEN from http.client import INTERNAL_SERVER_ERROR @@ -945,6 +959,26 @@ class ReqHandler(BaseHTTPRequestHandler): self.wfile.write(b"Request blocked: security policy violation (WAF)") return + if self.url == "/xxe": + self.send_response(OK) + self.send_header("Content-type", "application/xml; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + body = getattr(self, "data", "") or "" + try: + from lxml import etree + # VULNERABLE: a parser configured to load DTDs and resolve entities (incl. + # external file:// general entities) - the textbook XXE misconfiguration. + parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=True) + root = etree.fromstring(body.encode(UNICODE_ENCODING), parser) + output = "%s" % "".join(root.itertext()) # reflects expanded entities + except Exception as ex: + output = "%s: %s" % (type(ex).__name__, ex) # parser diagnostic (error-based tier) + + self.wfile.write(output.encode(UNICODE_ENCODING, "ignore")) + return + if self.url == "/csrf": if self.params.get("csrf_token") == _csrf_token: self.url = "/" diff --git a/lib/core/settings.py b/lib/core/settings.py index aaa47cec6..7eb7576fb 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.68" +VERSION = "1.10.7.69" 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) diff --git a/lib/core/testing.py b/lib/core/testing.py index cc0215baa..c983ebdc6 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -97,6 +97,7 @@ def vulnTest(tests=None, label="vuln"): ("-u \"xpath/search?q=x\" --xpath --flush-session --disable-hashing", ("is vulnerable to XPath injection", "Title: XPath boolean-based blind", "XPath: GET parameter 'q' XML tree", "extracted", "XPath scan complete")), # XPath: error-based detection + boolean oracle + blind XML tree-walking via starts-with character extraction ("-u \"ssti/search?q=x\" --ssti --flush-session --disable-hashing", ("is vulnerable to SSTI", "Title: SSTI Jinja2 injection", "back-end template engine: 'Jinja2'", "in-band arithmetic proof confirmed", "SSTI scan complete")), # SSTI: Jinja2 detection via arithmetic control-pair + boolean oracle + distinguishing probe ("-u \"hql/search?name=admin\" -p name --hql --flush-session --disable-hashing", ("is vulnerable to HQL injection", "back-end: 'Hibernate'", "entity 'Users'", "s3cr3t", "HQL scan complete")), # HQL: error-based Hibernate fingerprint + boolean oracle + error-leaked entity + blind attribute enumeration and substring value extraction + ("-u \"xxe\" --data=\"x\" --xxe --file-read=\"%s\" --flush-session" % vulnserver.XXE_READ_FILE, ("the XML body processes DTD/internal entities", "in-band XXE file-read impact confirmed", "Type: XXE injection", "XXE scan complete")), # XXE: in-band internal-entity reflection (real libxml2/lxml parser) + external file:// entity file read ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), ("-d \"\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")), ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)), @@ -104,14 +105,14 @@ def vulnTest(tests=None, label="vuln"): ("--purge -v 3", ("~ERROR", "~CRITICAL", "deleting the whole directory tree")), ) - # The vulnserver's XPath endpoint renders with lxml and its SSTI endpoint with jinja2; where those - # optional third-party engines are not importable (e.g. PyPy 2.7, which has no lxml wheel), skip + # The vulnserver's XPath and XXE endpoints render with lxml and its SSTI endpoint with jinja2; where + # those optional third-party engines are not importable (e.g. PyPy 2.7, which has no lxml wheel), skip # just those entries instead of failing the whole run - the rest of the suite is unaffected. try: __import__("lxml") except ImportError: - TESTS = tuple(_ for _ in TESTS if "--xpath" not in _[0]) - logger.warning("skipping the XPath vuln-test entry ('lxml' not available)") + TESTS = tuple(_ for _ in TESTS if "--xpath" not in _[0] and "--xxe" not in _[0]) + logger.warning("skipping the XPath and XXE vuln-test entries ('lxml' not available)") try: __import__("jinja2") except ImportError: diff --git a/lib/techniques/graphql/inject.py b/lib/techniques/graphql/inject.py index 00e6abcf8..d5eb9c750 100644 --- a/lib/techniques/graphql/inject.py +++ b/lib/techniques/graphql/inject.py @@ -709,12 +709,18 @@ def _detectBoolean(slot, endpoint): return None, None truePage, _ = _gqlSend(endpoint, trueQuery) + truePage2, _ = _gqlSend(endpoint, trueQuery) falsePage, _ = _gqlSend(endpoint, falseQuery) trueVal = _slotValue(truePage) + trueVal2 = _slotValue(truePage2) falseVal = _slotValue(falsePage) - if _ratio(trueVal, falseVal) < (1.0 - _MIN_RATIO_DIFF): + # Require the true response to be REPRODUCIBLE (trueVal ~= trueVal2) and to diverge + # from the false response. A single true-vs-false compare turns page jitter into a + # false positive; a reproducibility guard (like the other non-SQL engines' _boolean) + # rejects it, since a jittery page also fails to reproduce against itself. + if _ratio(trueVal, trueVal2) >= (1.0 - _MIN_RATIO_DIFF) and _ratio(trueVal, falseVal) < (1.0 - _MIN_RATIO_DIFF): return "boolean-based blind (string)", truePage return None, None @@ -731,21 +737,29 @@ def _detectTime(slot, endpoint): if not baseQuery: return None, None, None - start = time.time() - _gqlSend(endpoint, baseQuery) - baseline = time.time() - start + def elapsed(query): + start = time.time() + _gqlSend(endpoint, query) + return time.time() - start + baseline = elapsed(baseQuery) delay = conf.timeSec + cutoff = baseline + delay * 0.5 + for dbms, dialect in DIALECTS.items(): if not dialect.delay: continue - query = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, dialect.delay("1=1", delay))) - if not query: + sleepQuery = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, dialect.delay("1=1", delay))) + if not sleepQuery or elapsed(sleepQuery) <= cutoff: continue - start = time.time() - _gqlSend(endpoint, query) - if (time.time() - start) > baseline + delay * 0.5: - return "time-based blind", baseline + delay * 0.5, dbms + + # Confirm before attributing: the delay must REPRODUCE and a false-condition + # control must stay fast. A single sample turns jitter/a uniformly-slow endpoint + # into a false positive and can pin the wrong dialect; requiring the delay to + # track the condition rules both out. + controlQuery = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, dialect.delay("1=2", delay))) + if elapsed(sleepQuery) > cutoff and (controlQuery is None or elapsed(controlQuery) <= cutoff): + return "time-based blind", cutoff, dbms return None, None, None