mirror of
https://github.com/elicpeter/nyx.git
synced 2026-07-06 20:42:10 +02:00
Dynamic (#77)
This commit is contained in:
parent
55247b7fcd
commit
991c84a1eb
1464 changed files with 225448 additions and 1985 deletions
15
tests/dynamic_fixtures/header_injection/go/benign.go
Normal file
15
tests/dynamic_fixtures/header_injection/go/benign.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Phase 08 (Track J.6) — Go HEADER_INJECTION benign control fixture.
|
||||
//
|
||||
// Same shape as `vuln.go` but URL-encodes the value via
|
||||
// `net/url.QueryEscape` before the header set, so CRLF bytes land as
|
||||
// `%0D%0A` and the wire keeps a single header.
|
||||
package benign
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func Run(w http.ResponseWriter, value string) {
|
||||
w.Header().Set("Set-Cookie", url.QueryEscape(value))
|
||||
}
|
||||
13
tests/dynamic_fixtures/header_injection/go/vuln.go
Normal file
13
tests/dynamic_fixtures/header_injection/go/vuln.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 08 (Track J.6) — Go HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// The function assigns the attacker-controlled `value` directly into a
|
||||
// `Set-Cookie` header via `http.ResponseWriter.Header().Set`. A
|
||||
// payload carrying `\r\nSet-Cookie: nyx-injected=pwn` splits the
|
||||
// single header into two on the wire.
|
||||
package vuln
|
||||
|
||||
import "net/http"
|
||||
|
||||
func Run(w http.ResponseWriter, value string) {
|
||||
w.Header().Set("Set-Cookie", value)
|
||||
}
|
||||
16
tests/dynamic_fixtures/header_injection/java/Benign.java
Normal file
16
tests/dynamic_fixtures/header_injection/java/Benign.java
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 08 (Track J.6) — Java HEADER_INJECTION benign control fixture.
|
||||
//
|
||||
// Same shape as `Vuln.java` but URL-encodes the value via
|
||||
// `URLEncoder.encode` (the OWASP-recommended defence), so any CRLF
|
||||
// bytes in the value land as `%0D%0A` and the wire keeps a single
|
||||
// header.
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
public class Benign {
|
||||
public static void run(HttpServletResponse response, String value) {
|
||||
String encoded = URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||
response.setHeader("Set-Cookie", encoded);
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/header_injection/java/Vuln.java
Normal file
13
tests/dynamic_fixtures/header_injection/java/Vuln.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 08 (Track J.6) — Java HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// The function string-concatenates the attacker-controlled `value`
|
||||
// directly into a `Set-Cookie` header set via
|
||||
// `HttpServletResponse.setHeader`. A payload carrying `\r\nSet-Cookie:
|
||||
// nyx-injected=pwn` splits the single header into two on the wire.
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
public class Vuln {
|
||||
public static void run(HttpServletResponse response, String value) {
|
||||
response.setHeader("Set-Cookie", value);
|
||||
}
|
||||
}
|
||||
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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/header_injection/js/benign.js
Normal file
13
tests/dynamic_fixtures/header_injection/js/benign.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 08 (Track J.6) — JavaScript HEADER_INJECTION benign control
|
||||
// fixture.
|
||||
//
|
||||
// Same shape as `vuln.js` but URL-encodes the value first via
|
||||
// `encodeURIComponent`, so CRLF bytes land as `%0D%0A` and the wire
|
||||
// keeps a single header.
|
||||
const http = require('http');
|
||||
|
||||
function run(res, value) {
|
||||
res.setHeader('Set-Cookie', encodeURIComponent(value));
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
13
tests/dynamic_fixtures/header_injection/js/vuln.js
Normal file
13
tests/dynamic_fixtures/header_injection/js/vuln.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 08 (Track J.6) — JavaScript HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// The function assigns the attacker-controlled `value` directly into a
|
||||
// Node response's `Set-Cookie` header via `http.ServerResponse
|
||||
// #setHeader`. A payload carrying `\r\nSet-Cookie: nyx-injected=pwn`
|
||||
// splits the single header into two on the wire.
|
||||
const http = require('http');
|
||||
|
||||
function run(res, value) {
|
||||
res.setHeader('Set-Cookie', value);
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
50
tests/dynamic_fixtures/header_injection/js_raw/vuln.js
Normal file
50
tests/dynamic_fixtures/header_injection/js_raw/vuln.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Phase 08 (Track J.6) — JavaScript raw-socket HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// Writes the response status line and headers directly to the wire via
|
||||
// `socket.write`, bypassing the framework-level CRLF validator that
|
||||
// Node's `http.ServerResponse#setHeader` / Express / axum / Tomcat
|
||||
// would otherwise interpose. 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/js_shared.rs::emit_header_injection_harness`)
|
||||
// detects the `net.createServer` import in this file and routes
|
||||
// through the tier-(b) wire-frame branch: boot a `net.Server` on a
|
||||
// loopback port, issue one `GET /` over a raw socket, read the bytes
|
||||
// the handler wrote to the response socket, and emit them as a
|
||||
// `ProbeKind::HeaderWireFrame` record.
|
||||
const net = require('net');
|
||||
|
||||
// Set by the harness before each request. Bytes go straight onto
|
||||
// the wire with no encoding pass.
|
||||
let cookieValue = Buffer.alloc(0);
|
||||
|
||||
function setCookieValue(value) {
|
||||
if (Buffer.isBuffer(value)) {
|
||||
cookieValue = value;
|
||||
} else {
|
||||
cookieValue = Buffer.from(String(value), 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
function createServer() {
|
||||
return net.createServer((socket) => {
|
||||
socket.once('data', () => {
|
||||
const body = Buffer.from('ok\n');
|
||||
const head = Buffer.concat([
|
||||
Buffer.from('HTTP/1.0 200 OK\r\n'),
|
||||
Buffer.from('Content-Length: ' + body.length + '\r\n'),
|
||||
Buffer.from('Set-Cookie: '),
|
||||
cookieValue,
|
||||
Buffer.from('\r\n'),
|
||||
Buffer.from('\r\n'),
|
||||
]);
|
||||
socket.write(Buffer.concat([head, body]));
|
||||
socket.end();
|
||||
});
|
||||
socket.on('error', () => {});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { setCookieValue, createServer };
|
||||
9
tests/dynamic_fixtures/header_injection/php/benign.php
Normal file
9
tests/dynamic_fixtures/header_injection/php/benign.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
// Phase 08 (Track J.6) — PHP HEADER_INJECTION benign control fixture.
|
||||
//
|
||||
// Same shape as `vuln.php` but URL-encodes the value first via
|
||||
// `urlencode`, so CRLF bytes land as `%0D%0A` and the wire keeps a
|
||||
// single header.
|
||||
function run($value) {
|
||||
header("Set-Cookie: " . urlencode($value));
|
||||
}
|
||||
10
tests/dynamic_fixtures/header_injection/php/vuln.php
Normal file
10
tests/dynamic_fixtures/header_injection/php/vuln.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// Phase 08 (Track J.6) — PHP HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// The function concatenates the attacker-controlled `$value` directly
|
||||
// into a `Set-Cookie` header set via the built-in `header()` function.
|
||||
// A payload carrying `\r\nSet-Cookie: nyx-injected=pwn` splits the
|
||||
// single header into two on the wire.
|
||||
function run($value) {
|
||||
header("Set-Cookie: " . $value);
|
||||
}
|
||||
68
tests/dynamic_fixtures/header_injection/php_raw/vuln.php
Normal file
68
tests/dynamic_fixtures/header_injection/php_raw/vuln.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
// Phase 08 (Track J.6) — PHP raw-socket HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// Writes the response status line and headers directly to the wire via
|
||||
// `fwrite($conn, $raw)` against a `stream_socket_server` server-side
|
||||
// stream, bypassing the framework-level CRLF validator that PHP's
|
||||
// built-in `header()` (rejected since PHP 5.1.2) or a Slim / Symfony
|
||||
// / Laravel response serializer would otherwise interpose. 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/php.rs::emit_header_injection_harness`)
|
||||
// detects the `stream_socket_server` token in this file and routes
|
||||
// through the tier-(b) wire-frame branch: bind a loopback server via
|
||||
// `create_server`, accept one client (`run_once`), issue one raw
|
||||
// `GET / HTTP/1.0` from the harness, read the bytes the fixture wrote
|
||||
// to the response stream up to the CRLF-CRLF boundary, and emit them
|
||||
// as a `ProbeKind::HeaderWireFrame` record.
|
||||
//
|
||||
// Uses `stream_socket_server` rather than `socket_create` so the
|
||||
// fixture has no hard dep on `ext-sockets` (which is core but not
|
||||
// always built into minimal PHP images); `stream_socket_server`
|
||||
// resolves through the always-available filesystem-and-network
|
||||
// stream layer.
|
||||
|
||||
// Bytes go straight onto the wire with no encoding pass. The harness
|
||||
// installs the cookie value before booting the accept loop, mirroring
|
||||
// the Ruby `set_cookie_value` and Python `Handler.cookie_value` setters.
|
||||
$GLOBALS['nyx_cookie_value'] = '';
|
||||
|
||||
function set_cookie_value($value) {
|
||||
$GLOBALS['nyx_cookie_value'] = (string) $value;
|
||||
}
|
||||
|
||||
function create_server() {
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
|
||||
if ($server === false) {
|
||||
throw new \RuntimeException("stream_socket_server failed: $errno $errstr");
|
||||
}
|
||||
return $server;
|
||||
}
|
||||
|
||||
function run_once($server) {
|
||||
$conn = @stream_socket_accept($server, 5.0);
|
||||
if ($conn === false) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Drain whatever request bytes the client sent so the kernel
|
||||
// does not stall the write that follows. Ignore errors.
|
||||
@stream_set_timeout($conn, 1, 0);
|
||||
@fread($conn, 4096);
|
||||
$body = "ok\n";
|
||||
$cookie = isset($GLOBALS['nyx_cookie_value']) ? $GLOBALS['nyx_cookie_value'] : '';
|
||||
$raw = "HTTP/1.0 200 OK\r\n"
|
||||
. "Content-Length: " . strlen($body) . "\r\n"
|
||||
. "Set-Cookie: " . $cookie . "\r\n"
|
||||
. "\r\n"
|
||||
. $body;
|
||||
@fwrite($conn, $raw);
|
||||
@fflush($conn);
|
||||
} finally {
|
||||
@fclose($conn);
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/header_injection/python/benign.py
Normal file
13
tests/dynamic_fixtures/header_injection/python/benign.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Phase 08 (Track J.6) — Python HEADER_INJECTION benign control fixture.
|
||||
#
|
||||
# Same shape as `vuln.py` but URL-encodes the value via
|
||||
# `urllib.parse.quote` first, so CRLF bytes land as `%0D%0A` and the
|
||||
# wire keeps a single header.
|
||||
from urllib.parse import quote
|
||||
from flask import Response
|
||||
|
||||
|
||||
def run(value):
|
||||
response = Response("ok")
|
||||
response.headers["Set-Cookie"] = quote(value, safe="")
|
||||
return response
|
||||
13
tests/dynamic_fixtures/header_injection/python/vuln.py
Normal file
13
tests/dynamic_fixtures/header_injection/python/vuln.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Phase 08 (Track J.6) — Python HEADER_INJECTION vuln fixture.
|
||||
#
|
||||
# The function assigns the attacker-controlled `value` directly into
|
||||
# a Flask response's `Set-Cookie` header via `Response.headers
|
||||
# .__setitem__`. A payload carrying `\r\nSet-Cookie: nyx-injected=pwn`
|
||||
# splits the single header into two on the wire.
|
||||
from flask import Response
|
||||
|
||||
|
||||
def run(value):
|
||||
response = Response("ok")
|
||||
response.headers["Set-Cookie"] = value
|
||||
return response
|
||||
37
tests/dynamic_fixtures/header_injection/python_raw/vuln.py
Normal file
37
tests/dynamic_fixtures/header_injection/python_raw/vuln.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Phase 08 (Track J.6) — Python raw-socket HEADER_INJECTION vuln fixture.
|
||||
#
|
||||
# Writes the response status line and headers directly to the wire via
|
||||
# `self.wfile.write`, bypassing the framework-level CRLF validator that
|
||||
# werkzeug / Flask / axum / Tomcat would otherwise interpose. 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/python.rs::emit_header_injection_harness`)
|
||||
# detects the `BaseHTTPRequestHandler` import in this file and routes
|
||||
# through the tier-(b) wire-frame branch: boot `HTTPServer` on a
|
||||
# loopback port, issue one `GET /` over a raw socket, read the bytes
|
||||
# the handler wrote to the response socket, and emit them as a
|
||||
# `ProbeKind::HeaderWireFrame` record.
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
|
||||
|
||||
class VulnHandler(BaseHTTPRequestHandler):
|
||||
# Set by the harness before each request. Bytes go straight onto
|
||||
# the wire with no encoding pass.
|
||||
cookie_value: bytes = b""
|
||||
|
||||
def do_GET(self):
|
||||
body = b"ok\n"
|
||||
raw = (
|
||||
b"HTTP/1.0 200 OK\r\n"
|
||||
b"Content-Length: " + str(len(body)).encode("ascii") + b"\r\n"
|
||||
b"Set-Cookie: " + self.__class__.cookie_value + b"\r\n"
|
||||
b"\r\n"
|
||||
) + body
|
||||
self.wfile.write(raw)
|
||||
|
||||
def log_message(self, *args, **kwargs):
|
||||
# Silence default stderr logging so the harness captures only
|
||||
# the probe + sink-hit sentinel.
|
||||
return
|
||||
13
tests/dynamic_fixtures/header_injection/ruby/benign.rb
Normal file
13
tests/dynamic_fixtures/header_injection/ruby/benign.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Phase 08 (Track J.6) — Ruby HEADER_INJECTION benign control fixture.
|
||||
#
|
||||
# Same shape as `vuln.rb` but URL-encodes the value first via
|
||||
# `URI.encode_www_form_component`, so CRLF bytes land as `%0D%0A` and
|
||||
# the wire keeps a single header.
|
||||
require 'rack'
|
||||
require 'uri'
|
||||
|
||||
def run(value)
|
||||
response = Rack::Response.new
|
||||
response.set_header('Set-Cookie', URI.encode_www_form_component(value))
|
||||
response
|
||||
end
|
||||
13
tests/dynamic_fixtures/header_injection/ruby/vuln.rb
Normal file
13
tests/dynamic_fixtures/header_injection/ruby/vuln.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Phase 08 (Track J.6) — Ruby HEADER_INJECTION vuln fixture.
|
||||
#
|
||||
# The function assigns the attacker-controlled `value` directly into a
|
||||
# Rack response's `Set-Cookie` header via `Rack::Response#set_header`.
|
||||
# A payload carrying `\r\nSet-Cookie: nyx-injected=pwn` splits the
|
||||
# single header into two on the wire.
|
||||
require 'rack'
|
||||
|
||||
def run(value)
|
||||
response = Rack::Response.new
|
||||
response.set_header('Set-Cookie', value)
|
||||
response
|
||||
end
|
||||
54
tests/dynamic_fixtures/header_injection/ruby_raw/vuln.rb
Normal file
54
tests/dynamic_fixtures/header_injection/ruby_raw/vuln.rb
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Phase 08 (Track J.6) — Ruby raw-socket HEADER_INJECTION vuln fixture.
|
||||
#
|
||||
# Writes the response status line and headers directly to the wire via
|
||||
# `socket.write`, bypassing the framework-level CRLF validator that
|
||||
# Rack / Sinatra / Rails would otherwise interpose. 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/ruby.rs::emit_header_injection_harness`)
|
||||
# detects the `TCPServer.new` token in this file and routes through the
|
||||
# tier-(b) wire-frame branch: bind a loopback `TCPServer` via
|
||||
# `create_server`, accept one client (`run_once`), issue one raw
|
||||
# `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.
|
||||
require 'socket'
|
||||
|
||||
# Bytes go straight onto the wire with no encoding pass. The harness
|
||||
# installs the cookie value before booting the accept loop, mirroring
|
||||
# the JS `setCookieValue` and Python `Handler.cookie_value =` setters.
|
||||
$nyx_cookie_value = String.new(encoding: 'BINARY')
|
||||
|
||||
def set_cookie_value(value)
|
||||
$nyx_cookie_value = value.respond_to?(:b) ? value.b : value.to_s.b
|
||||
end
|
||||
|
||||
def create_server
|
||||
TCPServer.new('127.0.0.1', 0)
|
||||
end
|
||||
|
||||
def run_once(server)
|
||||
socket = server.accept
|
||||
begin
|
||||
socket.recv(4096)
|
||||
rescue StandardError
|
||||
# ignore client-side read errors
|
||||
end
|
||||
body = "ok\n".b
|
||||
raw = String.new(encoding: 'BINARY')
|
||||
raw << "HTTP/1.0 200 OK\r\n".b
|
||||
raw << "Content-Length: #{body.bytesize}\r\n".b
|
||||
raw << "Set-Cookie: ".b
|
||||
raw << $nyx_cookie_value
|
||||
raw << "\r\n\r\n".b
|
||||
raw << body
|
||||
socket.write(raw)
|
||||
ensure
|
||||
begin
|
||||
socket.close if socket
|
||||
rescue StandardError
|
||||
# ignore close errors
|
||||
end
|
||||
end
|
||||
16
tests/dynamic_fixtures/header_injection/rust/benign.rs
Normal file
16
tests/dynamic_fixtures/header_injection/rust/benign.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 08 (Track J.6) — Rust HEADER_INJECTION benign control fixture.
|
||||
//
|
||||
// Same shape as `vuln.rs` but routes the value through the
|
||||
// `percent-encoding` crate first, so CRLF bytes land as `%0D%0A` and
|
||||
// the wire keeps a single header.
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::HeaderValue;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
pub fn run(headers: &mut HeaderMap, value: &str) {
|
||||
let encoded: String = utf8_percent_encode(value, NON_ALPHANUMERIC).collect();
|
||||
headers.insert(
|
||||
"set-cookie",
|
||||
HeaderValue::from_str(&encoded).unwrap(),
|
||||
);
|
||||
}
|
||||
17
tests/dynamic_fixtures/header_injection/rust/vuln.rs
Normal file
17
tests/dynamic_fixtures/header_injection/rust/vuln.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 08 (Track J.6) — Rust HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// The function inserts the attacker-controlled `value` into an axum
|
||||
// `HeaderMap` via `headers_mut().insert`, bypassing
|
||||
// `HeaderValue::from_str`'s newline check by passing the tainted
|
||||
// bytes through `HeaderValue::from_bytes(...).unwrap()`. A payload
|
||||
// carrying `\r\nSet-Cookie: nyx-injected=pwn` splits the single
|
||||
// header into two on the wire.
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::HeaderValue;
|
||||
|
||||
pub fn run(headers: &mut HeaderMap, value: &str) {
|
||||
headers.insert(
|
||||
"set-cookie",
|
||||
HeaderValue::from_bytes(value.as_bytes()).unwrap(),
|
||||
);
|
||||
}
|
||||
58
tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs
Normal file
58
tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Phase 08 (Track J.6) — Rust raw-socket HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// Writes the response status line and headers directly to the wire via
|
||||
// `TcpStream::write_all`, bypassing the framework-level CRLF validator
|
||||
// that axum / Tomcat would otherwise interpose. 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/rust.rs::emit_header_injection_harness`)
|
||||
// detects the `TcpListener::bind` token in this file and routes through
|
||||
// the tier-(b) wire-frame branch: bind a loopback `TcpListener` via
|
||||
// `create_server`, spawn the accept loop on a thread (`run_once`),
|
||||
// issue one raw `GET / HTTP/1.0\r\n` from the harness, read the bytes
|
||||
// the fixture wrote to the response socket, and emit them as a
|
||||
// `ProbeKind::HeaderWireFrame` record.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{Shutdown, TcpListener};
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Bytes go straight onto the wire with no encoding pass. The harness
|
||||
/// installs the cookie value before booting the accept loop, mirroring
|
||||
/// the JS `setCookieValue` and Python `Handler.cookie_value =` setters.
|
||||
static COOKIE_VALUE: Mutex<Vec<u8>> = Mutex::new(Vec::new());
|
||||
|
||||
pub fn set_cookie_value(value: &[u8]) {
|
||||
let mut guard = COOKIE_VALUE.lock().expect("cookie mutex poisoned");
|
||||
guard.clear();
|
||||
guard.extend_from_slice(value);
|
||||
}
|
||||
|
||||
pub fn create_server() -> TcpListener {
|
||||
TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port")
|
||||
}
|
||||
|
||||
pub fn run_once(listener: TcpListener) {
|
||||
let Ok((mut socket, _addr)) = listener.accept() else {
|
||||
return;
|
||||
};
|
||||
let mut scratch = [0u8; 4096];
|
||||
let _ = socket.read(&mut scratch);
|
||||
let cookie = COOKIE_VALUE
|
||||
.lock()
|
||||
.expect("cookie mutex poisoned")
|
||||
.clone();
|
||||
let body = b"ok\n";
|
||||
let mut raw = Vec::new();
|
||||
raw.extend_from_slice(b"HTTP/1.0 200 OK\r\n");
|
||||
raw.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
|
||||
raw.extend_from_slice(b"Set-Cookie: ");
|
||||
raw.extend_from_slice(&cookie);
|
||||
raw.extend_from_slice(b"\r\n");
|
||||
raw.extend_from_slice(b"\r\n");
|
||||
raw.extend_from_slice(body);
|
||||
let _ = socket.write_all(&raw);
|
||||
let _ = socket.shutdown(Shutdown::Both);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue