#!/usr/bin/env python """ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission Cross-engine invariants of the non-SQL heuristic signatures in lib/core/settings.py. heuristicCheckSqlInjection() tries every one of these regexes against the same response and prints a hint for each one that matches. There is no order and no priority, so a signature that is a little too generous does not merely lose precision - it makes sqlmap recommend a switch that has nothing to do with the target. The two ways that happens: overlap one engine claims another engine's error (a stylesheet failure suggesting '--xpath', a Hibernate failure suggesting '--nosql', an XML parser failure suggesting '--xpath'). false positive a signature matches a page that is not an error at all - a surname that contains 'pug', a "), (NOBODY, "asset (handlebars)", ""), (NOBODY, "colour name", ""), (NOBODY, "error page", "

500 Internal Server Error

The server encountered an internal error.

"), (NOBODY, "CSS comment", "/* template error state: .alert{color:red} */"), (NOBODY, "documentation", "This page explains how to use a query exception handler in your application."), (NOBODY, "calculator", "Invalid expression: 2++2"), ) # The only overlaps that are allowed, because the error text really does belong to both. libxslt and # .NET report the failure of an XPath expression that sits inside a stylesheet, so both switches are # worth trying - which is what the response says. Anything else is a bug in the signatures. ACCEPTED_SHARED = { ("xslt", ".NET"): ("xpath",), ("xslt", "libxslt runtime"): ("xpath",), # breaking a stylesheet also breaks the XML that carries it, so libxml2 reports a malformed # document as well. Harmless: the XXE hint needs an XML/SOAP request body to fire at all ("xslt", "libxslt (live)"): ("xxe",), } def _matches(text): return tuple(name for name, regex in ENGINES if re.search(regex, text)) class HeuristicSignatureTest(unittest.TestCase): def test_each_engine_recognises_its_own_errors(self): for owner, backend, text in CORPUS: if owner == NOBODY: continue self.assertIn(owner, _matches(text), msg="'--%s' no longer recognises its own %s error: %r" % (owner, backend, text)) def test_no_engine_claims_another_engines_error(self): for owner, backend, text in CORPUS: if owner == NOBODY: continue allowed = ACCEPTED_SHARED.get((owner, backend), ()) stolen = [_ for _ in _matches(text) if _ != owner and _ not in allowed] self.assertEqual(stolen, [], msg="a %s %s error also suggests %s" % (owner, backend, '/'.join("'--%s'" % _ for _ in stolen))) def test_nothing_fires_on_sql_errors_or_ordinary_pages(self): for owner, backend, text in CORPUS: if owner != NOBODY: continue fired = _matches(text) self.assertEqual(fired, (), msg="%s output suggests %s: %r" % (backend, '/'.join("'--%s'" % _ for _ in fired), text)) def test_no_signature_has_an_unbounded_gap(self): # an unbounded '.*' between a literal and a keyword is quadratic on a long line (minified # JS/JSON, a one-line JSON error body), which is how a stray 'xpath' in a 400KB response # cost ~25s per parameter - and made the regex engine itself blow up (#5994, #6105). # The bounded form ('[^\n]{0,100}?') matches the same real errors in constant work for name, regex in ENGINES: found = re.search(r"(?lorem ipsum dolor sit amet consectetur adipiscing elit" + ''.join("%s " % _ for _ in ("xpath", "HQL", "ParseException", "NameError", "Entity x", "handlebars", "no such file", "Twig"))) * 2000 for name, regex in ENGINES: start = time.time() re.search(regex, page) elapsed = time.time() - start self.assertLess(elapsed, 2, msg="'--%s' signatures took %.1fs on a %dKB single-line response" % (name, elapsed, len(page) // 1024)) def test_every_engine_is_covered(self): # a new switch must arrive here with its own errors, or the matrix above proves nothing about it owners = set(owner for owner, _, _ in CORPUS) missing = set(name for name, _ in ENGINES) - owners self.assertEqual(missing, set(), msg="engines with no corpus entry: %s" % missing) if __name__ == "__main__": unittest.main(verbosity=2)