mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
feat(dynamic): implement entry-driven verification with fallback to synthetic direct-sink, enhance per-language emitters, and improve test coverage
This commit is contained in:
parent
130bf904eb
commit
738f1fedbc
9 changed files with 686 additions and 116 deletions
|
|
@ -759,8 +759,98 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
/// boundary fires for both vuln and benign payloads; downstream
|
||||
/// instantiation failures (e.g. `serialVersionUID` mismatch on the
|
||||
/// allow-listed payload) are caught and treated as non-probe paths.
|
||||
pub fn emit_deserialize_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
pub fn emit_deserialize_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
|
||||
// Tier-(a) main: drive the fixture's enclosing entry with the forged
|
||||
// blob so a caller-side mitigation (a `resolveClass` allowlist /
|
||||
// restricted ObjectInputStream subclass) runs before the gadget class
|
||||
// is resolved. Detection is by exception type: a vanilla
|
||||
// ObjectInputStream reaches `resolveClass(gadget)` and raises
|
||||
// ClassNotFoundException (the gadget is not on the classpath) — that is
|
||||
// unrestricted deserialization, so a probe fires. A guarded fixture
|
||||
// raises InvalidClassException at its allowlist check *before* the
|
||||
// class resolves, so no probe is written. Falls back to the tier-(b)
|
||||
// synthetic restricted-OIS path when reflection setup fails.
|
||||
let main_body = if spec.entry_is_derivable() {
|
||||
let class_name = java_entry_class_name(spec);
|
||||
let method_name = &spec.entry_name;
|
||||
format!(
|
||||
r#" public static void main(String[] args) {{
|
||||
String payload = System.getenv("NYX_PAYLOAD");
|
||||
if (payload == null) payload = "";
|
||||
String prefix = "NYX_GADGET_CLASS:";
|
||||
boolean drove = false;
|
||||
if (payload.startsWith(prefix)) {{
|
||||
String cls = payload.substring(prefix.length());
|
||||
// Tier-(a): drive `{class_name}.{method_name}(byte[])` so the
|
||||
// fixture's own (un)restricted deserialization path runs.
|
||||
try {{
|
||||
byte[] blob = nyxForgeClassDescriptor(cls);
|
||||
Class<?> entryCls = Class.forName("{class_name}");
|
||||
java.lang.reflect.Method m = entryCls.getMethod("{method_name}", byte[].class);
|
||||
drove = true;
|
||||
try {{
|
||||
m.invoke(null, (Object) blob);
|
||||
}} catch (java.lang.reflect.InvocationTargetException ite) {{
|
||||
if (nyxCauseChainHas(ite.getCause(), ClassNotFoundException.class)) {{
|
||||
// The fixture's deserializer reached and tried to
|
||||
// resolve the gadget class (unrestricted path).
|
||||
nyxDeserializeProbe(true);
|
||||
}}
|
||||
// InvalidClassException (a caller-side allowlist block)
|
||||
// lands here too but is not a ClassNotFoundException, so
|
||||
// a guarded fixture writes no probe.
|
||||
}} catch (Throwable t) {{
|
||||
// Other reflective-call failure — non-probe path.
|
||||
}}
|
||||
}} catch (Throwable setup) {{
|
||||
// Reflection setup failed (class / method missing) — fall
|
||||
// through to the tier-(b) synthetic path below.
|
||||
drove = false;
|
||||
}}
|
||||
}}
|
||||
if (!drove) {{
|
||||
// Tier-(b): the enclosing entry could not be driven — synthetic
|
||||
// restricted-OIS direct path (recorded as direct-sink fallback).
|
||||
nyxSyntheticDeserialize(payload);
|
||||
}}
|
||||
// Sink-reachability sentinel — runner's `vuln_fired && sink_hit`
|
||||
// gate consumes this; without it differential confirmation cannot
|
||||
// fire even when the probe was written.
|
||||
System.out.println("__NYX_SINK_HIT__");
|
||||
}}
|
||||
|
||||
/// True when `t` or any exception in its cause chain is an instance of
|
||||
/// `want` — used to detect the gadget-class resolution attempt that a
|
||||
/// vanilla ObjectInputStream surfaces as ClassNotFoundException.
|
||||
static boolean nyxCauseChainHas(Throwable t, Class<?> want) {{
|
||||
int hops = 0;
|
||||
while (t != null && hops < 32) {{
|
||||
if (want.isInstance(t)) return true;
|
||||
t = t.getCause();
|
||||
hops++;
|
||||
}}
|
||||
return false;
|
||||
}}
|
||||
"#
|
||||
)
|
||||
} else {
|
||||
// No derivable enclosing entry — drive the synthetic restricted-OIS
|
||||
// path directly.
|
||||
r#" public static void main(String[] args) {
|
||||
String payload = System.getenv("NYX_PAYLOAD");
|
||||
if (payload == null) payload = "";
|
||||
nyxSyntheticDeserialize(payload);
|
||||
// Sink-reachability sentinel — runner's `vuln_fired && sink_hit`
|
||||
// gate consumes this; without it differential confirmation cannot
|
||||
// fire even when the probe was written.
|
||||
System.out.println("__NYX_SINK_HIT__");
|
||||
}
|
||||
"#
|
||||
.to_owned()
|
||||
};
|
||||
|
||||
let source = format!(
|
||||
r#"// Nyx dynamic harness — deserialize (Phase 03 / Track J.1).
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
|
@ -835,36 +925,33 @@ public class NyxHarness {{
|
|||
return baos.toByteArray();
|
||||
}}
|
||||
|
||||
public static void main(String[] args) {{
|
||||
String payload = System.getenv("NYX_PAYLOAD");
|
||||
if (payload == null) payload = "";
|
||||
/// Tier-(b) synthetic direct-sink: run the forged blob through a
|
||||
/// restricted ObjectInputStream the harness controls. Bypasses any
|
||||
/// caller-side guard, so it is used only when the fixture's own entry
|
||||
/// could not be driven.
|
||||
static void nyxSyntheticDeserialize(String payload) {{
|
||||
String prefix = "NYX_GADGET_CLASS:";
|
||||
if (payload.startsWith(prefix)) {{
|
||||
String cls = payload.substring(prefix.length());
|
||||
if (!payload.startsWith(prefix)) return;
|
||||
String cls = payload.substring(prefix.length());
|
||||
try {{
|
||||
byte[] blob = nyxForgeClassDescriptor(cls);
|
||||
NyxRestrictedOIS ois = new NyxRestrictedOIS(
|
||||
new ByteArrayInputStream(blob));
|
||||
try {{
|
||||
byte[] blob = nyxForgeClassDescriptor(cls);
|
||||
NyxRestrictedOIS ois = new NyxRestrictedOIS(
|
||||
new ByteArrayInputStream(blob));
|
||||
try {{
|
||||
ois.readObject();
|
||||
}} finally {{
|
||||
try {{ ois.close(); }} catch (IOException ignored) {{}}
|
||||
}}
|
||||
}} catch (InvalidClassException e) {{
|
||||
// Restricted block — probe already written above.
|
||||
}} catch (Throwable t) {{
|
||||
// Allow-listed but downstream instantiation fails (the
|
||||
// minimal stream omits the field bytes the real class
|
||||
// expects). resolveClass already fired; treat as a
|
||||
// non-probe path.
|
||||
ois.readObject();
|
||||
}} finally {{
|
||||
try {{ ois.close(); }} catch (IOException ignored) {{}}
|
||||
}}
|
||||
}} catch (InvalidClassException e) {{
|
||||
// Restricted block — probe already written above.
|
||||
}} catch (Throwable t) {{
|
||||
// Allow-listed but downstream instantiation fails (the minimal
|
||||
// stream omits the field bytes the real class expects).
|
||||
// resolveClass already fired; treat as a non-probe path.
|
||||
}}
|
||||
// Sink-reachability sentinel — runner's `vuln_fired && sink_hit`
|
||||
// gate consumes this; without it differential confirmation cannot
|
||||
// fire even when the probe was written.
|
||||
System.out.println("__NYX_SINK_HIT__");
|
||||
}}
|
||||
}}
|
||||
|
||||
{main_body}}}
|
||||
"#
|
||||
);
|
||||
HarnessSource {
|
||||
|
|
@ -881,6 +968,18 @@ public class NyxHarness {{
|
|||
}
|
||||
}
|
||||
|
||||
/// Derive the Java class that declares the entry method from the spec's
|
||||
/// `entry_file` basename (Java's public-class-per-file convention: a sink
|
||||
/// in `Vuln.java` lives in `public class Vuln`). Used by the
|
||||
/// deserialize harness to reflectively load the fixture class.
|
||||
fn java_entry_class_name(spec: &HarnessSpec) -> String {
|
||||
std::path::Path::new(&spec.entry_file)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_else(|| "NyxEntry".to_owned())
|
||||
}
|
||||
|
||||
/// Phase 04 — Track J.2 SSTI harness for Java (Thymeleaf).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, simulates Thymeleaf's `[[${expr}]]` inlined-
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue