mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-07-12 19:43:21 +00:00
231 lines
9 KiB
Python
231 lines
9 KiB
Python
#!/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 HQL/JPQL (Hibernate ORM) injection engine. Mock
|
|
oracles stand in for the HTTP/ORM layer so detection, fingerprinting, entity leakage,
|
|
blind attribute enumeration, value extraction and output formatting can be exercised
|
|
without a live Hibernate target.
|
|
"""
|
|
|
|
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.hql.inject as hql
|
|
|
|
|
|
class TestHelpers(unittest.TestCase):
|
|
def test_ratio(self):
|
|
self.assertGreater(hql._ratio("abc", "abc"), 0.9)
|
|
self.assertLess(hql._ratio("abc", "xyz"), 0.5)
|
|
|
|
def test_is_error(self):
|
|
self.assertTrue(hql._isError("org.hibernate.query.SyntaxException: token recognition error at: '''"))
|
|
self.assertTrue(hql._isError("org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity 'Ghost'"))
|
|
self.assertFalse(hql._isError("normal page content"))
|
|
|
|
def test_backend_from_error(self):
|
|
self.assertEqual(hql._backendFromError("org.hibernate.query.SemanticException: boom"), "Hibernate")
|
|
self.assertIsNotNone(hql._backendFromError("org.eclipse.persistence.exceptions.JPQLException"))
|
|
self.assertIsNone(hql._backendFromError("plain page"))
|
|
|
|
def test_entity_from_error(self):
|
|
self.assertEqual(hql._entityFromError("Could not resolve attribute 'zzz' of 'lab.App$Member'"), "lab.App$Member")
|
|
self.assertEqual(hql._entityFromError("Could not resolve root entity 'Ghost'"), "Ghost")
|
|
self.assertIsNone(hql._entityFromError("nothing here"))
|
|
|
|
def test_short_entity(self):
|
|
self.assertEqual(hql._shortEntity("lab.App$Member"), "Member")
|
|
self.assertEqual(hql._shortEntity("com.acme.model.User"), "User")
|
|
self.assertEqual(hql._shortEntity("User"), "User")
|
|
|
|
|
|
class TestBoundary(unittest.TestCase):
|
|
def test_wrap_string(self):
|
|
b = hql.Boundary("' OR ", " OR '1'='2", True)
|
|
self.assertEqual(hql._wrap("SEN", b, "1=1"), "SEN' OR 1=1 OR '1'='2")
|
|
|
|
def test_base_sentinel_vs_numeric(self):
|
|
self.assertEqual(hql._base(hql.Boundary("' OR ", " OR '1'='2", True), "x"), hql.SENTINEL)
|
|
self.assertEqual(hql._base(hql.Boundary(" OR ", "", False), "5"), "-1")
|
|
|
|
def test_scalar_casts_attribute(self):
|
|
expr = hql._scalar("Member", "LENGTH(CAST(_h.id AS string))", "id")
|
|
self.assertIn("CAST(_h.id AS string)", expr)
|
|
self.assertIn("FROM Member _h", expr)
|
|
self.assertIn("MIN(_h2.id)", expr)
|
|
|
|
|
|
class TestDetection(unittest.TestCase):
|
|
def setUp(self):
|
|
self.original = hql._send
|
|
|
|
def tearDown(self):
|
|
hql._send = self.original
|
|
|
|
def test_boolean_detection(self):
|
|
# TRUE payloads return rows, FALSE payloads return an empty (but stable) page
|
|
def mock(place, parameter, value):
|
|
if "OR '1'='1" in value or "OR 1=1" in value:
|
|
return "<html><body><div>alice</div><div>bob</div></body></html>"
|
|
return "<html><body></body></html>"
|
|
hql._send = mock
|
|
template, payload, boundary = hql._detectBoolean("GET", "name")
|
|
self.assertIsNotNone(template)
|
|
self.assertIn("OR '1'='1", payload)
|
|
self.assertTrue(boundary.sentinel)
|
|
|
|
def test_no_detection_when_static(self):
|
|
hql._send = lambda place, parameter, value: "<html>same</html>"
|
|
template, _, _ = hql._detectBoolean("GET", "name")
|
|
self.assertIsNone(template)
|
|
|
|
|
|
def _recordOracle(record, entity="Member"):
|
|
"""Build a truth(predicate) that answers the LENGTH/SUBSTRING/EXISTS predicates
|
|
the engine emits, evaluated against an in-memory record dict."""
|
|
|
|
def truth(predicate):
|
|
# entity brute / attribute existence
|
|
m = re.search(r"FROM (\w+) _h", predicate)
|
|
if m and m.group(1) != entity:
|
|
return False
|
|
# entity brute: EXISTS(SELECT 1 FROM <entity> _h)
|
|
if re.search(r"EXISTS\(SELECT 1 FROM \w+ _h\)", predicate):
|
|
return True
|
|
# attribute existence: EXISTS(SELECT _h.<attr> FROM <entity> _h)
|
|
m = re.search(r"SELECT _h\.(\w+) FROM", predicate)
|
|
if m:
|
|
return m.group(1) in record
|
|
# length: LENGTH(CAST(_h.<attr> AS string)) ... >= N
|
|
if "LENGTH" in predicate:
|
|
m = re.search(r"CAST\(_h\.(\w+) AS string\)", predicate)
|
|
n = re.search(r">=(\d+)", predicate)
|
|
if m and n:
|
|
return len(str(record.get(m.group(1), ""))) >= int(n.group(1))
|
|
# char: LOCATE(SUBSTRING(CAST(_h.<attr> AS string),<pos>,1),'<lit>') ... >=<N>
|
|
if "LOCATE" in predicate:
|
|
m = re.search(r"SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'", predicate)
|
|
n = re.search(r">=(\d+)\s*$", predicate)
|
|
if m and n:
|
|
attr, pos, literal = m.group(1), int(m.group(2)), m.group(3)
|
|
value = str(record.get(attr, ""))
|
|
index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0
|
|
return index >= int(n.group(1))
|
|
return False
|
|
|
|
return truth
|
|
|
|
|
|
class TestExtraction(unittest.TestCase):
|
|
def setUp(self):
|
|
self.record = {"id": "1", "name": "alice", "secret": "s3cr3t-alice", "role": "admin"}
|
|
self.truth = _recordOracle(self.record)
|
|
|
|
def test_brute_entities(self):
|
|
self.assertEqual(hql._bruteEntities(self.truth), ["Member"])
|
|
|
|
def test_enum_fields(self):
|
|
fields = hql._enumFields(self.truth, "Member")
|
|
for expected in ("id", "name", "secret", "role"):
|
|
self.assertIn(expected, fields)
|
|
|
|
def test_infer_string_value(self):
|
|
self.assertEqual(hql._inferValue(self.truth, "Member", "name", "id"), "alice")
|
|
|
|
def test_infer_secret_with_symbols(self):
|
|
self.assertEqual(hql._inferValue(self.truth, "Member", "secret", "id"), "s3cr3t-alice")
|
|
|
|
def test_infer_numeric_via_cast(self):
|
|
self.assertEqual(hql._inferValue(self.truth, "Member", "id", "id"), "1")
|
|
|
|
def test_infer_absent_attribute_empty(self):
|
|
self.assertEqual(hql._inferValue(self.truth, "Member", "nope", "id"), "")
|
|
|
|
|
|
def _multiOracle(records):
|
|
"""Row-aware oracle: honors the "_h2.<pin> > <after>" walk bound by selecting the
|
|
smallest-id record strictly greater than `after`."""
|
|
|
|
def pick(predicate):
|
|
m = re.search(r"_h2\.\w+>(\d+)", predicate)
|
|
after = int(m.group(1)) if m else None
|
|
cands = [r for r in records if after is None or int(r["id"]) > after]
|
|
return min(cands, key=lambda r: int(r["id"])) if cands else None
|
|
|
|
def truth(predicate):
|
|
if re.search(r"EXISTS\(SELECT 1 FROM \w+ _h\)", predicate):
|
|
return True
|
|
m = re.search(r"EXISTS\(SELECT _h\.(\w+) FROM", predicate)
|
|
if m:
|
|
return m.group(1) in records[0]
|
|
rec = pick(predicate)
|
|
if rec is None:
|
|
return False
|
|
if "LENGTH" in predicate:
|
|
m = re.search(r"CAST\(_h\.(\w+) AS string\)", predicate)
|
|
n = re.search(r">=(\d+)", predicate)
|
|
if m and n:
|
|
return len(str(rec.get(m.group(1), ""))) >= int(n.group(1))
|
|
if "LOCATE" in predicate:
|
|
m = re.search(r"SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'", predicate)
|
|
n = re.search(r">=(\d+)\s*$", predicate)
|
|
if m and n:
|
|
value = str(rec.get(m.group(1), ""))
|
|
pos, literal = int(m.group(2)), m.group(3)
|
|
index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0
|
|
return index >= int(n.group(1))
|
|
return False
|
|
|
|
return truth
|
|
|
|
|
|
class TestMultiRow(unittest.TestCase):
|
|
def setUp(self):
|
|
self.records = [
|
|
{"id": "1", "name": "alice", "role": "admin"},
|
|
{"id": "2", "name": "bob", "role": "user"},
|
|
{"id": "5", "name": "carol", "role": "staff"},
|
|
]
|
|
self.truth = _multiOracle(self.records)
|
|
|
|
def _walk(self, fields, pin):
|
|
rows, after = [], None
|
|
for _ in range(20):
|
|
pinValue = hql._inferValue(self.truth, "Member", pin, pin, after)
|
|
if not pinValue:
|
|
break
|
|
record = {pin: pinValue}
|
|
for field in fields:
|
|
if field != pin:
|
|
record[field] = hql._inferValue(self.truth, "Member", field, pin, after)
|
|
rows.append(record)
|
|
if not re.match(r"\A\d+\Z", pinValue):
|
|
break
|
|
after = pinValue
|
|
return rows
|
|
|
|
def test_walks_all_rows_in_order(self):
|
|
rows = self._walk(["id", "name", "role"], "id")
|
|
self.assertEqual([r["name"] for r in rows], ["alice", "bob", "carol"])
|
|
self.assertEqual([r["id"] for r in rows], ["1", "2", "5"])
|
|
self.assertEqual(rows[2]["role"], "staff")
|
|
|
|
|
|
class TestGrid(unittest.TestCase):
|
|
def test_grid(self):
|
|
out = hql._grid(["id", "name"], [["1", "alice"]])
|
|
self.assertIn("alice", out)
|
|
self.assertIn("+--", out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|