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
3 changes: 3 additions & 0 deletions doc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Added the switch `--ssti`. It tests for server-side template injection. It also covers Struts2 and OGNL.
* Added the switch `--graphql`. It tests for GraphQL injection.
* Added the switch `--hql`. It tests for HQL and JPQL (Hibernate ORM) injection.
* Added the switch `--xslt`. It tests for XSLT injection. The engine names itself in the response. sqlmap then dumps the XML document that the stylesheet transforms. It also reads the files that the engine can reach.
* Added the switch `--xxe`. It tests for XML External Entity injection. It uses in-band, error-based, and out-of-band channels.
* Added the switch `--jwt`. It examines JSON Web Tokens for weak keys and for injection in the claims.

Expand Down Expand Up @@ -45,6 +46,7 @@
* Added support for Google Cloud Spanner.
* Added support for DuckDB and Trino as forks.
* Added the switch `--esperanto`. It enumerates a back-end DBMS that sqlmap cannot identify.
* Added XQuery support to the switch `--xpath`. XQuery is a superset of XPath, so the same injection point gives more. On a back-end that supports it, sqlmap reads a file through `unparsed-text()`.
* Added error-based payloads for CUBRID, InterSystems Cache, Virtuoso, H2, Firebird, and Vertica.
* Added time-based payloads for CUBRID.
* Added out-of-band DNS channels for H2 and ClickHouse.
Expand Down Expand Up @@ -81,6 +83,7 @@
* sqlmap now keeps the value of an injected Host header.
* Removed the time outliers from the time statistics.
* Improved the detection of the SQL dialect of the target.
* The switch `--xpath` no longer reports an injection when the page changes only because it shows the payload again. Reflection is not proof that an expression was evaluated.

## Quality

Expand Down
50 changes: 50 additions & 0 deletions extra/vulnserver/vulnserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,23 @@ def hql_evaluate(value):

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

XSLT_DOC = """<?xml version="1.0"?><catalog><item><name>luther</name><price>10</price></item>\
<item><name>fluffy</name><price>20</price></item></catalog>"""

# The element slot: user input lands BETWEEN elements, so it can introduce whole XSLT instructions.
XSLT_ELEMENT_SHEET = """<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/><xsl:template match="/"><html><body><div>%s</div></body></html></xsl:template>
</xsl:stylesheet>"""

# The value slot: user input lands INSIDE select="...", so it can only carry an XPath expression.
XSLT_VALUE_SHEET = """<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/><xsl:template match="/"><html><body><table>
<xsl:for-each select="catalog/item"><xsl:sort select="%s"/><tr><td><xsl:value-of select="name"/></td></tr></xsl:for-each>
</table></body></html></xsl:template>
</xsl:stylesheet>"""

XPATH_XML = """<?xml version="1.0" encoding="UTF-8"?>
<directory>
<department name="IT Operations">
Expand Down Expand Up @@ -1177,6 +1194,39 @@ def do_REQUEST(self):
self.wfile.write(output.encode(UNICODE_ENCODING))
return

if self.url == "/echo":
# A pure reflector: no engine of any kind behind it, it only shows the parameter back. Every
# non-SQL switch must stay silent here. A differential built on "the page changed" is
# satisfied by reflection alone, which is how several engines reported this shape as
# injectable - so this endpoint is the regression gate for that whole class.
self.send_response(OK)
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(("<html><body>you searched for: %s</body></html>" % self.params.get("q", "")).encode(UNICODE_ENCODING))
return

if self.url in ("/xslt/element", "/xslt/value"):
# VULNERABLE: user input is concatenated into a stylesheet which is then compiled and applied
element = self.url.endswith("element")
source = self.params.get("tpl" if element else "sort", "" if element else "name")
try:
from lxml import etree
sheet = (XSLT_ELEMENT_SHEET if element else XSLT_VALUE_SHEET) % source
transform = etree.XSLT(etree.fromstring(sheet.encode("utf-8")))
output = str(transform(etree.fromstring(XSLT_DOC.encode("utf-8"))))
code = OK
except Exception as ex:
output = "<html><body><h1>XSLT error</h1><pre>%s: %s</pre></body></html>" % (type(ex).__name__, ex)
code = INTERNAL_SERVER_ERROR

self.send_response(code)
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
self.send_header("Connection", "close")
self.end_headers()
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
11 changes: 9 additions & 2 deletions lib/controller/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
from lib.core.settings import LDAP_ERROR_REGEX
from lib.core.settings import SSTI_ERROR_REGEX
from lib.core.settings import XPATH_ERROR_REGEX
from lib.core.settings import XSLT_ERROR_REGEX
from lib.core.settings import XXE_ERROR_REGEX
from lib.core.settings import IPS_WAF_CHECK_PAYLOAD
from lib.core.settings import IPS_WAF_CHECK_RATIO
Expand Down Expand Up @@ -1188,13 +1189,13 @@ def _(page):
kb.ignoreCasted = readInput(message, default='Y' if conf.multipleTargets else 'N', boolean=True)

elif result:
infoMsg += "be injectable"
infoMsg += "be SQL injectable"
if Backend.getErrorParsedDBMSes():
infoMsg += " (possible DBMS: '%s')" % Format.getErrorParsedDBMSes()
logger.info(infoMsg)

else:
infoMsg += "not be injectable"
infoMsg += "not be SQL injectable"
logger.warning(infoMsg)

kb.heuristicMode = True
Expand Down Expand Up @@ -1271,6 +1272,12 @@ def _(page):
if conf.beep:
beep()

if not conf.xslt and re.search(XSLT_ERROR_REGEX, page or ""):
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)
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
11 changes: 8 additions & 3 deletions lib/controller/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,12 +532,12 @@ def start():

checkJWT()

if conf.mineParams and not any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.hql, conf.jwt)):
if conf.mineParams and not any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.xslt, conf.hql, conf.jwt)):
from lib.utils.paraminer import mineParameters
mineParameters()

if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.hql, conf.jwt)) 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/--jwt) findings; these are reported on the console only")
if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.xslt, conf.hql, conf.jwt)) and (conf.reportJson or conf.resultsFile):
singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti/--xslt/--xxe/--hql/--jwt) findings; these are reported on the console only")

if conf.graphql:
from lib.techniques.graphql.inject import graphqlScan
Expand All @@ -564,6 +564,11 @@ def start():
sstiScan()
continue

if conf.xslt:
from lib.techniques.xslt.inject import xsltScan
xsltScan()
continue

if conf.xxe:
from lib.techniques.xxe.inject import xxeScan
xxeScan()
Expand Down
12 changes: 7 additions & 5 deletions lib/core/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,10 +945,12 @@ def _setTamperingFunctions():
logger.warning(warnMsg)

# tamper scripts rewrite SQL injection payloads; the self-contained non-SQL engines
# (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe) do not run payloads through the tampering hook, so
# warn instead of silently ignoring the user's '--tamper'
if kb.tamperFunctions and any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe)):
engine = next(_ for _ in ("graphql", "nosql", "ldap", "xpath", "ssti", "xxe") if conf.get(_))
# (--graphql/--nosql/--ldap/--xpath/--ssti/--xslt/--xxe) do not run payloads through the tampering
# hook, so warn instead of silently ignoring the user's '--tamper'. One tuple drives both the test
# and the name lookup - keeping two lists in step is exactly how this raised StopIteration.
_nonSqlEngines = ("graphql", "nosql", "ldap", "xpath", "ssti", "xslt", "xxe")
if kb.tamperFunctions and any(conf.get(_) for _ in _nonSqlEngines):
engine = next(_ for _ in _nonSqlEngines if conf.get(_))
warnMsg = "tamper scripts are applied to SQL injection payloads only and "
warnMsg += "will be ignored by the '--%s' engine" % engine
logger.warning(warnMsg)
Expand Down Expand Up @@ -2755,7 +2757,7 @@ def _checkTor():
def _basicOptionValidation():
_nonSqlTechniques = [name for name, enabled in (
("--graphql", conf.graphql), ("--nosql", conf.nosql), ("--ldap", conf.ldap),
("--xpath", conf.xpath), ("--ssti", conf.ssti), ("--xxe", conf.xxe), ("--hql", conf.hql)) if enabled]
("--xpath", conf.xpath), ("--ssti", conf.ssti), ("--xxe", conf.xxe), ("--xslt", conf.xslt), ("--hql", conf.hql)) if enabled]
if len(_nonSqlTechniques) > 1:
errMsg = "only one non-SQL technique switch may be used at a time (found: %s). " % ", ".join(_nonSqlTechniques)
errMsg += "each is a self-contained scan for a different back-end class - pick one"
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",
"xslt": "boolean",
"hql": "boolean",
"jwt": "boolean",
"oobServer": "string",
Expand Down
90 changes: 88 additions & 2 deletions lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from thirdparty import six

# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.8.12"
VERSION = "1.10.8.16"
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)
Expand Down Expand Up @@ -1168,7 +1168,9 @@
("eXist", r"org\.exist\.xquery\.(?:XPathException|XQueryException)"),
("eXist", r"exerr:ERROR"),
("Python ElementTree", r"xml\.etree\.ElementTree\.(?:ParseError|Element)"),
("Generic XPath", r"(?:XPath|XSLT).*?(?:error|exception|syntax)"),
# NOT XSLT: a dedicated '--xslt' engine owns those errors now, and claiming them here made every
# XSLT parser error suggest '--xpath' as well
("Generic XPath", r"XPath.*?(?:error|exception|syntax)"),
("Generic XPath", r"Invalid XPath|XPath evaluation failed"),
)

Expand All @@ -1184,6 +1186,90 @@
# Upper bound for the value-length search during XPath blind extraction
XPATH_MAX_LENGTH = 256

# XQuery (XPath 2.0/3.x) supersets XPath 1.0, so the same injection boundary reaches a much richer
# language. These probes are TRUE on an XQuery processor and a SYNTAX ERROR on an XPath 1.0 one, which is
# what makes them a capability test rather than a guess: string-join/matches/upper-case simply do not
# exist in 1.0. Engines: Saxon, BaseX, eXist-db, MarkLogic, Zorba.
XQUERY_CAPABILITY_PROBES = (
"string-join(('a','b'),'')='ab'",
"upper-case('a')='A'",
"matches('a','a')",
)

# XQuery file-read primitive: fn:unparsed-text() returns a text file as a string, so the existing blind
# character bisection recovers it unchanged. doc() is the XML equivalent (and the OOB vector when it is
# handed an http:// URI).
XQUERY_FILE_READ = "unparsed-text(%s)"
XQUERY_MAX_FILE_LENGTH = 4096

# Proactive harvest for a confirmed XQuery back-end. Deliberately SHORT: unlike an in-band read, every
# character here costs a bisection round-trip, so this is the identity/secret minimum rather than the
# broad sweep an in-band engine can afford.
XQUERY_FILE_HARVEST = (
"/etc/passwd",
"/etc/hostname",
"/proc/self/environ",
"/proc/self/cmdline",
"c:/windows/win.ini",
)
# Characters recovered from the ONE file the harvest extracts a sample from. Every character costs about
# eight bisection round-trips, so a full /etc/passwd would be thousands of requests against the target.
# The harvest therefore PROVES readability across the list for ~1 request each and samples a short prefix
# from the first hit only. An explicit '--file-read' is a deliberate request and still gets the full
# XQUERY_MAX_FILE_LENGTH.
XQUERY_HARVEST_CHARS = 32

# XSLT injection ('--xslt'). Compile/runtime errors are per-engine and are what reaches a target whose
# output is fixed, so they double as the fingerprint when nothing can be reflected.
# Ordered MOST SPECIFIC FIRST and matched in order: PHP's XSLTProcessor and lxml are both libxslt
# underneath and emit its wording too, so the generic libxslt entry has to come last or it would shadow
# the binding that actually tells the tester what they are talking to.
XSLT_ERROR_SIGNATURES = (
("PHP XSLTProcessor", r"XSLTProcessor::(?:importStylesheet|transformTo\w+)\(\)"),
("libxslt / lxml", r"lxml\.etree\.(?:XSLT(?:Parse|Apply|)Error|XPathEvalError)"),
("Saxon", r"(?:net\.sf\.saxon\.|SaxonApiException|Static error(?:s)? (?:in|at)|XTDE\d{4}|XTSE\d{4})"),
("Xalan / Java JAXP", r"(?:javax\.xml\.transform\.Transformer(?:Configuration)?Exception|org\.apache\.xalan|XSLT Error)"),
(".NET XslCompiledTransform", r"System\.Xml\.Xsl\.(?:XslLoadException|XsltException)"),
# Anchored to XSLT vocabulary on purpose: this regex also drives the GLOBAL heuristic hint in
# checks.py, and bare "compilation error" / "Invalid expression" match gcc, javac and regex failures,
# which would suggest '--xslt' on targets that have nothing to do with XSLT.
("libxslt", r"(?:xsltParseStylesheet|xsltApplyStylesheet|xsltCompilePattern|xsl:\w+ : |xmlXPathEval|XPath error : )"),
("Generic XSLT", r"(?:XSLT|xsl:stylesheet).{0,40}?(?:error|exception|fail)"),
)

XSLT_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in XSLT_ERROR_SIGNATURES)

# system-property() names the processor from inside the transformation, so a response carrying it is the
# engine speaking rather than the application echoing.
XSLT_VENDOR_PROPERTIES = ("xsl:vendor", "xsl:version", "xsl:vendor-url", "xsl:product-name", "xsl:product-version")

# Extension surfaces that would turn XSLT injection into code execution or a file WRITE. sqlmap reports
# their availability and never invokes them - probing whether a function EXISTS is not exercising it.
XSLT_RCE_PROBES = (
("PHP registerPHPFunctions (php:function)", "string(function-available('php:function'))"),
("EXSLT exsl:document (file write)", "string(element-available('exsl:document'))"),
("Saxon saxon:eval", "string(function-available('saxon:eval'))"),
("Xalan java: extension namespace", "string(function-available('java:java.lang.Runtime.getRuntime'))"),
)

XSLT_MAX_FILE_LENGTH = 65536

# XSLT 1.0's document() parses its target as XML, so a text file simply fails to load - only an XSLT 2.0+
# engine reaches arbitrary text through unparsed-text(). These are the high-value paths that ARE XML, so
# the auto-harvest still returns something on a 1.0 engine (which is most of the installed base).
XSLT_XML_HARVEST = (
"/var/www/html/WEB-INF/web.xml",
"/usr/local/tomcat/conf/tomcat-users.xml",
"/usr/local/tomcat/conf/server.xml",
"/opt/tomcat/conf/tomcat-users.xml",
"/etc/tomcat/tomcat-users.xml",
"c:/inetpub/wwwroot/web.config",
"c:/windows/system32/inetsrv/config/applicationHost.config",
)

# Bound on the proactive harvest so a confirmed finding cannot turn into hundreds of requests.
XSLT_MAX_HARVEST = 12

# SSTI error signatures per template engine for detection and fingerprinting.
# Each tuple is (engine_name, regex_fragment).
SSTI_ERROR_SIGNATURES = (
Expand Down
Loading