diff --git a/internal/scan/frameworks/detectors/waf.go b/internal/scan/frameworks/detectors/waf.go new file mode 100644 index 00000000..5412eae2 --- /dev/null +++ b/internal/scan/frameworks/detectors/waf.go @@ -0,0 +1,134 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +/* + + BSD 3-Clause License + (c) 2022-2026 vmfunc, xyzeva & contributors + +*/ + +package detectors + +import ( + "net/http" + + fw "github.com/vmfunc/sif/internal/scan/frameworks" +) + +// The detectors here identify the WAF in front of a target. Each keys on a +// header, Set-Cookie name, or block-page string the WAF itself sets, so a page +// that merely names the vendor in prose does not trip it. +func init() { + fw.Register(&cloudflareWAFDetector{}) + fw.Register(&sucuriDetector{}) + fw.Register(&incapsulaDetector{}) + fw.Register(&bigipASMDetector{}) + fw.Register(&modSecurityDetector{}) +} + +// cloudflareWAFDetector detects an active Cloudflare WAF or bot challenge via the +// cf-mitigated header and __cf_chl_ tokens, which appear only on an interception +// response (plain proxying is covered by the cf-ray CDN marker). +type cloudflareWAFDetector struct{} + +func (d *cloudflareWAFDetector) Name() string { return "Cloudflare WAF" } + +func (d *cloudflareWAFDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "cf-mitigated", Weight: 0.6, HeaderOnly: true}, + {Pattern: "__cf_chl_", Weight: 0.5}, + {Pattern: "Attention Required! | Cloudflare", Weight: 0.4}, + } +} + +func (d *cloudflareWAFDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} + +// sucuriDetector detects the Sucuri (GoDaddy) cloud WAF via its x-sucuri-id and +// x-sucuri-cache headers, backed by the block-page string. +type sucuriDetector struct{} + +func (d *sucuriDetector) Name() string { return "Sucuri WAF" } + +func (d *sucuriDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "x-sucuri-id", Weight: 0.6, HeaderOnly: true}, + {Pattern: "x-sucuri-cache", Weight: 0.4, HeaderOnly: true}, + {Pattern: "Sucuri WebSite Firewall", Weight: 0.5}, + } +} + +func (d *sucuriDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} + +// incapsulaDetector detects the Imperva Incapsula cloud WAF via its x-iinfo +// header, incap_ses_ Set-Cookie name, and block-page incident string. Each is +// Incapsula-exclusive and clears the threshold on its own. +type incapsulaDetector struct{} + +func (d *incapsulaDetector) Name() string { return "Imperva Incapsula" } + +func (d *incapsulaDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "x-iinfo", Weight: 0.6, HeaderOnly: true}, + {Pattern: "incap_ses_", Weight: 0.5, HeaderOnly: true}, + {Pattern: "Incapsula incident ID", Weight: 0.5}, + } +} + +func (d *incapsulaDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} + +// bigipASMDetector detects the F5 BIG-IP Application Security Manager via its +// block-page rejection sentence and support-id line. Block-page only, so it never +// fires on a passing response. +type bigipASMDetector struct{} + +func (d *bigipASMDetector) Name() string { return "F5 BIG-IP ASM" } + +func (d *bigipASMDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "The requested URL was rejected. Please consult with your administrator.", Weight: 0.6}, + {Pattern: "Your support ID is:", Weight: 0.3}, + } +} + +func (d *bigipASMDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} + +// modSecurityDetector detects ModSecurity via its generated block page (primary) +// and the Mod_Security/NOYB server-token values (supporting, since suppressible). +type modSecurityDetector struct{} + +func (d *modSecurityDetector) Name() string { return "ModSecurity" } + +func (d *modSecurityDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "This error was generated by Mod_Security", Weight: 0.6}, + {Pattern: "Mod_Security", Weight: 0.3, HeaderOnly: true}, + {Pattern: "NOYB", Weight: 0.2, HeaderOnly: true}, + } +} + +func (d *modSecurityDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + return sigmoidConfidence(base.MatchSignatures(body, headers)), "" +} diff --git a/internal/scan/frameworks/detectors/waf_test.go b/internal/scan/frameworks/detectors/waf_test.go new file mode 100644 index 00000000..3f42b208 --- /dev/null +++ b/internal/scan/frameworks/detectors/waf_test.go @@ -0,0 +1,80 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package detectors + +import ( + "net/http" + "testing" + + fw "github.com/vmfunc/sif/internal/scan/frameworks" +) + +// TestWAFDetectors_Positive fires each WAF detector on a marker shaped like the +// real header, Set-Cookie, or block-page body that WAF emits. +func TestWAFDetectors_Positive(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + headers http.Header + }{ + {"Cloudflare mitigated header", &cloudflareWAFDetector{}, "", http.Header{"Cf-Mitigated": {"challenge"}}}, + {"Cloudflare challenge page", &cloudflareWAFDetector{}, `
This error was generated by Mod_Security.
`, http.Header{}}, + {"ModSecurity server token", &modSecurityDetector{}, `This error was generated by Mod_Security.`, http.Header{"Server": {"Apache Mod_Security"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, tt.headers) + if conf <= 0.5 { + t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf) + } + }) + } +} + +// TestWAFDetectors_Negative keeps the detectors quiet on prose that names the +// vendor and on unrelated proxy headers, so a mention or a coincidental header +// cannot trip a WAF finding. +func TestWAFDetectors_Negative(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + headers http.Header + }{ + {"Cloudflare plain proxy", &cloudflareWAFDetector{}, "", http.Header{"Cf-Ray": {"8a1-EWR"}}}, + {"Cloudflare prose", &cloudflareWAFDetector{}, `We compared Cloudflare and Fastly for WAF coverage.
`, http.Header{}}, + {"Sucuri prose", &sucuriDetector{}, `Sucuri is a popular website firewall.
`, http.Header{}}, + {"Incapsula prose", &incapsulaDetector{}, `Imperva Incapsula competes with Cloudflare.
`, http.Header{}}, + {"F5 unrelated body", &bigipASMDetector{}, `Our support team can help with your F5 BIG-IP setup.
`, http.Header{}}, + {"ModSecurity prose", &modSecurityDetector{}, `We enabled ModSecurity rules on the origin.
`, http.Header{}}, + {"plain nginx", &sucuriDetector{}, "", http.Header{"Server": {"nginx/1.25.3"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, tt.headers) + if conf > 0.5 { + t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf) + } + }) + } +}