[pitboss] phase 03: Track J.1 + Track L.1 — DESERIALIZE corpus + Java/Python/PHP/Ruby adapters

This commit is contained in:
pitboss 2026-05-17 16:37:20 -05:00
parent 01fcaab310
commit 9dc60b51c0
33 changed files with 1625 additions and 53 deletions

View file

@ -0,0 +1,39 @@
// Phase 03 (Track J.1) Java deserialize benign fixture.
//
// Same shape as the vuln fixture but wraps `ObjectInputStream` in a
// subclass whose `resolveClass` only accepts a tiny allowlist. A
// gadget chain never resolves so no Deserialize probe fires.
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class Benign {
static final Set<String> ALLOWED =
new HashSet<>(Arrays.asList("java.lang.Integer", "java.lang.String"));
static class RestrictedObjectInputStream extends ObjectInputStream {
RestrictedObjectInputStream(ByteArrayInputStream s) throws IOException {
super(s);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
if (!ALLOWED.contains(desc.getName())) {
throw new InvalidClassException("blocked: " + desc.getName());
}
return super.resolveClass(desc);
}
}
public static Object run(byte[] payload) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(payload);
try (RestrictedObjectInputStream ois = new RestrictedObjectInputStream(bis)) {
return ois.readObject();
}
}
}

View file

@ -0,0 +1,16 @@
// Phase 03 (Track J.1) Java deserialize vuln fixture.
//
// The function reads bytes off the wire and hands them straight to
// `ObjectInputStream.readObject` without restricting `resolveClass`.
// A gadget chain inside the byte stream is materialised before any
// allowlist check fires, so a CVE-class object-injection is reachable.
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
public class Vuln {
public static Object run(byte[] payload) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(payload);
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}

View file

@ -0,0 +1,8 @@
<?php
// Phase 03 (Track J.1) — PHP deserialize benign fixture.
//
// Passes `allowed_classes => false` so every object becomes a
// `__PHP_Incomplete_Class` instead of materialising the gadget.
function run(string $blob) {
return unserialize($blob, ['allowed_classes' => false]);
}

View file

@ -0,0 +1,9 @@
<?php
// Phase 03 (Track J.1) — PHP deserialize vuln fixture.
//
// `unserialize` without `allowed_classes` will materialise any
// `O:N:"ClassName":` blob the attacker sends, triggering `__wakeup`
// / `__destruct` chains.
function run(string $blob) {
return unserialize($blob);
}

View file

@ -0,0 +1,22 @@
"""Phase 03 (Track J.1) — Python deserialize benign fixture.
Wraps `pickle.Unpickler` with a `find_class` override that hard-codes
a tiny allowlist. A gadget chain in the payload trips
`UnpicklingError` before any code runs, so no Deserialize probe
fires.
"""
import io
import pickle
ALLOWED = {("builtins", "list"), ("builtins", "dict"), ("builtins", "int")}
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module: str, name: str):
if (module, name) not in ALLOWED:
raise pickle.UnpicklingError(f"blocked: {module}.{name}")
return super().find_class(module, name)
def run(blob: bytes):
return RestrictedUnpickler(io.BytesIO(blob)).load()

View file

@ -0,0 +1,11 @@
"""Phase 03 (Track J.1) — Python deserialize vuln fixture.
`pickle.loads` accepts arbitrary classes; a gadget chain inside the
payload runs straight through `__reduce__` without bumping into any
allowlist.
"""
import pickle
def run(blob: bytes):
return pickle.loads(blob)

View file

@ -0,0 +1,15 @@
# Phase 03 (Track J.1) — Ruby deserialize benign fixture.
#
# Inspects the marshalled stream's const name before handing it to
# `Marshal.load`; anything outside the tiny allowlist raises before
# any gadget code runs.
ALLOWED = %w[Integer String Array].freeze
def run(blob)
# Quick const-name sniff — `Marshal` writes the class name as a
# length-prefixed string after the `o` tag.
if blob.bytes.any? && !ALLOWED.any? { |c| blob.include?(c) }
raise ArgumentError, "blocked: non-allowlisted gadget class"
end
Marshal.load(blob)
end

View file

@ -0,0 +1,8 @@
# Phase 03 (Track J.1) — Ruby deserialize vuln fixture.
#
# `Marshal.load` materialises arbitrary constants; a CVE-class gadget
# in the payload runs through `_load` / `_load_data` without any
# allowlist check.
def run(blob)
Marshal.load(blob)
end