use super::ProbeContext;
use std::collections::HashMap;
pub fn extract_evasion_features(ctx: &ProbeContext, features: &mut HashMap<String, f64>) {
let body_lower = ctx.response.body.to_lowercase();
let payload_reflected = ctx.response.body.contains(&ctx.probe_payload);
if ctx.raw_payload_blocked {
if let Some(ref encoding) = ctx.encoding_used {
let succeeded = payload_reflected
|| (ctx.response.status < 400 && ctx.response.status != ctx.baseline.status)
|| ctx.response.body_bytes != ctx.baseline.body_bytes;
if succeeded {
match encoding.as_str() {
"double_url" => {
features.insert("evasion:double_url_encode_bypass".into(), 1.0);
}
"unicode" => {
features.insert("evasion:unicode_normalize_bypass".into(), 1.0);
}
"html_entity" => {
features.insert("evasion:html_entity_bypass".into(), 1.0);
}
"hex" => {
features.insert("evasion:hex_encode_bypass".into(), 1.0);
}
"base64" => {
features.insert("evasion:base64_encode_bypass".into(), 1.0);
}
"overlong_utf8" => {
features.insert("evasion:overlong_utf8_bypass".into(), 1.0);
}
_ => {}
}
}
}
}
if ctx.raw_payload_blocked {
if let Some(ref encoding) = ctx.encoding_used {
let succeeded = ctx.response.status < 400
|| payload_reflected
|| ctx.response.body_bytes != ctx.baseline.body_bytes;
if succeeded {
match encoding.as_str() {
"case_variation" => {
features.insert("evasion:case_variation_bypass".into(), 1.0);
}
"comment_insertion" => {
features.insert("evasion:comment_insertion_bypass".into(), 1.0);
}
"whitespace_variation" => {
features.insert("evasion:whitespace_variation_bypass".into(), 1.0);
}
"null_byte" => {
features.insert("evasion:null_byte_bypass".into(), 1.0);
}
"concatenation" => {
features.insert("evasion:concatenation_bypass".into(), 1.0);
}
"alternative_syntax" => {
features.insert("evasion:alternative_syntax_bypass".into(), 1.0);
}
_ => {}
}
}
}
}
let server = ctx
.response
.headers
.get("server")
.map(|s| s.to_lowercase())
.unwrap_or_default();
let has_waf = server.contains("cloudflare")
|| server.contains("imperva")
|| server.contains("sucuri")
|| server.contains("barracuda")
|| server.contains("fortiweb")
|| ctx.response.headers.contains_key("x-sucuri-id")
|| ctx.response.headers.contains_key("x-cdn")
|| body_lower.contains("modsecurity")
|| body_lower.contains("cloudflare")
|| body_lower.contains("akamai")
|| body_lower.contains("incapsula");
if has_waf {
features.insert("evasion:waf_fingerprinted".into(), 1.0);
}
if ctx.raw_payload_blocked && !payload_reflected && ctx.response.status < 400 {
if ctx.encoding_used.is_some() && ctx.response.body_bytes != ctx.baseline.body_bytes {
features.insert("evasion:waf_rule_gap_found".into(), 1.0);
}
}
if ctx.raw_payload_blocked && ctx.probe_payload.len() < 20 && ctx.response.status < 400 {
features.insert("evasion:waf_threshold_bypass".into(), 1.0);
}
if ctx.raw_payload_blocked {
let method = ctx.request_method.to_uppercase();
if (method == "PUT" || method == "PATCH" || method == "DELETE")
&& ctx.response.status < 400
{
features.insert("evasion:waf_method_bypass".into(), 1.0);
}
}
if let Some(ref headers) = ctx.request_headers {
if ctx.raw_payload_blocked {
if let Some(ct) = headers.get("content-type") {
let ct_lower = ct.to_lowercase();
if (ct_lower.contains("application/xml")
|| ct_lower.contains("multipart/form-data"))
&& ctx.response.status < 400
{
features.insert("evasion:waf_content_type_bypass".into(), 1.0);
}
}
}
}
if let Some(ref headers) = ctx.request_headers {
if ctx.raw_payload_blocked {
if let Some(te) = headers.get("transfer-encoding") {
if te.to_lowercase().contains("chunked") && ctx.response.status < 400 {
features.insert("evasion:waf_chunked_bypass".into(), 1.0);
}
}
}
}
if ctx.raw_payload_blocked && ctx.encoding_used.is_some() {
if !payload_reflected && ctx.response.status == ctx.baseline.status {
features.insert("evasion:bypass_not_exploitable".into(), 1.0);
}
}
if ctx.response.status == 400 || ctx.response.status == 422 {
if body_lower.contains("invalid")
|| body_lower.contains("validation")
|| body_lower.contains("must be")
|| body_lower.contains("required field")
{
features.insert("evasion:filter_is_application_logic".into(), 1.0);
}
}
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
use super::*;
#[test]
fn test_waf_fingerprinted_cloudflare() {
let mut response = make_response("Attention Required! | Cloudflare", 403);
response
.headers
.insert("server".to_string(), "cloudflare".to_string());
let ctx = make_ctx("sqli", "'", response);
let mut features = HashMap::new();
extract_evasion_features(&ctx, &mut features);
assert!(features.contains_key("evasion:waf_fingerprinted"));
}
#[test]
fn test_double_url_encode_bypass() {
let response = make_response("SQL error near '%27'", 500);
let mut ctx = make_ctx("sqli", "%2527", response);
ctx.raw_payload_blocked = true;
ctx.encoding_used = Some("double_url".to_string());
let mut features = HashMap::new();
extract_evasion_features(&ctx, &mut features);
assert!(features.contains_key("evasion:double_url_encode_bypass"));
}
#[test]
fn test_no_bypass_without_blocked() {
let response = make_response("OK", 200);
let mut ctx = make_ctx("sqli", "'", response);
ctx.encoding_used = Some("double_url".to_string());
ctx.raw_payload_blocked = false;
let mut features = HashMap::new();
extract_evasion_features(&ctx, &mut features);
assert!(!features.contains_key("evasion:double_url_encode_bypass"));
}
#[test]
fn test_filter_is_application_logic() {
let response = make_response("Validation error: id must be a number", 422);
let ctx = make_ctx("sqli", "'", response);
let mut features = HashMap::new();
extract_evasion_features(&ctx, &mut features);
assert!(features.contains_key("evasion:filter_is_application_logic"));
}
#[test]
fn test_waf_method_bypass() {
let response = make_response("data", 200);
let mut ctx = make_ctx("sqli", "' OR 1=1--", response);
ctx.raw_payload_blocked = true;
ctx.request_method = "PUT".to_string();
let mut features = HashMap::new();
extract_evasion_features(&ctx, &mut features);
assert!(features.contains_key("evasion:waf_method_bypass"));
}
#[test]
fn test_bypass_not_exploitable() {
let response = make_response("<html><body>Normal page</body></html>", 200);
let mut ctx = make_ctx("sqli", "' OR 1=1--", response);
ctx.raw_payload_blocked = true;
ctx.encoding_used = Some("unicode".to_string());
let mut features = HashMap::new();
extract_evasion_features(&ctx, &mut features);
assert!(features.contains_key("evasion:bypass_not_exploitable"));
}
}