mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0027 (20260522T043516Z-29b8)
This commit is contained in:
parent
ed237ab45a
commit
cfb240281c
4 changed files with 669 additions and 1 deletions
|
|
@ -60,4 +60,63 @@ pub const PAYLOADS: &[CuratedPayload] = &[
|
|||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
// Phase 08 tier-(b): raw-socket wire-frame smuggling payload.
|
||||
// Same CRLF-bearing bytes as the servlet payload above, but pinned
|
||||
// to the `java_raw` fixture (a `java.net.ServerSocket` driven by
|
||||
// `createServer` + `runOnce` that writes raw bytes via
|
||||
// `OutputStream.write(byte[])`). The wire frame captured off the
|
||||
// response socket carries two distinct `Set-Cookie:` lines, so
|
||||
// `HeaderSmuggledInWire { primary: "Set-Cookie", smuggled:
|
||||
// "Set-Cookie" }` fires — proving the smuggled header survived
|
||||
// to the actual wire instead of being CRLF-stripped en route by
|
||||
// Tomcat / Jetty / Undertow.
|
||||
//
|
||||
// Distinct payload (not just an extra predicate on the servlet
|
||||
// row) because every modern Java servlet container response
|
||||
// serializer strips CRLF at the wire-write boundary, so the
|
||||
// wire-frame predicate would never fire against the canonical
|
||||
// servlet fixture.
|
||||
CuratedPayload {
|
||||
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
|
||||
label: "header-injection-java-raw-wire-smuggle",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::HeaderSmuggledInWire {
|
||||
primary: "Set-Cookie",
|
||||
smuggled: "Set-Cookie",
|
||||
}],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 12,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/header_injection/java_raw/Vuln.java"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::HeaderSmuggledInWire {
|
||||
primary: "Set-Cookie",
|
||||
smuggled: "Set-Cookie",
|
||||
}],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "header-injection-java-raw-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
|
||||
label: "header-injection-java-raw-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::HeaderSmuggledInWire {
|
||||
primary: "Set-Cookie",
|
||||
smuggled: "Set-Cookie",
|
||||
}],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 12,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/header_injection/java_raw/Vuln.java"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1474,9 +1474,12 @@ public class NyxHarness {{
|
|||
/// `ProbeKind::HeaderEmit` probe. Mirrors the synthetic-harness
|
||||
/// pattern used by Phase 03 / 04 / 05 / 06 / 07.
|
||||
pub fn emit_header_injection_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
if entry_source_uses_raw_socket(&entry_source) {
|
||||
return emit_header_injection_wire_frame_harness(spec, &entry_source);
|
||||
}
|
||||
let shim = probe_shim();
|
||||
let extra_files = servlet_stubs_for_entry(&spec.entry_file);
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let servlet_pkg = if entry_source.contains("jakarta.servlet") {
|
||||
"jakarta.servlet.http"
|
||||
} else {
|
||||
|
|
@ -1612,6 +1615,311 @@ public class NyxHarness {{
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 08 tier-(b) gate: route to the wire-frame harness when the
|
||||
/// entry file exposes the raw-socket fixture API (`createServer` +
|
||||
/// `runOnce` + `setCookieValue`) driven by `java.net.ServerSocket`.
|
||||
/// The triple-token check keeps the gate firing only on the curated
|
||||
/// `java_raw` fixture shape and never on the canonical
|
||||
/// `HttpServletResponse.setHeader` fixture above.
|
||||
fn entry_source_uses_raw_socket(src: &str) -> bool {
|
||||
src.contains("java.net.ServerSocket") && src.contains("setCookieValue")
|
||||
}
|
||||
|
||||
/// Phase 08 — Track J.6 tier-(b) wire-frame harness for Java.
|
||||
/// Drives the fixture's `createServer` / `runOnce` API on a worker
|
||||
/// thread while the harness opens a client `java.net.Socket` against
|
||||
/// the bound port, issues one `GET / HTTP/1.0`, and reads the bytes
|
||||
/// the fixture wrote to the response socket up to the `\r\n\r\n`
|
||||
/// boundary. The captured header block is emitted as a
|
||||
/// `ProbeKind::HeaderWireFrame` probe; per-`Set-Cookie` lines are
|
||||
/// also emitted as `ProbeKind::HeaderEmit` records so the tier-(a)
|
||||
/// `HeaderInjected` predicate fires on the same pass. Prints a
|
||||
/// `wire_frame_len` stdout marker so e2e tests can pin the branch.
|
||||
///
|
||||
/// Reflective dispatch via `Class.forName(entry_fqn)
|
||||
/// .getDeclaredMethod("setCookieValue", byte[].class)` etc. mirrors
|
||||
/// the Phase 06 LDAP Java tier-(b) pattern. Avoids any external
|
||||
/// jar bundling — only `java.net.*` + `java.io.*` (JDK built-ins).
|
||||
fn emit_header_injection_wire_frame_harness(
|
||||
_spec: &HarnessSpec,
|
||||
entry_source: &str,
|
||||
) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let entry_class = derive_entry_class(entry_source);
|
||||
let entry_fqn = derive_entry_qualifier(entry_source, &entry_class);
|
||||
let source = format!(
|
||||
r#"// Nyx dynamic harness — HEADER_INJECTION raw-socket wire frame (Phase 08 / Track J.6).
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class NyxHarness {{
|
||||
{shim}
|
||||
|
||||
static void nyxWireFrameHeaderProbe(String name, String value) {{
|
||||
String p = System.getenv("NYX_PROBE_PATH");
|
||||
if (p == null || p.isEmpty()) return;
|
||||
long now = System.nanoTime();
|
||||
String pid = System.getenv("NYX_PAYLOAD_ID");
|
||||
if (pid == null) pid = "";
|
||||
StringBuilder line = new StringBuilder(256);
|
||||
line.append("{{\"sink_callee\":\"Socket.getOutputStream().write\",\"args\":[");
|
||||
line.append("{{\"kind\":\"String\",\"value\":\"");
|
||||
nyxJsonEscape(name, line);
|
||||
line.append("\"}},{{\"kind\":\"String\",\"value\":\"");
|
||||
nyxJsonEscape(value, line);
|
||||
line.append("\"}}],");
|
||||
line.append("\"captured_at_ns\":").append(now).append(',');
|
||||
line.append("\"payload_id\":\"");
|
||||
nyxJsonEscape(pid, line);
|
||||
line.append("\",\"kind\":{{\"kind\":\"HeaderEmit\",\"name\":\"");
|
||||
nyxJsonEscape(name, line);
|
||||
line.append("\",\"value\":\"");
|
||||
nyxJsonEscape(value, line);
|
||||
line.append("\",\"protocol\":\"wire\"}},");
|
||||
line.append("\"witness\":");
|
||||
line.append(nyxWitnessJson("Socket.getOutputStream().write", new String[]{{name, value}}));
|
||||
line.append("}}\n");
|
||||
try (FileWriter fw = new FileWriter(p, true)) {{
|
||||
fw.write(line.toString());
|
||||
}} catch (IOException e) {{
|
||||
// best-effort
|
||||
}}
|
||||
}}
|
||||
|
||||
static void nyxWireFrameProbe(byte[] rawBytes) {{
|
||||
String p = System.getenv("NYX_PROBE_PATH");
|
||||
if (p == null || p.isEmpty()) return;
|
||||
long now = System.nanoTime();
|
||||
String pid = System.getenv("NYX_PAYLOAD_ID");
|
||||
if (pid == null) pid = "";
|
||||
StringBuilder line = new StringBuilder(256 + rawBytes.length * 4);
|
||||
line.append("{{\"sink_callee\":\"Socket.getOutputStream().write\",\"args\":[],");
|
||||
line.append("\"captured_at_ns\":").append(now).append(',');
|
||||
line.append("\"payload_id\":\"");
|
||||
nyxJsonEscape(pid, line);
|
||||
line.append("\",\"kind\":{{\"kind\":\"HeaderWireFrame\",\"raw_bytes\":[");
|
||||
for (int i = 0; i < rawBytes.length; i++) {{
|
||||
if (i > 0) line.append(',');
|
||||
line.append(((int) rawBytes[i]) & 0xff);
|
||||
}}
|
||||
line.append("]}},");
|
||||
line.append("\"witness\":");
|
||||
line.append(nyxWitnessJson("Socket.getOutputStream().write", new String[0]));
|
||||
line.append("}}\n");
|
||||
try (FileWriter fw = new FileWriter(p, true)) {{
|
||||
fw.write(line.toString());
|
||||
}} catch (IOException e) {{
|
||||
// best-effort
|
||||
}}
|
||||
}}
|
||||
|
||||
// Phase 08 tier-(b): install the cookie value on the fixture,
|
||||
// boot its `ServerSocket` on 127.0.0.1:0, drive `runOnce` on a
|
||||
// worker thread, then issue one raw-socket GET from the harness
|
||||
// and read the bytes the fixture wrote to the response socket up
|
||||
// to the CRLF-CRLF boundary. Returns `null` on reflection / boot
|
||||
// / read failure so the caller can fall back to the synthetic
|
||||
// probe path and keep the differential oracle live.
|
||||
static byte[] nyxWireFrameViaFixture(String payload) {{
|
||||
Class<?> entry;
|
||||
try {{
|
||||
entry = Class.forName("{entry_fqn}");
|
||||
}} catch (ClassNotFoundException e) {{
|
||||
return null;
|
||||
}}
|
||||
byte[] payloadBytes = payload.getBytes(StandardCharsets.ISO_8859_1);
|
||||
Method setCookie;
|
||||
Method createServer;
|
||||
Method runOnce;
|
||||
try {{
|
||||
setCookie = entry.getDeclaredMethod("setCookieValue", byte[].class);
|
||||
setCookie.setAccessible(true);
|
||||
createServer = entry.getDeclaredMethod("createServer");
|
||||
createServer.setAccessible(true);
|
||||
runOnce = entry.getDeclaredMethod("runOnce", ServerSocket.class);
|
||||
runOnce.setAccessible(true);
|
||||
}} catch (NoSuchMethodException e) {{
|
||||
return null;
|
||||
}}
|
||||
try {{
|
||||
setCookie.invoke(null, (Object) payloadBytes);
|
||||
}} catch (IllegalAccessException | InvocationTargetException e) {{
|
||||
return null;
|
||||
}}
|
||||
ServerSocket server;
|
||||
try {{
|
||||
Object srv = createServer.invoke(null);
|
||||
if (!(srv instanceof ServerSocket)) {{
|
||||
return null;
|
||||
}}
|
||||
server = (ServerSocket) srv;
|
||||
}} catch (IllegalAccessException | InvocationTargetException e) {{
|
||||
return null;
|
||||
}}
|
||||
final ServerSocket serverFinal = server;
|
||||
final Method runOnceFinal = runOnce;
|
||||
Thread worker = new Thread(() -> {{
|
||||
try {{
|
||||
runOnceFinal.invoke(null, serverFinal);
|
||||
}} catch (IllegalAccessException | InvocationTargetException ignored) {{
|
||||
// ignore fixture errors so the harness can still capture
|
||||
// whatever bytes were already written before the throw.
|
||||
}}
|
||||
}}, "nyx-wire-frame-worker");
|
||||
worker.setDaemon(true);
|
||||
worker.start();
|
||||
int port = server.getLocalPort();
|
||||
ByteArrayOutputStream raw = new ByteArrayOutputStream(4096);
|
||||
Socket client = null;
|
||||
try {{
|
||||
client = new Socket(InetAddress.getByName("127.0.0.1"), port);
|
||||
client.setSoTimeout(2000);
|
||||
OutputStream out = client.getOutputStream();
|
||||
out.write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"
|
||||
.getBytes(StandardCharsets.ISO_8859_1));
|
||||
out.flush();
|
||||
InputStream in = client.getInputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
long deadline = System.currentTimeMillis() + 5000;
|
||||
while (raw.size() < 65536 && System.currentTimeMillis() < deadline) {{
|
||||
int read;
|
||||
try {{
|
||||
read = in.read(buf, 0, buf.length);
|
||||
}} catch (java.net.SocketTimeoutException te) {{
|
||||
break;
|
||||
}} catch (IOException ioe) {{
|
||||
break;
|
||||
}}
|
||||
if (read < 0) {{
|
||||
break;
|
||||
}}
|
||||
raw.write(buf, 0, read);
|
||||
if (nyxContainsCrlfCrlf(raw.toByteArray())) {{
|
||||
break;
|
||||
}}
|
||||
}}
|
||||
}} catch (IOException ioe) {{
|
||||
// boot / connect / read failed — surface null so the caller
|
||||
// takes the synthetic fallback path.
|
||||
try {{ worker.interrupt(); }} catch (Exception ignored) {{}}
|
||||
try {{ server.close(); }} catch (IOException ignored) {{}}
|
||||
return null;
|
||||
}} finally {{
|
||||
if (client != null) {{
|
||||
try {{ client.close(); }} catch (IOException ignored) {{}}
|
||||
}}
|
||||
try {{ worker.join(2000); }} catch (InterruptedException ignored) {{}}
|
||||
try {{ server.close(); }} catch (IOException ignored) {{}}
|
||||
}}
|
||||
byte[] rawBytes = raw.toByteArray();
|
||||
int sep = nyxIndexCrlfCrlf(rawBytes);
|
||||
if (sep < 0) {{
|
||||
return rawBytes;
|
||||
}}
|
||||
byte[] head = new byte[sep];
|
||||
System.arraycopy(rawBytes, 0, head, 0, sep);
|
||||
return head;
|
||||
}}
|
||||
|
||||
private static boolean nyxContainsCrlfCrlf(byte[] buf) {{
|
||||
return nyxIndexCrlfCrlf(buf) >= 0;
|
||||
}}
|
||||
|
||||
private static int nyxIndexCrlfCrlf(byte[] buf) {{
|
||||
for (int i = 0; i + 3 < buf.length; i++) {{
|
||||
if (buf[i] == 0x0d && buf[i + 1] == 0x0a
|
||||
&& buf[i + 2] == 0x0d && buf[i + 3] == 0x0a) {{
|
||||
return i;
|
||||
}}
|
||||
}}
|
||||
return -1;
|
||||
}}
|
||||
|
||||
// Derive `Set-Cookie:` HeaderEmit records from the raw wire-frame
|
||||
// bytes so the tier-(a) `HeaderInjected` predicate fires on the
|
||||
// same harness pass. The wire-frame branch owns the bytes; the
|
||||
// HeaderEmit records are derived from them.
|
||||
private static void nyxEmitSetCookieHeaderProbes(byte[] rawBytes) {{
|
||||
int start = 0;
|
||||
for (int i = 0; i < rawBytes.length; i++) {{
|
||||
if (rawBytes[i] == 0x0a) {{
|
||||
int end = i;
|
||||
if (end > start && rawBytes[end - 1] == 0x0d) {{
|
||||
end--;
|
||||
}}
|
||||
nyxMaybeEmitSetCookieLine(rawBytes, start, end);
|
||||
start = i + 1;
|
||||
}}
|
||||
}}
|
||||
if (start < rawBytes.length) {{
|
||||
nyxMaybeEmitSetCookieLine(rawBytes, start, rawBytes.length);
|
||||
}}
|
||||
}}
|
||||
|
||||
private static void nyxMaybeEmitSetCookieLine(byte[] rawBytes, int start, int end) {{
|
||||
if (end <= start) return;
|
||||
int colon = -1;
|
||||
for (int i = start; i < end; i++) {{
|
||||
if (rawBytes[i] == 0x3a) {{
|
||||
colon = i;
|
||||
break;
|
||||
}}
|
||||
}}
|
||||
if (colon < 0) return;
|
||||
String name = new String(rawBytes, start, colon - start, StandardCharsets.ISO_8859_1);
|
||||
if (!name.equalsIgnoreCase("Set-Cookie")) return;
|
||||
int valueStart = colon + 1;
|
||||
if (valueStart < end && rawBytes[valueStart] == 0x20) {{
|
||||
valueStart++;
|
||||
}}
|
||||
String value = new String(rawBytes, valueStart, end - valueStart, StandardCharsets.ISO_8859_1);
|
||||
nyxWireFrameHeaderProbe(name, value);
|
||||
}}
|
||||
|
||||
public static void main(String[] args) {{
|
||||
String payload = System.getenv("NYX_PAYLOAD");
|
||||
if (payload == null) payload = "";
|
||||
byte[] rawBytes = nyxWireFrameViaFixture(payload);
|
||||
if (rawBytes != null) {{
|
||||
nyxWireFrameProbe(rawBytes);
|
||||
nyxEmitSetCookieHeaderProbes(rawBytes);
|
||||
System.out.println("__NYX_SINK_HIT__");
|
||||
System.out.println("{{\"wire_frame_len\":" + rawBytes.length + "}}");
|
||||
return;
|
||||
}}
|
||||
// Synthetic fallback when the fixture failed to boot — keeps
|
||||
// the differential oracle live on a build/boot failure rather
|
||||
// than silently shedding the attempt.
|
||||
nyxWireFrameHeaderProbe("Set-Cookie", payload);
|
||||
System.out.println("__NYX_SINK_HIT__");
|
||||
System.out.println("{{\"payload_len\":" + payload.getBytes(StandardCharsets.UTF_8).length + "}}");
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
);
|
||||
HarnessSource {
|
||||
source,
|
||||
filename: "NyxHarness.java".to_owned(),
|
||||
command: vec![
|
||||
"java".to_owned(),
|
||||
"-cp".to_owned(),
|
||||
".".to_owned(),
|
||||
"NyxHarness".to_owned(),
|
||||
],
|
||||
extra_files: Vec::new(),
|
||||
entry_subpath: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 09 — Track J.7 open-redirect harness for Java
|
||||
/// (`HttpServletResponse.sendRedirect`).
|
||||
///
|
||||
|
|
@ -3558,6 +3866,126 @@ mod tests {
|
|||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_header_injection_harness_routes_through_wire_frame_when_raw_socket_imported() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase08_java_test_wire_frame");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = write_servlet_fixture(
|
||||
&dir,
|
||||
"import java.net.ServerSocket;\n\
|
||||
public class Vuln {\n \
|
||||
public static void setCookieValue(byte[] value) {}\n \
|
||||
public static ServerSocket createServer() throws java.io.IOException { return new ServerSocket(0); }\n \
|
||||
public static void runOnce(ServerSocket server) {}\n\
|
||||
}\n",
|
||||
);
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.expected_cap = Cap::HEADER_INJECTION;
|
||||
spec.entry_file = entry;
|
||||
spec.entry_name = "run".into();
|
||||
let h = emit_header_injection_harness(&spec);
|
||||
assert!(
|
||||
h.extra_files.is_empty(),
|
||||
"tier-(b) wire-frame harness must not ship servlet stubs: {:?}",
|
||||
h.extra_files,
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("static byte[] nyxWireFrameViaFixture(String payload)"),
|
||||
"tier-(b) harness must define the wire-frame helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("Class.forName(\"Vuln\")"),
|
||||
"tier-(b) harness must reflectively load the fixture entry class: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("getDeclaredMethod(\"setCookieValue\", byte[].class)"),
|
||||
"tier-(b) harness must install the cookie value via reflection: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("getDeclaredMethod(\"createServer\")"),
|
||||
"tier-(b) harness must boot the fixture's ServerSocket via reflection: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("getDeclaredMethod(\"runOnce\", ServerSocket.class)"),
|
||||
"tier-(b) harness must drive runOnce on a worker thread: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("new Thread(()"),
|
||||
"tier-(b) harness must spawn a worker thread for the accept loop: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("new Socket(InetAddress.getByName(\"127.0.0.1\"), port)"),
|
||||
"tier-(b) harness must open a client Socket against the bound port: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("GET / HTTP/1.0\\r\\nHost: 127.0.0.1"),
|
||||
"tier-(b) harness must issue a raw GET request: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("\\\"kind\\\":\\\"HeaderWireFrame\\\""),
|
||||
"tier-(b) harness must emit a HeaderWireFrame probe kind: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("\\\"raw_bytes\\\":["),
|
||||
"tier-(b) harness must carry the raw_bytes array on the wire-frame probe: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("\"{\\\"wire_frame_len\\\":\" + rawBytes.length"),
|
||||
"tier-(b) harness must emit the wire_frame_len stdout marker: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
!h.source.contains("nyxDrainHeaders()"),
|
||||
"tier-(b) harness must not invoke the servlet-stub drain path: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_header_injection_harness_wire_frame_branch_drops_when_only_servlet_imported() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase08_java_test_no_wire_frame");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = write_servlet_fixture(
|
||||
&dir,
|
||||
"import javax.servlet.http.HttpServletResponse;\n\
|
||||
public class Vuln {\n public static void run(HttpServletResponse r, String v) {\n r.setHeader(\"Set-Cookie\", v);\n }\n}\n",
|
||||
);
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.expected_cap = Cap::HEADER_INJECTION;
|
||||
spec.entry_file = entry;
|
||||
spec.entry_name = "run".into();
|
||||
let h = emit_header_injection_harness(&spec);
|
||||
assert!(
|
||||
!h.source.contains("nyxWireFrameViaFixture"),
|
||||
"servlet-only harness must not define the wire-frame helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
!h.source.contains("HeaderWireFrame"),
|
||||
"servlet-only harness must not emit the HeaderWireFrame probe shape: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
!h.source.contains("wire_frame_len"),
|
||||
"servlet-only harness must not emit the wire_frame_len stdout marker: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_drives_fixture_through_stub_when_servlet_present() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase09_test_drive_fixture");
|
||||
|
|
|
|||
86
tests/dynamic_fixtures/header_injection/java_raw/Vuln.java
Normal file
86
tests/dynamic_fixtures/header_injection/java_raw/Vuln.java
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Phase 08 (Track J.6) — Java raw-socket HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// Writes the response status line and headers directly to the wire via
|
||||
// `OutputStream.write(byte[])` against the `java.net.Socket` returned
|
||||
// by `ServerSocket.accept()`, bypassing the framework-level CRLF
|
||||
// validator that Tomcat / Jetty / Undertow would otherwise interpose
|
||||
// on `HttpServletResponse.setHeader`. A payload carrying
|
||||
// `\r\nSet-Cookie: ...` splits the single Set-Cookie header into two
|
||||
// on the wire, producing the canonical smuggled-second-header shape
|
||||
// that `ProbeKind::HeaderWireFrame` is designed to catch.
|
||||
//
|
||||
// The harness (`src/dynamic/lang/java.rs::emit_header_injection_harness`)
|
||||
// detects the `java.net.ServerSocket` + `setCookieValue` tokens in
|
||||
// this file and routes through the tier-(b) wire-frame branch: bind
|
||||
// a loopback `ServerSocket` via `createServer`, accept one client
|
||||
// (`runOnce`) on a worker thread, issue one raw-socket
|
||||
// `GET / HTTP/1.0` from the harness, read the bytes the fixture
|
||||
// wrote to the response socket up to the CRLF-CRLF boundary, and
|
||||
// emit them as a `ProbeKind::HeaderWireFrame` record.
|
||||
//
|
||||
// All three entry points are `public static` so the harness can
|
||||
// resolve them via `Class.forName("Vuln").getDeclaredMethod(...)`
|
||||
// reflective dispatch (same pattern as Phase 06 LDAP Java tier-(b)).
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
public class Vuln {
|
||||
// Bytes go straight onto the wire with no encoding pass. The
|
||||
// harness installs the cookie value before booting the accept
|
||||
// loop, mirroring the Python `Handler.cookie_value` and Ruby
|
||||
// `set_cookie_value` setters.
|
||||
private static byte[] nyxCookieValue = new byte[0];
|
||||
|
||||
public static void setCookieValue(byte[] value) {
|
||||
nyxCookieValue = (value == null) ? new byte[0] : value;
|
||||
}
|
||||
|
||||
public static ServerSocket createServer() throws IOException {
|
||||
return new ServerSocket(0, 1, java.net.InetAddress.getByName("127.0.0.1"));
|
||||
}
|
||||
|
||||
public static void runOnce(ServerSocket server) {
|
||||
Socket client = null;
|
||||
try {
|
||||
server.setSoTimeout(5000);
|
||||
client = server.accept();
|
||||
client.setSoTimeout(1000);
|
||||
// Drain whatever request bytes the client sent so the
|
||||
// kernel does not stall the write that follows. Ignore
|
||||
// read errors — the client may have already shut its
|
||||
// write side.
|
||||
try {
|
||||
InputStream in = client.getInputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int read = in.read(buf, 0, buf.length);
|
||||
// discard
|
||||
if (read < 0) {
|
||||
// EOF, nothing to drain
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// ignore drain errors
|
||||
}
|
||||
byte[] body = "ok\n".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
java.io.ByteArrayOutputStream raw = new java.io.ByteArrayOutputStream();
|
||||
raw.write("HTTP/1.0 200 OK\r\n".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
|
||||
raw.write(("Content-Length: " + body.length + "\r\n")
|
||||
.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
|
||||
raw.write("Set-Cookie: ".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
|
||||
raw.write(nyxCookieValue);
|
||||
raw.write("\r\n\r\n".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
|
||||
raw.write(body);
|
||||
OutputStream out = client.getOutputStream();
|
||||
out.write(raw.toByteArray());
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
// ignore — harness will time out reading and fall back
|
||||
} finally {
|
||||
if (client != null) {
|
||||
try { client.close(); } catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1101,4 +1101,99 @@ mod e2e_phase_08 {
|
|||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 08 tier-(b): Java raw-socket wire-frame fixture.
|
||||
// `tests/dynamic_fixtures/header_injection/java_raw/Vuln.java`
|
||||
// binds a `java.net.ServerSocket` via `createServer` whose
|
||||
// `runOnce` handler writes raw bytes via
|
||||
// `Socket.getOutputStream().write(byte[])`, bypassing Tomcat /
|
||||
// Jetty / Undertow's CRLF strip on `HttpServletResponse.setHeader`.
|
||||
// The harness boots the server on a loopback port via reflective
|
||||
// dispatch (`Class.forName("Vuln").getDeclaredMethod(...)`), opens
|
||||
// a client `java.net.Socket`, reads the response-header block off
|
||||
// the socket, and emits a `ProbeKind::HeaderWireFrame` record.
|
||||
// Asserts the test exercises the wire-frame branch (not the
|
||||
// synthetic fallback) by pinning `wire_frame_len` in the captured
|
||||
// stdout — that literal only appears in the tier-(b) write path.
|
||||
fn build_java_raw_spec(entry_name: &str) -> (HarnessSpec, TempDir) {
|
||||
let fixture_src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/header_injection/java_raw/Vuln.java");
|
||||
let tmp = TempDir::new().expect("create tempdir");
|
||||
let dst = tmp.path().join("Vuln.java");
|
||||
std::fs::copy(&fixture_src, &dst).expect("copy java_raw fixture into tempdir");
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
let mut digest = blake3::Hasher::new();
|
||||
digest.update(b"phase08-e2e-header-injection|java_raw|Vuln.java");
|
||||
let spec_hash = format!("{:016x}", {
|
||||
let bytes = digest.finalize();
|
||||
u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap())
|
||||
});
|
||||
// Mirror the Java workdir wipe used by build_spec — javac caches
|
||||
// compiled bytecode under the shared workdir at
|
||||
// `/tmp/nyx-harness/<spec_hash>`, so a previous run with a
|
||||
// different harness source can serve stale class files.
|
||||
let workdir = std::path::PathBuf::from("/tmp/nyx-harness").join(&spec_hash);
|
||||
let _ = std::fs::remove_dir_all(&workdir);
|
||||
let spec = HarnessSpec {
|
||||
finding_id: spec_hash.clone(),
|
||||
entry_file: entry_file.clone(),
|
||||
entry_name: entry_name.to_owned(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Java,
|
||||
toolchain_id: default_toolchain_id(Lang::Java).into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::HEADER_INJECTION,
|
||||
constraint_hints: vec![],
|
||||
sink_file: entry_file,
|
||||
sink_line: 1,
|
||||
spec_hash: spec_hash.clone(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(),
|
||||
};
|
||||
(spec, tmp)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_raw_socket_vuln_confirms_via_wire_frame_probe() {
|
||||
if !command_available("javac") {
|
||||
eprintln!("SKIP java_raw: missing javac");
|
||||
return;
|
||||
}
|
||||
if !command_available("java") {
|
||||
eprintln!("SKIP java_raw: missing java");
|
||||
return;
|
||||
}
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let (spec, _tmp) = build_java_raw_spec("run");
|
||||
let opts = SandboxOptions {
|
||||
backend: SandboxBackend::Process,
|
||||
..SandboxOptions::default()
|
||||
};
|
||||
let outcome = match run_spec(&spec, &opts) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(RunError::BuildFailed { stderr, attempts }) => {
|
||||
eprintln!(
|
||||
"SKIP java_raw: harness build failed after {attempts} attempts: {stderr}",
|
||||
);
|
||||
return;
|
||||
}
|
||||
Err(e) => panic!("run_spec(java_raw) errored: {e:?}"),
|
||||
};
|
||||
assert_confirmed(Lang::Java, &outcome);
|
||||
let any_wire_frame_marker = outcome.attempts.iter().any(|a| {
|
||||
String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len")
|
||||
});
|
||||
assert!(
|
||||
any_wire_frame_marker,
|
||||
"java_raw fixture must exercise the tier-(b) wire-frame harness branch; \
|
||||
expected `wire_frame_len` substring in at least one attempt's stdout, got attempts={:?}",
|
||||
outcome
|
||||
.attempts
|
||||
.iter()
|
||||
.map(|a| String::from_utf8_lossy(&a.outcome.stdout).into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue