Skip to content

Commit 1ee368f

Browse files
committed
Fixes #6105
1 parent e10092e commit 1ee368f

3 files changed

Lines changed: 49 additions & 17 deletions

File tree

lib/controller/checks.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,14 @@ def _(page):
12231223
if conf.beep:
12241224
beep()
12251225

1226+
def _search(regex):
1227+
# Note: on a rare (e.g. huge) response the regex engine itself can fail, and losing one
1228+
# advisory heuristic beats losing the whole run (e.g. #5994 and #6105)
1229+
try:
1230+
return re.search(regex, page or "")
1231+
except (SystemError, RuntimeError) as ex:
1232+
logger.debug("skipping heuristic check because of a regex engine failure ('%s')" % getSafeExString(ex))
1233+
12261234
try:
12271235
for match in re.finditer(FI_ERROR_REGEX, page or ""):
12281236
if randStr1.lower() in match.group(0).lower():
@@ -1234,71 +1242,71 @@ def _(page):
12341242

12351243
break
12361244
except (SystemError, RuntimeError) as ex:
1237-
logger.debug("Skipping FI heuristic due to regex failure: %s", getSafeExString(ex))
1245+
logger.debug("skipping heuristic check because of a regex engine failure ('%s')" % getSafeExString(ex))
12381246

1239-
if not conf.nosql and re.search(NOSQL_ERROR_REGEX, page or ""):
1247+
if not conf.nosql and _search(NOSQL_ERROR_REGEX):
12401248
infoMsg = "heuristic (NoSQL) test shows that %sparameter '%s' might be vulnerable to NoSQL injection attacks (rerun with switch '--nosql')" % ("%s " % paramType if paramType != parameter else "", parameter)
12411249
logger.info(infoMsg)
12421250

12431251
if conf.beep:
12441252
beep()
12451253

1246-
if not conf.graphql and re.search(GRAPHQL_ERROR_REGEX, page or ""):
1254+
if not conf.graphql and _search(GRAPHQL_ERROR_REGEX):
12471255
infoMsg = "heuristic (GraphQL) test shows that %sparameter '%s' appears to be a GraphQL endpoint (rerun with switch '--graphql')" % ("%s " % paramType if paramType != parameter else "", parameter)
12481256
logger.info(infoMsg)
12491257

12501258
if conf.beep:
12511259
beep()
12521260

1253-
if not conf.ldap and re.search(LDAP_ERROR_REGEX, page or ""):
1261+
if not conf.ldap and _search(LDAP_ERROR_REGEX):
12541262
infoMsg = "heuristic (LDAP) test shows that %sparameter '%s' might be vulnerable to LDAP injection (rerun with switch '--ldap')" % ("%s " % paramType if paramType != parameter else "", parameter)
12551263
logger.info(infoMsg)
12561264

12571265
if conf.beep:
12581266
beep()
12591267

1260-
if not conf.xpath and re.search(XPATH_ERROR_REGEX, page or ""):
1268+
if not conf.xpath and _search(XPATH_ERROR_REGEX):
12611269
infoMsg = "heuristic (XPath) test shows that %sparameter '%s' might be vulnerable to XPath injection (rerun with switch '--xpath')" % ("%s " % paramType if paramType != parameter else "", parameter)
12621270
logger.info(infoMsg)
12631271

12641272
if conf.beep:
12651273
beep()
12661274

1267-
if not conf.ssti and re.search(SSTI_ERROR_REGEX, page or ""):
1275+
if not conf.ssti and _search(SSTI_ERROR_REGEX):
12681276
infoMsg = "heuristic (SSTI) test shows that %sparameter '%s' might be vulnerable to server-side template injection (rerun with switch '--ssti')" % ("%s " % paramType if paramType != parameter else "", parameter)
12691277
logger.info(infoMsg)
12701278

12711279
if conf.beep:
12721280
beep()
12731281

1274-
if not conf.hql and re.search(HQL_ERROR_REGEX, page or ""):
1282+
if not conf.hql and _search(HQL_ERROR_REGEX):
12751283
infoMsg = "heuristic (HQL) test shows that %sparameter '%s' might be vulnerable to HQL/JPQL (Hibernate ORM) injection (rerun with switch '--hql')" % ("%s " % paramType if paramType != parameter else "", parameter)
12761284
logger.info(infoMsg)
12771285

12781286
if conf.beep:
12791287
beep()
12801288

1281-
if not conf.xslt and re.search(XSLT_ERROR_REGEX, page or ""):
1289+
if not conf.xslt and _search(XSLT_ERROR_REGEX):
12821290
infoMsg = "heuristic (XSLT) test shows that %sparameter '%s' might be vulnerable to XSLT injection (rerun with switch '--xslt')" % ("%s " % paramType if paramType != parameter else "", parameter)
12831291
logger.info(infoMsg)
12841292
if conf.beep:
12851293
beep()
12861294

1287-
if not conf.sparql and re.search(SPARQL_ERROR_REGEX, page or ""):
1295+
if not conf.sparql and _search(SPARQL_ERROR_REGEX):
12881296
infoMsg = "heuristic (SPARQL) test shows that %sparameter '%s' might be vulnerable to SPARQL injection (rerun with switch '--sparql')" % ("%s " % paramType if paramType != parameter else "", parameter)
12891297
logger.info(infoMsg)
12901298

12911299
if conf.beep:
12921300
beep()
12931301

1294-
if not conf.odata and re.search(ODATA_ERROR_REGEX, page or ""):
1302+
if not conf.odata and _search(ODATA_ERROR_REGEX):
12951303
infoMsg = "heuristic (OData) test shows that %sparameter '%s' might be vulnerable to OData $filter injection (rerun with switch '--odata')" % ("%s " % paramType if paramType != parameter else "", parameter)
12961304
logger.info(infoMsg)
12971305

12981306
if conf.beep:
12991307
beep()
13001308

1301-
if not conf.xxe and kb.postHint in (POST_HINT.XML, POST_HINT.SOAP) and re.search(XXE_ERROR_REGEX, page or ""):
1309+
if not conf.xxe and kb.postHint in (POST_HINT.XML, POST_HINT.SOAP) and _search(XXE_ERROR_REGEX):
13021310
infoMsg = "heuristic (XXE) test shows that the XML request body might be vulnerable to XML External Entity injection (rerun with switch '--xxe')"
13031311
logger.info(infoMsg)
13041312

lib/core/settings.py

Lines changed: 9 additions & 6 deletions
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.9.3"
23+
VERSION = "1.10.9.4"
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)
@@ -1193,7 +1193,10 @@
11931193
("Python ElementTree", r"xml\.etree\.ElementTree\.(?:ParseError|Element)"),
11941194
# NOT XSLT: a dedicated '--xslt' engine owns those errors now, and claiming them here made every
11951195
# XSLT parser error suggest '--xpath' as well
1196-
("Generic XPath", r"XPath.*?(?:error|exception|syntax)"),
1196+
# NOTE: the gap has to stay bounded (like in 'Handlebars' below). An unbounded '.*?' turns this
1197+
# into a quadratic scan of every long line that merely carries the word 'xpath' (e.g. minified
1198+
# JS/JSON), which took ~25s on a 400KB response - and blew up the regex engine itself (#6105)
1199+
("Generic XPath", r"XPath[^\n]{0,100}?(?:error|exception|syntax)"),
11971200
("Generic XPath", r"Invalid XPath|XPath evaluation failed"),
11981201
)
11991202

@@ -1337,7 +1340,7 @@
13371340
("Velocity", r"org\.apache\.velocity\.(?:runtime|exception)\.\w+|ParseErrorException|MethodInvocationException|ResourceNotFoundException"),
13381341
("Spring EL / Thymeleaf", r"org\.springframework\.expression\.\w+|org\.thymeleaf\.\w+|SpelEvaluationException|TemplateProcessingException|ExpressionParsingException"),
13391342
("Struts2 (OGNL)", r"ognl\.(?:OgnlException|NoSuchPropertyException|MethodFailedException|InappropriateExpressionException|ExpressionSyntaxException)|com\.opensymphony\.xwork2|org\.apache\.struts2|There is no Action mapped for|Struts (?:Problem Report|has detected an unhandled exception)"),
1340-
("ERB", r"\(erb\):\d+|NameError.*undefined local variable"),
1343+
("ERB", r"\(erb\):\d+|NameError[^\n]{0,100}?undefined local variable"),
13411344
# NOTE: these must stay anchored to a diagnostic. The bare product names matched any page that
13421345
# carries the word 'pug'/'jade'/'handlebars' (a surname, a colour, a <script src=> of the runtime),
13431346
# and the bare 'ParseError' matched lxml.etree.XSLTParseError and ElementTree.ParseError
@@ -1361,7 +1364,7 @@
13611364
("Java (Xerces/JAXP)", r"(?:org\.xml\.sax\.SAXParseException|com\.sun\.org\.apache\.xerces|javax\.xml\.stream\.XMLStreamException|The (?:entity|element type) \"[^\"]*\" was referenced|DOCTYPE is disallowed when the feature|External (?:DTD|parsed entities|Entity): failed|\"[^\"]*\" must be declared|had to be read but the maximum)"),
13621365
(".NET System.Xml", r"(?:System\.Xml\.XmlException|For security reasons DTD is prohibited|Reference to undeclared entity|An error occurred while parsing EntityName|XmlTextReaderImpl)"),
13631366
("Python expat", r"(?:xml\.parsers\.expat\.ExpatError|undefined entity|not well-formed \(invalid token\)|ExpatError)"),
1364-
("Ruby Nokogiri/REXML", r"(?:Nokogiri::XML::SyntaxError|REXML::ParseException|Entity .* not defined)"),
1367+
("Ruby Nokogiri/REXML", r"(?:Nokogiri::XML::SyntaxError|REXML::ParseException|Entity [^\n]{0,100}? not defined)"),
13651368
("Go encoding/xml", r"XML syntax error on line \d+"),
13661369
# NOTE: 'unexpected end of ...' is what every parser says, not what an XML parser says. It matched
13671370
# the "Unexpected end of query" of BaseX, the "Unexpected <EOF>" of GraphQL and the "Unexpected end
@@ -1417,7 +1420,7 @@
14171420
("Hibernate", r"(?:unexpected (?:token:|end of subtree|AST node)|Could not (?:resolve|interpret) (?:attribute|root entity|path|property))"),
14181421
("EclipseLink / JPQL", r"(?:org\.eclipse\.persistence\.exceptions\.JPQLException|Exception \[EclipseLink|Problem compiling \[|An exception occurred while creating a query)"),
14191422
("JPA / JPQL", r"(?:javax|jakarta)\.persistence\.(?:PersistenceException|Query(?:Syntax|Timeout)?Exception)"),
1420-
("Generic HQL/JPQL", r"(?:HQL|JPQL|EJBQL)\b.*?(?:error|exception|syntax|not (?:mapped|resolve))"),
1423+
("Generic HQL/JPQL", r"(?:HQL|JPQL|EJBQL)\b[^\n]{0,100}?(?:error|exception|syntax|not (?:mapped|resolve))"),
14211424
)
14221425

14231426
HQL_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in HQL_ERROR_SIGNATURES)
@@ -1484,7 +1487,7 @@
14841487
# too - matching on it alone mislabelled them as RDF4J, so only the package name is kept
14851488
("RDF4J / GraphDB", r"org\.eclipse\.rdf4j|org\.openrdf\.query"),
14861489
("Blazegraph", r"com\.bigdata\.rdf|\bBlazegraph\b"),
1487-
("rdflib", r"rdflib\.plugins\.sparql|\bParseException\b.*?(?:SPARQL|sparql)"),
1490+
("rdflib", r"rdflib\.plugins\.sparql|\bParseException\b[^\n]{0,100}?(?:SPARQL|sparql)"),
14881491
("Stardog", r"com\.(?:complexible\.)?stardog"),
14891492
)
14901493

tests/test_heuristic_signatures.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import os
2727
import re
2828
import sys
29+
import time
2930
import unittest
3031

3132
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -223,6 +224,26 @@ def test_nothing_fires_on_sql_errors_or_ordinary_pages(self):
223224
self.assertEqual(fired, (),
224225
msg="%s output suggests %s: %r" % (backend, '/'.join("'--%s'" % _ for _ in fired), text))
225226

227+
def test_no_signature_has_an_unbounded_gap(self):
228+
# an unbounded '.*' between a literal and a keyword is quadratic on a long line (minified
229+
# JS/JSON, a one-line JSON error body), which is how a stray 'xpath' in a 400KB response
230+
# cost ~25s per parameter - and made the regex engine itself blow up (#5994, #6105).
231+
# The bounded form ('[^\n]{0,100}?') matches the same real errors in constant work
232+
for name, regex in ENGINES:
233+
found = re.search(r"(?<!\\)\.[*+]", regex)
234+
self.assertIsNone(found, msg="'--%s' signatures carry an unbounded '%s' gap" % (name, found.group(0) if found else ""))
235+
236+
def test_signatures_stay_linear_on_a_long_line(self):
237+
# the same invariant, measured: every engine has to survive a single-line response that
238+
# carries the words its signatures start with, without any error actually being present
239+
page = ("<div class=\"x\">lorem ipsum dolor sit amet consectetur adipiscing elit</div>" + ''.join("%s " % _ for _ in ("xpath", "HQL", "ParseException", "NameError", "Entity x", "handlebars", "no such file", "Twig"))) * 2000
240+
241+
for name, regex in ENGINES:
242+
start = time.time()
243+
re.search(regex, page)
244+
elapsed = time.time() - start
245+
self.assertLess(elapsed, 2, msg="'--%s' signatures took %.1fs on a %dKB single-line response" % (name, elapsed, len(page) // 1024))
246+
226247
def test_every_engine_is_covered(self):
227248
# a new switch must arrive here with its own errors, or the matrix above proves nothing about it
228249
owners = set(owner for owner, _, _ in CORPUS)

0 commit comments

Comments
 (0)