2026-05-05 03:43:45 -04:00
|
|
|
//! Per-capability payload corpus.
|
|
|
|
|
//!
|
|
|
|
|
//! Each [`Cap`] maps to a small set of canonical payloads plus a matching
|
|
|
|
|
//! detection oracle. Payloads are static data — adding a new one is a code
|
|
|
|
|
//! review, not a runtime config knob, so they cannot drift between versions.
|
|
|
|
|
//!
|
|
|
|
|
//! The corpus is intentionally minimal at the start. Goal is one payload
|
|
|
|
|
//! per cap that triggers reliably on the obvious case; tuning happens once
|
|
|
|
|
//! we have real targets.
|
|
|
|
|
|
|
|
|
|
use crate::labels::Cap;
|
|
|
|
|
|
2026-05-11 21:19:03 -04:00
|
|
|
/// Bump when the corpus content changes in a way that invalidates previously-
|
|
|
|
|
/// computed [`crate::dynamic::spec::HarnessSpec::spec_hash`] values (e.g.
|
|
|
|
|
/// payloads renamed, oracle semantics changed, new cap entries added).
|
|
|
|
|
pub const CORPUS_VERSION: u32 = 1;
|
|
|
|
|
|
2026-05-05 03:43:45 -04:00
|
|
|
/// A single payload + the oracle that confirms it fired.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct Payload {
|
|
|
|
|
/// Bytes injected into the [`crate::dynamic::spec::PayloadSlot`].
|
|
|
|
|
pub bytes: &'static [u8],
|
|
|
|
|
/// Human label for logs and reports (`"sqli-quote-or-1"` etc.).
|
|
|
|
|
pub label: &'static str,
|
|
|
|
|
/// How we decide the sink fired. See [`Oracle`].
|
|
|
|
|
pub oracle: Oracle,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Detection strategy. Multiple oracles run in parallel; first hit wins.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub enum Oracle {
|
|
|
|
|
/// Substring on stdout/stderr (e.g. `"PAYLOAD-MARKER"`, SQL error text).
|
|
|
|
|
OutputContains(&'static str),
|
|
|
|
|
/// Process exited with a crash signal (SIGSEGV, SIGABRT).
|
|
|
|
|
Crash,
|
|
|
|
|
/// Outbound network connection observed to a controlled sink host.
|
|
|
|
|
OobCallback { host: &'static str },
|
|
|
|
|
/// File written outside the sandbox sandbox root.
|
|
|
|
|
FileEscape,
|
|
|
|
|
/// Non-zero exit with specific status (e.g. shell command success).
|
|
|
|
|
ExitStatus(i32),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Pick the payload set for a given cap. Empty slice = unsupported cap.
|
2026-05-11 21:19:03 -04:00
|
|
|
///
|
|
|
|
|
/// # Cap coverage (update when adding/removing Cap bits)
|
|
|
|
|
///
|
|
|
|
|
/// | Cap | Supported | Notes |
|
|
|
|
|
/// |--------------------|-----------|--------------------------------|
|
|
|
|
|
/// | SQL_QUERY | yes | SQLI payloads |
|
|
|
|
|
/// | CODE_EXEC | yes | command injection echo marker |
|
|
|
|
|
/// | FILE_IO | yes | path traversal to /etc/passwd |
|
|
|
|
|
/// | SSRF | yes | OOB callback probe |
|
|
|
|
|
/// | HTML_ESCAPE | yes | XSS script marker |
|
|
|
|
|
/// | ENV_VAR | no | source-only cap; no sink oracle|
|
|
|
|
|
/// | SHELL_ESCAPE | no | sanitizer cap; no sink oracle |
|
|
|
|
|
/// | URL_ENCODE | no | sanitizer cap; no sink oracle |
|
|
|
|
|
/// | JSON_PARSE | no | no reliable oracle |
|
|
|
|
|
/// | FMT_STRING | no | no reliable oracle |
|
|
|
|
|
/// | DESERIALIZE | no | no reliable oracle |
|
|
|
|
|
/// | CRYPTO | no | no reliable oracle |
|
|
|
|
|
/// | UNAUTHORIZED_ID | no | auth bypass; no oracle |
|
|
|
|
|
/// | DATA_EXFIL | no | exfil; no oracle |
|
|
|
|
|
/// | LDAP_INJECTION | no | no oracle |
|
|
|
|
|
/// | XPATH_INJECTION | no | no oracle |
|
|
|
|
|
/// | HEADER_INJECTION | no | no oracle |
|
|
|
|
|
/// | OPEN_REDIRECT | no | no oracle |
|
|
|
|
|
/// | SSTI | no | no oracle |
|
|
|
|
|
/// | XXE | no | no oracle |
|
|
|
|
|
/// | PROTOTYPE_POLLUTION| no | JS-runtime; no oracle |
|
|
|
|
|
///
|
|
|
|
|
/// When adding a new `Cap` bit: add a row above, update this function, and
|
|
|
|
|
/// bump [`CORPUS_VERSION`] if you add payload support.
|
2026-05-05 03:43:45 -04:00
|
|
|
pub fn payloads_for(cap: Cap) -> &'static [Payload] {
|
|
|
|
|
if cap.contains(Cap::SQL_QUERY) {
|
|
|
|
|
return SQLI;
|
|
|
|
|
}
|
|
|
|
|
if cap.contains(Cap::CODE_EXEC) {
|
|
|
|
|
return CMDI;
|
|
|
|
|
}
|
|
|
|
|
if cap.contains(Cap::FILE_IO) {
|
|
|
|
|
return PATH_TRAV;
|
|
|
|
|
}
|
|
|
|
|
if cap.contains(Cap::SSRF) {
|
|
|
|
|
return SSRF_PAYLOADS;
|
|
|
|
|
}
|
|
|
|
|
if cap.contains(Cap::HTML_ESCAPE) {
|
|
|
|
|
return XSS;
|
|
|
|
|
}
|
|
|
|
|
&[]
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 21:19:03 -04:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn supported_caps_have_payloads() {
|
|
|
|
|
assert!(!payloads_for(Cap::SQL_QUERY).is_empty());
|
|
|
|
|
assert!(!payloads_for(Cap::CODE_EXEC).is_empty());
|
|
|
|
|
assert!(!payloads_for(Cap::FILE_IO).is_empty());
|
|
|
|
|
assert!(!payloads_for(Cap::SSRF).is_empty());
|
|
|
|
|
assert!(!payloads_for(Cap::HTML_ESCAPE).is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unsupported_caps_return_empty() {
|
|
|
|
|
let unsupported = [
|
|
|
|
|
Cap::ENV_VAR,
|
|
|
|
|
Cap::SHELL_ESCAPE,
|
|
|
|
|
Cap::URL_ENCODE,
|
|
|
|
|
Cap::JSON_PARSE,
|
|
|
|
|
Cap::FMT_STRING,
|
|
|
|
|
Cap::DESERIALIZE,
|
|
|
|
|
Cap::CRYPTO,
|
|
|
|
|
Cap::UNAUTHORIZED_ID,
|
|
|
|
|
Cap::DATA_EXFIL,
|
|
|
|
|
Cap::LDAP_INJECTION,
|
|
|
|
|
Cap::XPATH_INJECTION,
|
|
|
|
|
Cap::HEADER_INJECTION,
|
|
|
|
|
Cap::OPEN_REDIRECT,
|
|
|
|
|
Cap::SSTI,
|
|
|
|
|
Cap::XXE,
|
|
|
|
|
Cap::PROTOTYPE_POLLUTION,
|
|
|
|
|
];
|
|
|
|
|
for cap in unsupported {
|
|
|
|
|
assert!(
|
|
|
|
|
payloads_for(cap).is_empty(),
|
|
|
|
|
"expected {cap:?} to return empty payloads; update coverage table if adding support"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 03:43:45 -04:00
|
|
|
const SQLI: &[Payload] = &[Payload {
|
|
|
|
|
bytes: b"' OR 1=1-- NYX",
|
|
|
|
|
label: "sqli-or-1",
|
|
|
|
|
oracle: Oracle::OutputContains("NYX"),
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
const CMDI: &[Payload] = &[Payload {
|
|
|
|
|
bytes: b"; echo NYX_PWN",
|
|
|
|
|
label: "cmdi-echo-marker",
|
|
|
|
|
oracle: Oracle::OutputContains("NYX_PWN"),
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
const PATH_TRAV: &[Payload] = &[Payload {
|
|
|
|
|
bytes: b"../../../../etc/passwd",
|
|
|
|
|
label: "path-traversal-passwd",
|
|
|
|
|
oracle: Oracle::OutputContains("root:"),
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
const SSRF_PAYLOADS: &[Payload] = &[Payload {
|
|
|
|
|
bytes: b"http://nyx-oob.invalid/probe",
|
|
|
|
|
label: "ssrf-oob",
|
|
|
|
|
oracle: Oracle::OobCallback {
|
|
|
|
|
host: "nyx-oob.invalid",
|
|
|
|
|
},
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
const XSS: &[Payload] = &[Payload {
|
|
|
|
|
bytes: b"<script>NYX_XSS</script>",
|
|
|
|
|
label: "xss-script-marker",
|
|
|
|
|
oracle: Oracle::OutputContains("<script>NYX_XSS</script>"),
|
|
|
|
|
}];
|