mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-30 20:39:39 +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
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