Skip to content

Commit f4c4025

Browse files
committed
Minor patch
1 parent 986933e commit f4c4025

4 files changed

Lines changed: 64 additions & 15 deletions

File tree

‎extra/vulnserver/vulnserver.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
import base64
1313
import json
14+
import os
1415
import random
1516
import re
1617
import sqlite3
1718
import string
1819
import sys
20+
import tempfile
1921
import threading
2022
import time
2123
import traceback
@@ -24,6 +26,18 @@
2426
UNICODE_ENCODING = "utf-8"
2527
DEBUG = False
2628

29+
# A benign file with random content/name that the XXE endpoint can disclose via a file://
30+
# external entity, so '--xxe --file-read' has a target in the vuln-test. Randomized (never a
31+
# static literal) to match sqlmap's below-the-radar convention, so nothing here becomes a
32+
# blacklistable signature. In-process server, so callers read these same values.
33+
XXE_READ_MARKER = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(20))
34+
XXE_READ_FILE = os.path.join(tempfile.gettempdir(), "%s.txt" % "".join(random.choice(string.ascii_lowercase) for _ in range(12)))
35+
try:
36+
with open(XXE_READ_FILE, "w") as _f:
37+
_f.write(XXE_READ_MARKER + "\n")
38+
except (IOError, OSError):
39+
pass
40+
2741
if PY3:
2842
from http.client import FORBIDDEN
2943
from http.client import INTERNAL_SERVER_ERROR
@@ -945,6 +959,26 @@ def do_REQUEST(self):
945959
self.wfile.write(b"<html><body>Request blocked: security policy violation (WAF)</body></html>")
946960
return
947961

962+
if self.url == "/xxe":
963+
self.send_response(OK)
964+
self.send_header("Content-type", "application/xml; charset=%s" % UNICODE_ENCODING)
965+
self.send_header("Connection", "close")
966+
self.end_headers()
967+
968+
body = getattr(self, "data", "") or ""
969+
try:
970+
from lxml import etree
971+
# VULNERABLE: a parser configured to load DTDs and resolve entities (incl.
972+
# external file:// general entities) - the textbook XXE misconfiguration.
973+
parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=True)
974+
root = etree.fromstring(body.encode(UNICODE_ENCODING), parser)
975+
output = "<result>%s</result>" % "".join(root.itertext()) # reflects expanded entities
976+
except Exception as ex:
977+
output = "<error>%s: %s</error>" % (type(ex).__name__, ex) # parser diagnostic (error-based tier)
978+
979+
self.wfile.write(output.encode(UNICODE_ENCODING, "ignore"))
980+
return
981+
948982
if self.url == "/csrf":
949983
if self.params.get("csrf_token") == _csrf_token:
950984
self.url = "/"

‎lib/core/settings.py‎

Lines changed: 1 addition & 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.68"
23+
VERSION = "1.10.7.69"
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)

‎lib/core/testing.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,22 @@ def vulnTest(tests=None, label="vuln"):
9797
("-u \"<base>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
9898
("-u \"<base>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
9999
("-u \"<base>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
100+
("-u \"<base>xxe\" --data=\"<root><q>x</q></root>\" --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
100101
("-u \"<url>&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")),
101102
("-d \"<direct>\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")),
102103
("-d \"<direct>\" --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'",)),
103104
("-u <base>csrf --data=\"id=1&csrf_token=1\" --banner --answers=\"update=y\" --flush-session --technique=B", ("back-end DBMS: SQLite", "banner: '3.")),
104105
("--purge -v 3", ("~ERROR", "~CRITICAL", "deleting the whole directory tree")),
105106
)
106107

107-
# The vulnserver's XPath endpoint renders with lxml and its SSTI endpoint with jinja2; where those
108-
# optional third-party engines are not importable (e.g. PyPy 2.7, which has no lxml wheel), skip
108+
# The vulnserver's XPath and XXE endpoints render with lxml and its SSTI endpoint with jinja2; where
109+
# those optional third-party engines are not importable (e.g. PyPy 2.7, which has no lxml wheel), skip
109110
# just those entries instead of failing the whole run - the rest of the suite is unaffected.
110111
try:
111112
__import__("lxml")
112113
except ImportError:
113-
TESTS = tuple(_ for _ in TESTS if "--xpath" not in _[0])
114-
logger.warning("skipping the XPath vuln-test entry ('lxml' not available)")
114+
TESTS = tuple(_ for _ in TESTS if "--xpath" not in _[0] and "--xxe" not in _[0])
115+
logger.warning("skipping the XPath and XXE vuln-test entries ('lxml' not available)")
115116
try:
116117
__import__("jinja2")
117118
except ImportError:

‎lib/techniques/graphql/inject.py‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -709,12 +709,18 @@ def _detectBoolean(slot, endpoint):
709709
return None, None
710710

711711
truePage, _ = _gqlSend(endpoint, trueQuery)
712+
truePage2, _ = _gqlSend(endpoint, trueQuery)
712713
falsePage, _ = _gqlSend(endpoint, falseQuery)
713714

714715
trueVal = _slotValue(truePage)
716+
trueVal2 = _slotValue(truePage2)
715717
falseVal = _slotValue(falsePage)
716718

717-
if _ratio(trueVal, falseVal) < (1.0 - _MIN_RATIO_DIFF):
719+
# Require the true response to be REPRODUCIBLE (trueVal ~= trueVal2) and to diverge
720+
# from the false response. A single true-vs-false compare turns page jitter into a
721+
# false positive; a reproducibility guard (like the other non-SQL engines' _boolean)
722+
# rejects it, since a jittery page also fails to reproduce against itself.
723+
if _ratio(trueVal, trueVal2) >= (1.0 - _MIN_RATIO_DIFF) and _ratio(trueVal, falseVal) < (1.0 - _MIN_RATIO_DIFF):
718724
return "boolean-based blind (string)", truePage
719725

720726
return None, None
@@ -731,21 +737,29 @@ def _detectTime(slot, endpoint):
731737
if not baseQuery:
732738
return None, None, None
733739

734-
start = time.time()
735-
_gqlSend(endpoint, baseQuery)
736-
baseline = time.time() - start
740+
def elapsed(query):
741+
start = time.time()
742+
_gqlSend(endpoint, query)
743+
return time.time() - start
737744

745+
baseline = elapsed(baseQuery)
738746
delay = conf.timeSec
747+
cutoff = baseline + delay * 0.5
748+
739749
for dbms, dialect in DIALECTS.items():
740750
if not dialect.delay:
741751
continue
742-
query = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, dialect.delay("1=1", delay)))
743-
if not query:
752+
sleepQuery = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, dialect.delay("1=1", delay)))
753+
if not sleepQuery or elapsed(sleepQuery) <= cutoff:
744754
continue
745-
start = time.time()
746-
_gqlSend(endpoint, query)
747-
if (time.time() - start) > baseline + delay * 0.5:
748-
return "time-based blind", baseline + delay * 0.5, dbms
755+
756+
# Confirm before attributing: the delay must REPRODUCE and a false-condition
757+
# control must stay fast. A single sample turns jitter/a uniformly-slow endpoint
758+
# into a false positive and can pin the wrong dialect; requiring the delay to
759+
# track the condition rules both out.
760+
controlQuery = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, dialect.delay("1=2", delay)))
761+
if elapsed(sleepQuery) > cutoff and (controlQuery is None or elapsed(controlQuery) <= cutoff):
762+
return "time-based blind", cutoff, dbms
749763

750764
return None, None, None
751765

0 commit comments

Comments
 (0)