use std::collections::HashSet;
use crate::encoding::{alternate_case, apply_encoding, EncodingError};
pub fn mutate_case(payload: &str) -> Vec<String> {
if payload.is_empty() {
return Vec::new();
}
collect_unique([
payload.to_lowercase(),
payload.to_uppercase(),
alternate_case(payload, 0),
alternate_case(payload, 1),
])
}
pub fn mutate_whitespace(payload: &str) -> Vec<String> {
let parts: Vec<&str> = payload.split_whitespace().collect();
if parts.len() >= 2 {
let variants = collect_unique([
parts.join("\t"),
parts.join("\n"),
parts.join("/**/"),
parts.join("/*comment*/"),
]);
return variants.into_iter().filter(|v| v != payload).collect();
}
let chars: Vec<char> = payload.chars().collect();
if chars.len() < 2 {
return Vec::new();
}
let split = chars.len() / 2;
let left: String = chars[..split].iter().collect();
let right: String = chars[split..].iter().collect();
collect_unique([
format!("{left}\t{right}"),
format!("{left}\n{right}"),
format!("{left}/**/{right}"),
format!("{left}/*comment*/{right}"),
])
}
pub fn mutate_encoding_mix(
payload: &str,
encodings: &[&str],
) -> Result<Vec<String>, EncodingError> {
if encodings.len() < 2 || payload.chars().count() < 2 {
return Ok(Vec::new());
}
let split_at = payload
.char_indices()
.nth(payload.chars().count() / 2)
.map_or(payload.len(), |(idx, _)| idx);
let (left, right) = payload.split_at(split_at);
let mut variants = Vec::new();
for left_encoding in encodings {
for right_encoding in encodings {
if left_encoding == right_encoding {
continue;
}
variants.push(format!(
"{}{}",
apply_encoding(left, left_encoding)?,
apply_encoding(right, right_encoding)?
));
}
}
Ok(collect_unique(variants))
}
pub fn mutate_null_bytes(payload: &str) -> Vec<String> {
if payload.is_empty() {
return Vec::new();
}
let chars: Vec<char> = payload.chars().collect();
if chars.len() < 3 {
return collect_unique([
format!("%00{payload}"),
format!("{payload}%00"),
format!("\x00{payload}"),
format!("{payload}\x00"),
]);
}
let mid = chars.len() / 2;
let left: String = chars[..mid].iter().collect();
let right: String = chars[mid..].iter().collect();
collect_unique([
format!("%00{payload}"),
format!("{payload}%00"),
format!("{left}%00{right}"),
format!("\x00{payload}"),
format!("{payload}\x00"),
format!("{left}\x00{right}"),
])
}
pub fn mutate_sql_comments(payload: &str) -> Vec<String> {
let parts: Vec<&str> = payload.split_whitespace().collect();
if parts.len() < 2 {
return Vec::new();
}
collect_unique([
parts.join("/**/"),
parts.join("/*!*/"),
parts.join("/*! */"),
parts.join("/**_**/"),
parts.join("--\n"),
parts.join("#\n"),
])
}
pub fn mutate_html(payload: &str) -> Vec<String> {
let mut variants = Vec::new();
if payload.contains('<') {
let lower = payload.to_lowercase();
let upper = payload.to_uppercase();
if lower != payload {
variants.push(lower);
}
if upper != payload {
variants.push(upper);
}
}
if payload.contains('"') {
variants.push(payload.replace('"', "'"));
variants.push(payload.replace('"', "`"));
variants.push(payload.replace('"', ""));
}
if payload.contains('=') {
variants.push(payload.replace('=', " = "));
variants.push(payload.replace('=', "\t=\t"));
variants.push(payload.replace('=', "\n=\n"));
}
let tags = [
"script", "img", "svg", "body", "iframe", "object", "embed", "math", "a", "form",
];
for tag in tags {
let mixed_tag = format!("<{}", alternate_case(tag, 1));
if let Some(variant) = replace_tag_span(payload, tag, &mixed_tag) {
variants.push(variant);
}
if let Some(variant) = replace_tag_span(payload, tag, &format!("<{tag}/")) {
variants.push(variant);
}
if let Some(variant) = replace_tag_span(payload, tag, &format!("<{tag} ")) {
variants.push(variant);
}
}
collect_unique(variants)
}
fn replace_tag_span(payload: &str, tag: &str, replacement: &str) -> Option<String> {
let span = 1 + tag.len(); let bytes = payload.as_bytes();
let mut out = String::with_capacity(payload.len());
let mut last = 0;
let mut i = 0;
let mut replaced = false;
while i + span <= bytes.len() {
if bytes[i] == b'<'
&& payload
.get(i + 1..i + span)
.is_some_and(|s| s.eq_ignore_ascii_case(tag))
&& (i + span == bytes.len() || !bytes[i + span].is_ascii_alphanumeric())
{
out.push_str(&payload[last..i]);
out.push_str(replacement);
i += span;
last = i;
replaced = true;
} else {
i += 1;
}
}
out.push_str(&payload[last..]);
replaced.then_some(out)
}
pub fn mutate_unicode(payload: &str) -> Vec<String> {
let mut variants = Vec::new();
let fullwidth: String = payload
.chars()
.map(|c| {
let u = c as u32;
if (0x21..=0x7E).contains(&u) {
char::from_u32(u + 0xFEE0).unwrap_or(c)
} else if u == 0x20 {
'\u{3000}' } else {
c
}
})
.collect();
if fullwidth != payload {
variants.push(fullwidth);
}
let homoglyph: String = payload
.chars()
.map(|c| match c.to_ascii_lowercase() {
'a' => '\u{0430}', 'e' => '\u{0435}', 'o' => '\u{03BF}', 'p' => '\u{0440}', 'c' => '\u{0441}', 'x' => '\u{0445}', 'y' => '\u{0443}', 'd' => '\u{217E}', '>' => '\u{FE65}', '<' => '\u{FE64}', '\'' => '\u{02B9}', '"' => '\u{02BA}', _ => c,
})
.collect();
if homoglyph != payload {
variants.push(homoglyph);
}
collect_unique(variants)
}
pub fn mutate_all(payload: &str) -> Result<Vec<String>, EncodingError> {
let mut variants = Vec::new();
variants.extend(mutate_case(payload));
variants.extend(mutate_whitespace(payload));
variants.extend(mutate_encoding_mix(
payload,
&["url_encode", "html_entities", "unicode"],
)?);
variants.extend(mutate_null_bytes(payload));
variants.extend(mutate_sql_comments(payload));
variants.extend(mutate_html(payload));
variants.extend(mutate_unicode(payload));
Ok(collect_unique(variants))
}
fn collect_unique<I>(variants: I) -> Vec<String>
where
I: IntoIterator<Item = String>,
{
let mut seen = HashSet::new();
variants
.into_iter()
.filter(|variant| seen.insert(variant.clone()))
.collect()
}