mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-18 20:15:14 +02:00
[pitboss] phase 04: Track J.2 + Track L.2 — SSTI corpus + Jinja2 / ERB / Twig / Thymeleaf / Handlebars adapters
This commit is contained in:
parent
b5e6dddf2c
commit
8583b29796
34 changed files with 1868 additions and 29 deletions
|
|
@ -23,7 +23,7 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::{cmdi, deserialize, fmt_string, path_trav, sqli, ssrf, xss};
|
||||
use super::{cmdi, deserialize, fmt_string, path_trav, sqli, ssrf, ssti, xss};
|
||||
use super::{CapCorpus, CuratedPayload, Oracle};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
use crate::labels::Cap;
|
||||
|
|
@ -44,7 +44,6 @@ pub const CORPUS_UNSUPPORTED_LANG_NEUTRAL: u32 = Cap::ENV_VAR.bits()
|
|||
| Cap::XPATH_INJECTION.bits()
|
||||
| Cap::HEADER_INJECTION.bits()
|
||||
| Cap::OPEN_REDIRECT.bits()
|
||||
| Cap::SSTI.bits()
|
||||
| Cap::XXE.bits()
|
||||
| Cap::PROTOTYPE_POLLUTION.bits();
|
||||
|
||||
|
|
@ -61,6 +60,11 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[
|
|||
(Cap::DESERIALIZE, Lang::Python, deserialize::python::PAYLOADS),
|
||||
(Cap::DESERIALIZE, Lang::Php, deserialize::php::PAYLOADS),
|
||||
(Cap::DESERIALIZE, Lang::Ruby, deserialize::ruby::PAYLOADS),
|
||||
(Cap::SSTI, Lang::Python, ssti::python_jinja2::PAYLOADS),
|
||||
(Cap::SSTI, Lang::Ruby, ssti::ruby_erb::PAYLOADS),
|
||||
(Cap::SSTI, Lang::Php, ssti::php_twig::PAYLOADS),
|
||||
(Cap::SSTI, Lang::Java, ssti::java_thymeleaf::PAYLOADS),
|
||||
(Cap::SSTI, Lang::JavaScript, ssti::js_handlebars::PAYLOADS),
|
||||
];
|
||||
|
||||
/// Reserved for per-cap oracle defaults. Empty in Phase 02; populated by
|
||||
|
|
@ -267,6 +271,8 @@ mod tests {
|
|||
assert!(!payloads_for(Cap::SSRF).is_empty());
|
||||
assert!(!payloads_for(Cap::HTML_ESCAPE).is_empty());
|
||||
assert!(!payloads_for(Cap::FMT_STRING).is_empty());
|
||||
assert!(!payloads_for(Cap::DESERIALIZE).is_empty());
|
||||
assert!(!payloads_for(Cap::SSTI).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -283,7 +289,6 @@ mod tests {
|
|||
Cap::XPATH_INJECTION,
|
||||
Cap::HEADER_INJECTION,
|
||||
Cap::OPEN_REDIRECT,
|
||||
Cap::SSTI,
|
||||
Cap::XXE,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
];
|
||||
|
|
@ -314,6 +319,7 @@ mod tests {
|
|||
Cap::HTML_ESCAPE,
|
||||
Cap::FMT_STRING,
|
||||
Cap::DESERIALIZE,
|
||||
Cap::SSTI,
|
||||
] {
|
||||
let has_vuln = payloads_for(cap).iter().any(|p| !p.is_benign);
|
||||
assert!(has_vuln, "{cap:?} must have at least one vuln payload");
|
||||
|
|
@ -361,6 +367,7 @@ mod tests {
|
|||
Cap::HTML_ESCAPE,
|
||||
Cap::FMT_STRING,
|
||||
Cap::DESERIALIZE,
|
||||
Cap::SSTI,
|
||||
];
|
||||
for cap in caps {
|
||||
for p in payloads_for(cap) {
|
||||
|
|
@ -383,6 +390,7 @@ mod tests {
|
|||
Cap::HTML_ESCAPE,
|
||||
Cap::FMT_STRING,
|
||||
Cap::DESERIALIZE,
|
||||
Cap::SSTI,
|
||||
];
|
||||
for cap in caps {
|
||||
for p in payloads_for(cap) {
|
||||
|
|
@ -492,6 +500,7 @@ mod tests {
|
|||
Cap::HTML_ESCAPE,
|
||||
Cap::FMT_STRING,
|
||||
Cap::DESERIALIZE,
|
||||
Cap::SSTI,
|
||||
];
|
||||
for cap in caps {
|
||||
for p in payloads_for(cap).iter().filter(|p| p.is_benign) {
|
||||
|
|
@ -574,6 +583,52 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssti_has_per_lang_slices_for_phase_04() {
|
||||
// Phase 04 (Track J.2) acceptance: SSTI registers payloads in
|
||||
// Python / Ruby / PHP / Java / JavaScript and the lang-aware
|
||||
// lookup never returns empty for any of them.
|
||||
for lang in [
|
||||
Lang::Python,
|
||||
Lang::Ruby,
|
||||
Lang::Php,
|
||||
Lang::Java,
|
||||
Lang::JavaScript,
|
||||
] {
|
||||
assert!(
|
||||
!payloads_for_lang(Cap::SSTI, lang).is_empty(),
|
||||
"SSTI must have at least one payload for {lang:?}",
|
||||
);
|
||||
}
|
||||
// Rust / C / Cpp / Go / TypeScript not yet covered.
|
||||
for lang in [Lang::Rust, Lang::C, Lang::Cpp, Lang::Go, Lang::TypeScript] {
|
||||
assert!(
|
||||
payloads_for_lang(Cap::SSTI, lang).is_empty(),
|
||||
"SSTI has unexpected payloads for {lang:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssti_payloads_pair_benign_controls_per_lang() {
|
||||
for lang in [
|
||||
Lang::Python,
|
||||
Lang::Ruby,
|
||||
Lang::Php,
|
||||
Lang::Java,
|
||||
Lang::JavaScript,
|
||||
] {
|
||||
let slice = payloads_for_lang(Cap::SSTI, lang);
|
||||
let vuln = slice
|
||||
.iter()
|
||||
.find(|p| !p.is_benign)
|
||||
.expect("each lang must have an SSTI vuln payload");
|
||||
let resolved = super::resolve_benign_control_lang(vuln, Cap::SSTI, lang)
|
||||
.expect("lang-aware benign control must resolve");
|
||||
assert!(resolved.is_benign);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_payloads_pair_benign_controls_per_lang() {
|
||||
// The lang-aware resolver must find the paired benign control
|
||||
|
|
|
|||
50
src/dynamic/corpus/ssti/java_thymeleaf.rs
Normal file
50
src/dynamic/corpus/ssti/java_thymeleaf.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//! Java Thymeleaf `Cap::SSTI` payloads.
|
||||
//!
|
||||
//! Vuln payload: `[[${7*7}]]` — Thymeleaf evaluates the SpEL-style
|
||||
//! expression inside the inlined-output marker and renders `49`.
|
||||
//! Benign control sends the literal `7*7` text; without the `[[${...}]]`
|
||||
//! markers Thymeleaf passes the payload through unchanged.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"[[${7*7}]]",
|
||||
label: "ssti-thymeleaf-eval",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/java_thymeleaf/vuln.java",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "ssti-thymeleaf-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"7*7",
|
||||
label: "ssti-thymeleaf-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/java_thymeleaf/benign.java",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
56
src/dynamic/corpus/ssti/js_handlebars.rs
Normal file
56
src/dynamic/corpus/ssti/js_handlebars.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
//! JavaScript Handlebars `Cap::SSTI` payloads.
|
||||
//!
|
||||
//! Handlebars does not evaluate arbitrary arithmetic in `{{ ... }}`
|
||||
//! expressions out of the box, so the vuln payload reaches the engine
|
||||
//! through the built-in `lookup` helper combined with a constructor
|
||||
//! gadget chain: `{{#with (lookup this 'constructor')}}{{lookup
|
||||
//! this 'constructor'}}{{/with}}` is the canonical pattern, but the
|
||||
//! evaluation marker we need ("rendered constant only via eval")
|
||||
//! reduces to a much simpler `{{multiply 7 7}}` against the in-harness
|
||||
//! `multiply` helper. The harness registers that helper before
|
||||
//! compiling so the rendered body is `49`; benign control sends `7*7`
|
||||
//! plain text which Handlebars echoes verbatim.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"{{multiply 7 7}}",
|
||||
label: "ssti-handlebars-eval",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/js_handlebars/vuln.js",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "ssti-handlebars-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"7*7",
|
||||
label: "ssti-handlebars-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/js_handlebars/benign.js",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
19
src/dynamic/corpus/ssti/mod.rs
Normal file
19
src/dynamic/corpus/ssti/mod.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//! Server-Side Template Injection (`Cap::SSTI`) per-engine payload slices.
|
||||
//!
|
||||
//! Phase 04 (Track J.2) carves SSTI across the five most-common template
|
||||
//! engines: Jinja2 (Python), ERB (Ruby), Twig (PHP), Thymeleaf (Java), and
|
||||
//! Handlebars (JavaScript). Every vuln payload sends a template
|
||||
//! expression that resolves to a known constant *only* when the engine
|
||||
//! actually evaluates the expression (e.g. `{{7*7}}` → `49` in Jinja2,
|
||||
//! `<%= 7*7 %>` → `49` in ERB). The paired benign control sends the
|
||||
//! literal arithmetic text without engine markers so the per-engine
|
||||
//! harness echoes the payload verbatim rather than evaluating it; the
|
||||
//! oracle's [`crate::dynamic::oracle::ProbePredicate::TemplateEvalEqual`]
|
||||
//! check fires on the vuln render (`49`) and does not fire on the
|
||||
//! benign render (`7*7`), satisfying the §4.1 differential rule.
|
||||
|
||||
pub mod java_thymeleaf;
|
||||
pub mod js_handlebars;
|
||||
pub mod php_twig;
|
||||
pub mod python_jinja2;
|
||||
pub mod ruby_erb;
|
||||
50
src/dynamic/corpus/ssti/php_twig.rs
Normal file
50
src/dynamic/corpus/ssti/php_twig.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//! PHP Twig `Cap::SSTI` payloads.
|
||||
//!
|
||||
//! Vuln payload: `{{7*7}}` — Twig evaluates the expression and the
|
||||
//! rendered template body is `49`. Benign control sends the literal
|
||||
//! `7*7` text; Twig has no `{{ ... }}` markers around it and echoes
|
||||
//! the payload verbatim.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"{{7*7}}",
|
||||
label: "ssti-twig-eval",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/php_twig/vuln.php",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "ssti-twig-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"7*7",
|
||||
label: "ssti-twig-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/php_twig/benign.php",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
57
src/dynamic/corpus/ssti/python_jinja2.rs
Normal file
57
src/dynamic/corpus/ssti/python_jinja2.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//! Python Jinja2 `Cap::SSTI` payloads.
|
||||
//!
|
||||
//! Vuln payload: `{{7*7}}` — Jinja2 evaluates the expression and the
|
||||
//! rendered template body is `49`. The harness's
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::TemplateEvalEqual`] check
|
||||
//! compares the captured `{"render": "49"}` JSON body against
|
||||
//! `expected = 49` and the oracle fires.
|
||||
//!
|
||||
//! Benign control: literal `7*7` — Jinja2 has no `{{ ... }}` markers to
|
||||
//! evaluate so the engine echoes the payload verbatim. The rendered
|
||||
//! body is `7*7`, the oracle's integer parse fails, and the oracle
|
||||
//! does not fire. Together with the vuln payload this satisfies the
|
||||
//! §4.1 differential confirmation rule.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"{{7*7}}",
|
||||
label: "ssti-jinja2-eval",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/python_jinja2/vuln.py",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "ssti-jinja2-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"7*7",
|
||||
label: "ssti-jinja2-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/python_jinja2/benign.py",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
50
src/dynamic/corpus/ssti/ruby_erb.rs
Normal file
50
src/dynamic/corpus/ssti/ruby_erb.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//! Ruby ERB `Cap::SSTI` payloads.
|
||||
//!
|
||||
//! Vuln payload: `<%= 7*7 %>` — ERB evaluates the embedded Ruby
|
||||
//! expression and the rendered template body is `49`. Benign control
|
||||
//! ships the literal `7*7` text which ERB has no `<%= ... %>` marker
|
||||
//! around and so passes through verbatim.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"<%= 7*7 %>",
|
||||
label: "ssti-erb-eval",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/ruby_erb/vuln.rb",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "ssti-erb-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"7*7",
|
||||
label: "ssti-erb-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 8,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/dynamic_fixtures/ssti/ruby_erb/benign.rb",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue