berlin
6 TopicsGeneric iRule based on datagroup parsing
The creation of this iRule comes from a migration project from Apache configuration to F5 Big IP. Different constraints lead to this approach of storing the configuration elements from the Apache conf in a datagroup that is then parsed by this iRule to dynamically derive the rules to apply to traffic. These are some simple or complex rules, but they are all uniformly stored in the datagroup, that can be modified by non-F5 friendly persons without impacting the rest of the configuration.339Views5likes3Comments- 659Views3likes1Comment
SUPER-WEBSOCKET-HANDSHAKE-LOGGER™® (SWHL) iRule
The contest submission covers a so called SUPER-WEBSOCKET-HANDSHAKE-LOGGER™® (SWHL) iRule. The genius idea behind this iRule is to log and correlate every single WEBSOCKET-Handshakes via the WS_REQUEST and WS_RESPONSE events. The iRule uses a well-selected iRule syntax and it has been carefully tested on TMOS v16, v17 and v21 units. How to use: Save the iRule to your device. Attach it to your virtual server. Adjust the $static::super_websocket_handshake_logger(DEBUG_SOURCE) variable to match your client-ip address or client-subnet. Perform websocket request. Open your BAS and type: ~# tail -f /var/log/ltm | grep "SUPER-WEBSOCKET-HANDSHAKE-LOGGER" Enjoy the lovely iRule! when RULE_INIT { # SUPER-WEBSOCKET-HANDSHAKE-LOGGER iRule by Kai Wilke set static::super_websocket_handshake_logger(DEBUG_SOURCE) "10.11.12.0/24" ;# CIDR-Notation } when WS_REQUEST { set swl_requestID "" if { [IP::addr [IP::client_addr] equals $static::super_websocket_handshake_logger(DEBUG_SOURCE)] == 0 } then { return } set swl_requestID "[clock clicks][TMM::cmp_unit]" log -noname local0.debug "SUPER-WEBSOCKET-HANDSHAKE-LOGGER | $swl_requestID | [IP::client_addr]:[TCP::client_port] -> [IP::local_addr]:[TCP::local_port] | WS-REQUEST | [set httpRequest "[HTTP::method] [HTTP::host][HTTP::uri]"]" foreach header [HTTP::header names] { log -noname local0.debug "SUPER-WEBSOCKET-HANDSHAKE-LOGGER | $swl_requestID | [IP::client_addr]:[TCP::client_port] -> [IP::local_addr]:[TCP::local_port] | WS-REQUEST-HEADER | $header: [HTTP::header value $header]" } } when WS_RESPONSE { if { $swl_requestID eq "" } then { return } log -noname local0.debug "SUPER-WEBSOCKET-HANDSHAKE-LOGGER | $swl_requestID | [IP::local_addr]:[TCP::local_port] -> [IP::client_addr]:[TCP::client_port] | WS-RESPONSE | $httpRequest" foreach header [HTTP::header names] { log -noname local0.debug "SUPER-WEBSOCKET-HANDSHAKE-LOGGER | $swl_requestID | [IP::local_addr]:[TCP::local_port] -> [IP::client_addr]:[TCP::client_port] | WS-RESPONSE-HEADER | $header: [HTTP::header value $header]" } } Cheers, Kai178Views1like0CommentsWS-Exfil-Shield: Catching What WAFs Miss After the 101 Handshake
Problem WAFs inspect the WebSocket upgrade and individual frames against signatures and content profiles, but they do not correlate behavior across the lifetime of an established WebSocket session. All major WAF vendors document the same gap: inspection stops at the HTTP upgrade handshake; post-upgrade WebSocket frames are not correlated across a session. Three threat patterns exploit the post-upgrade behavioral blind spot: **C2 Beacon Timing**: WebSocket C2 channels are documented in active campaigns — e.g. PhantomCaptcha (SentinelLabs, Oct 2025) used a multi-stage WebSocket RAT with wss:// C2 and Base64/JSON commands; LightSpy (Huntress, macOS variant) uses WebSockets for command delivery and control. The behavioral signal is timing — beaconing implants tend toward regular intervals, humans do not. WAFs and most network controls do not analyze inter-frame timing across a session. **Credential Stuffing Over WebSocket**: 1000 credential pairs over one connection appear as one HTTP event to perimeter controls. Verizon DBIR 2025: compromised credentials were the initial access vector in 22% of breaches; the median daily share of credential stuffing in SSO authentication logs was 19%. **Exfiltration Signals**: /export paths, Authorization headers, and oversized payloads are visible at the handshake; per-frame inspection (where enabled) sees content but not session-level patterns. BSI Lagebericht 2025 (reporting period July 2024 - June 2025): 72% of analyzed ransomware incidents included a data leak; double extortion (encryption + exfiltration) is the dominant attack model. Solution Single iRule. No backend changes. Two-stage behavioral detection: not "what does this frame contain?" but "what does this connection do over time? — and does the payload confirm it?" L1 Suspicious URI/header regex + string BWC throttle + HSL alert L2 C2 beacon timing (CoV) online statistics Sideband check → quarantine/pass + close L3 High frame rate sliding window IP block + TCP close L4 Quarantined reconnect sideband verdict Block/release/honeypot + AI analysis **L1 - Exfiltration signals at the handshake**: HTTP_REQUEST checks the upgrade URI against a regex for known exfiltration endpoints (/export, /download, /dump, /backup, /extract) and scans headers for Authorization, X-API-Key, X-Secret. On match: BWC policy attached server-to-client (1 Mbps throttle) + HSL alert. No block — /export might be legitimate. Throttling buys the SOC time to investigate without disrupting a potentially valid operation. **L2 - CoV² online algorithm**: Welford-inspired, 5 table entries per connection regardless of session length. CoV² (no sqrt() in BIG-IP Tcl) < 0.0225 with >=5 samples = machine-like timing. On detection: iRule sends timing metadata to the sideband service and waits up to 500ms for a verdict. FALSE_POSITIVE (allowlisted IP) → session continues untouched; CONFIRMED or timeout → quarantine table set + TCP close. The quarantined IP will be routed to the honeypot on its next connection attempt (L4). **L3 - Frame rate sliding window**: WS_CLIENT_FRAME tracks frame count per connection within a 10-second window. At 5 frames in 10 seconds: TCP close + IP written to blocklist with 1-hour TTL. On any subsequent reconnect attempt, HTTP_REQUEST rejects the connection immediately. The sliding window resets when the window expires, allowing legitimate high-frequency bursts to pass without false positives. **L4 - Two-stage verification with closed-loop AI verdict**: When a quarantined IP reconnects, HTTP_REQUEST issues a QUARANTINE_CHECK to the sideband service before routing. Three outcomes apply at handshake time: PENDING (analysis still in progress) or sideband timeout → connection is silently routed to the honeypot pool via `pool quarantine_pool`. The attacker's implant keeps running, unaware it is isolated. After 5 frames are collected in the honeypot, the AI analyzer (Claude) classifies payload semantics independent of timing: agent identifiers, command structure, encoding patterns. Claude's verdict is pushed back to the sideband service and cached against the source IP, which closes the loop: CONFIRMED → next QUARANTINE_CHECK returns CONFIRMED, iRule emits a C2_CONFIRMED HSL event (source=claude, confidence, family) and rejects the handshake; the IP is held in a permanent 24h block. In the test run, the third reconnect after CONFIRMED never completes the WS upgrade — the client sees `InvalidMessage: did not receive a valid HTTP response`. FALSE_POSITIVE → quarantine entry deleted, the IP is released and subsequent sessions continue normally. This matters because C2 frameworks implement jitter — randomized beacon intervals designed to defeat timing-based detection. At >27% jitter (in our test corpus), CoV rises above threshold and L2 stops firing; the AI layer is jitter-immune because it inspects payload semantics, not cadence. Neither signal alone is sufficient. Reference result on a `{"t":"ping","id":"c2agent01"}` corpus: verdict=C2, confidence=0.95, family "Generic C2 Heartbeat", with indicators including "structured JSON protocol with type field", "persistent agent identifier across all frames", "repetitive ping pattern (5/5 frames identical)", "no human interaction artifacts", and "deterministic payload, no entropy". **Sideband**: iRule = sensor, endpoint = actor. Used at two points in the flow: L2 (timing verdict) and L4 (QUARANTINE_CHECK). Pluggable TCP port 9000 listener: SIEM (Splunk/QRadar), SOAR (auto-block via iControl REST), or AI analyzer (reference implementation included). catch{} ensures a non-responding endpoint never delays traffic. Impact - Defense in depth with Advanced WAF: WAF guards handshake, signatures, and frame content; WS-Exfil-Shield adds session-level behavioral detection. - All thresholds in RULE_INIT — tuning without redeployment; sideband endpoint swappable. - Graduated enforcement: throttle → quarantine → AI verify → block or release. Each layer independently tunable. - Full audit trail: CONNECT, L1_SIGNAL, C2_BEACON, QUARANTINE, RATE_LIMIT, BLOCKED, C2_CONFIRMED (source=claude, confidence, family) + FALSE_POSITIVE from AI layer. Demo https://www.youtube.com/watch?v=-XRipP0p_oc Code # WS-Exfil-Shield iRule # F5 AppWorld Berlin 2026 - iRules Contest # # Four-layer WebSocket security with graduated response: # Layer 1: Connection-level exfiltration signals (URL, headers) → BWC throttle # Layer 2: C2 beacon timing fingerprint (CoV-based behavioral analysis) → quarantine + TCP close # Layer 3: High-frequency frame rate detection (credential stuffing) → TCP close + IP block # Layer 4: Quarantined reconnect → sideband verdict check → block/release/honeypot # # Two-stage verification: # Stage 1 (L2): CoV² detects machine-like timing → sideband confirms → quarantine + TCP close # Stage 2 (L4): Reconnect → sideband QUARANTINE_CHECK returns Claude payload verdict: # CONFIRMED → permanent 24h block + C2_CONFIRMED HSL event + reject # FALSE_POSITIVE → quarantine released, session continues normally # PENDING/timeout → route to honeypot (Claude still analyzing) # # External dependencies (pre-configured on BIG-IP): # - BWC policy : ws_exfil_throttle (Network > Bandwidth Controllers, 1 Mbps) # - HSL pool : siem_hsl_pool (LTM > Pools, UDP 514, points to SIEM/syslog receiver) # # Requirements: BIG-IP TMOS 21.x when RULE_INIT { set static::beacon_min_samples 5 set static::beacon_cv_threshold 0.15 ;# CoV < 0.15 = machine-like timing set static::cs_frame_limit 5 ;# max frames per cs_window milliseconds set static::cs_window 3000 ;# sliding window size in milliseconds set static::cs_block_ttl 3600 ;# IP blocklist TTL in seconds set static::bwc_policy "ws_exfil_throttle" } # --------------------------------------------------------------------------- # PROCEDURES # --------------------------------------------------------------------------- proc check_beacon_fingerprint { conn_id } { set count [table lookup "bcn_count_${conn_id}"] if { $count eq "" || $count < $static::beacon_min_samples } { return 0 } set sum_t [table lookup "bcn_sumt_${conn_id}"] set sum_t2 [table lookup "bcn_sumt2_${conn_id}"] set n $count set mean [expr { double($sum_t) / $n }] if { $mean <= 0 } { return 0 } set variance [expr { double($sum_t2) / $n - $mean * $mean }] if { $variance < 0 } { set variance 0 } # CoV² comparison avoids sqrt (not available in BIG-IP Tcl) set cov_sq [expr { $variance / ($mean * $mean) }] set cv_thresh_sq [expr { $static::beacon_cv_threshold * $static::beacon_cv_threshold }] if { $cov_sq < $cv_thresh_sq } { return 1 } return 0 } proc hsl_send { event data } { HSL::send $static::hsl "\{\"event\":\"${event}\",\"ts\":[clock seconds],${data}\}\n" } # --------------------------------------------------------------------------- # EVENTS # --------------------------------------------------------------------------- when HTTP_REQUEST { if { [string tolower [HTTP::header "Upgrade"]] eq "websocket" } { if { ![info exists static::hsl] } { set static::hsl [HSL::open -proto UDP -pool siem_hsl_pool] } set conn_id "[IP::client_addr]:[TCP::client_port]" set client_ip [IP::client_addr] set uri [HTTP::uri] # Blocklist check (Layer 3 + confirmed C2 carry-over) if { [table lookup "cs_blocked_${client_ip}"] ne "" } { log local0.warning "WS-Exfil-Shield: BLOCKED ip=$client_ip conn=$conn_id" call hsl_send "BLOCKED" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\"" reject return } # Layer 4: Quarantined IP reconnect — check sideband for Claude verdict. # CONFIRMED: Claude analyzed honeypot frames and confirmed C2 → permanent block. # FALSE_POSITIVE: Claude found no C2 indicators → release quarantine, continue normally. # PENDING/timeout: Claude still analyzing → keep routing to honeypot. if { [table lookup "quar_${client_ip}"] ne "" } { set quar_action "honeypot" catch { set sb [connect -timeout 100 -protocol TCP 10.10.2.1 9000] if { $sb ne "" } { send -timeout 100 $sb "{\"conn_id\":\"$conn_id\",\"ip\":\"$client_ip\",\"threat\":\"QUARANTINE_CHECK\"}\n" set qverdict [recv -timeout 500 $sb] close $sb if { [string match "*\"verdict\":\"CONFIRMED\"*" $qverdict] } { set quar_action "block" } elseif { [string match "*\"verdict\":\"FALSE_POSITIVE\"*" $qverdict] } { set quar_action "release" } } } if { $quar_action eq "block" } { table set "cs_blocked_${client_ip}" 1 86400 86400 log local0.warning "WS-Exfil-Shield: C2_CONFIRMED ip=$client_ip conn=$conn_id" call hsl_send "C2_CONFIRMED" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\"" reject return } elseif { $quar_action eq "release" } { table delete "quar_${client_ip}" log local0.info "WS-Exfil-Shield: FALSE_POSITIVE ip=$client_ip conn=$conn_id" call hsl_send "FALSE_POSITIVE" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\"" # fall through to normal processing } else { log local0.info "WS-Exfil-Shield: QUARANTINE ip=$client_ip conn=$conn_id" call hsl_send "QUARANTINE" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\"" pool quarantine_pool return } } # Layer 1: Exfiltration signals in WebSocket upgrade request set threat "" if { [regexp -nocase {/(export|download|dump|backup|extract)} $uri] } { set threat "EXFIL_ENDPOINT" } if { $threat eq "" } { foreach hdr { Authorization X-API-Key X-Secret } { if { [HTTP::header $hdr] ne "" } { set threat "SENSITIVE_HEADER"; break } } } if { $threat ne "" } { log local0.warning "WS-Exfil-Shield: L1_SIGNAL threat=$threat ip=$client_ip uri=$uri" call hsl_send "L1_SIGNAL" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\",\"threat\":\"$threat\",\"uri\":\"$uri\"" # Throttle server→client bandwidth to slow active exfiltration BWC::policy attach $static::bwc_policy } table set "ws_start_${conn_id}" [clock clicks -milliseconds] indef 3600 log local0.info "WS-Exfil-Shield: CONNECT ip=$client_ip conn=$conn_id uri=$uri" call hsl_send "CONNECT" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\",\"uri\":\"$uri\"" } } when WS_CLIENT_FRAME { set conn_id "[IP::client_addr]:[TCP::client_port]" set client_ip [IP::client_addr] set now [clock clicks -milliseconds] # --- Layer 2: C2 Beacon Timing Fingerprint --- set last_ts [table lookup "bcn_last_${conn_id}"] if { $last_ts ne "" } { set interval [expr { $now - $last_ts }] set count [table lookup "bcn_count_${conn_id}"] set sum_t [table lookup "bcn_sumt_${conn_id}"] set sum_t2 [table lookup "bcn_sumt2_${conn_id}"] if { $count eq "" } { set count 0 } if { $sum_t eq "" } { set sum_t 0 } if { $sum_t2 eq "" } { set sum_t2 0 } if { $interval > 0 } { incr count set sum_t [expr { $sum_t + $interval }] set sum_t2 [expr { $sum_t2 + $interval * $interval }] table set "bcn_count_${conn_id}" $count indef 3600 table set "bcn_sumt_${conn_id}" $sum_t indef 3600 table set "bcn_sumt2_${conn_id}" $sum_t2 indef 3600 } if { [call check_beacon_fingerprint $conn_id] } { set mean_interval [expr { $sum_t / $count }] log local0.warning "WS-Exfil-Shield: C2_BEACON ip=$client_ip conn=$conn_id samples=$count mean_interval=${mean_interval}ms" call hsl_send "C2_BEACON" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\",\"samples\":$count,\"mean_interval_ms\":$mean_interval" # Stage 1 sideband: timing verdict determines quarantine vs pass # CONFIRMED/timeout → quarantine (honeypot collects payload for Stage 2 Claude analysis) # FALSE_POSITIVE → session continues untouched set action "quarantine" catch { set sb [connect -timeout 100 -protocol TCP 10.10.2.1 9000] if { $sb ne "" } { send -timeout 100 $sb "{\"conn_id\":\"$conn_id\",\"ip\":\"$client_ip\",\"threat\":\"C2_BEACON\",\"mean_interval_ms\":$mean_interval}\n" set verdict [recv -timeout 500 $sb] close $sb if { [string match "*\"verdict\":\"FALSE_POSITIVE\"*" $verdict] } { set action "pass" } } } if { $action eq "quarantine" } { table set "quar_${client_ip}" 1 indef 1800 TCP::close } # action "pass": allowlisted IP — session continues untouched return } } table set "bcn_last_${conn_id}" $now indef 3600 # --- Layer 3: High-frequency frame rate (credential stuffing indicator) --- set window_start [table lookup "cs_window_${conn_id}"] set frame_count [table lookup "cs_frames_${conn_id}"] if { $window_start eq "" } { set window_start $now table set "cs_window_${conn_id}" $now indef 3600 } if { $frame_count eq "" } { set frame_count 0 } set elapsed [expr { $now - $window_start }] if { $elapsed >= $static::cs_window } { table set "cs_window_${conn_id}" $now indef 3600 table set "cs_frames_${conn_id}" 1 indef 3600 } else { incr frame_count table set "cs_frames_${conn_id}" $frame_count indef 3600 if { $frame_count >= $static::cs_frame_limit } { log local0.warning "WS-Exfil-Shield: RATE_LIMIT ip=$client_ip conn=$conn_id frames=${frame_count} in ${elapsed}ms" call hsl_send "RATE_LIMIT" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\",\"frames\":$frame_count,\"elapsed_ms\":$elapsed" table set "cs_blocked_${client_ip}" 1 $static::cs_block_ttl $static::cs_block_ttl TCP::close return } } }299Views1like1Comment