mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 14:37:01 -07:00
fix(scan): baseline-diff lfi evidence to stop static-content false positives (#303)
the lfi scan matched evidence regexes against each response with no baseline, so a page whose static content matched a pattern (a blog containing an /etc/passwd-shaped line, a code sample with <?php) was reported as an lfi finding for every parameter and payload combination. take one baseline get of the unmodified target and exclude, per evidence class, any pattern already present in it, so only payload-caused disclosures are counted.
This commit is contained in:
+23
-2
@@ -170,6 +170,25 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
|
||||
|
||||
log.Info("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
|
||||
|
||||
// baseline the unmodified target so evidence-like content already present
|
||||
// on every response (a blog post about /etc/passwd, a "<?php" snippet) isn't
|
||||
// reported as an LFI hit for every param/payload combination.
|
||||
baselineEvidence := make(map[string]bool)
|
||||
if baseReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, targetURL, http.NoBody); err == nil {
|
||||
if baseResp, err := client.Do(baseReq); err == nil {
|
||||
baseBody, err := io.ReadAll(io.LimitReader(baseResp.Body, 1024*100))
|
||||
baseResp.Body.Close()
|
||||
if err == nil {
|
||||
baseBodyStr := string(baseBody)
|
||||
for _, evidence := range lfiEvidencePatterns {
|
||||
if evidence.pattern.MatchString(baseBodyStr) {
|
||||
baselineEvidence[evidence.description] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create work items
|
||||
type workItem struct {
|
||||
param string
|
||||
@@ -236,8 +255,10 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
|
||||
// check for evidence patterns
|
||||
for _, evidence := range lfiEvidencePatterns {
|
||||
match := evidence.pattern.FindString(bodyStr)
|
||||
// our own payload echoed back isn't proof of inclusion
|
||||
if match != "" && !strings.Contains(item.payload.payload, match) {
|
||||
// our own payload echoed back isn't proof of inclusion, and
|
||||
// evidence already present in the unmodified baseline means
|
||||
// the content is static, not caused by this payload
|
||||
if match != "" && !strings.Contains(item.payload.payload, match) && !baselineEvidence[evidence.description] {
|
||||
key := item.param + "|" + item.payload.payload
|
||||
mu.Lock()
|
||||
if seen[key] {
|
||||
|
||||
@@ -337,6 +337,65 @@ func TestLFI_ReflectedPayloadIsNotEvidence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFI_StaticPageContentIsNotEvidence(t *testing.T) {
|
||||
// a page that always returns the same body regardless of query params,
|
||||
// e.g. a blog post explaining /etc/passwd format. the content matches
|
||||
// the evidence pattern for every single request, not because a payload
|
||||
// caused a file to be included.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "<html>example: root:x:0:0:root:/root:/bin/bash explained here</html>")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
result, err := LFI(srv.URL, 5*time.Second, 4, "")
|
||||
if err != nil {
|
||||
t.Fatalf("LFI: %v", err)
|
||||
}
|
||||
if result != nil && len(result.Vulnerabilities) > 0 {
|
||||
t.Errorf("static page content present regardless of payload should not be flagged as LFI, got %d hits (e.g. %+v)", len(result.Vulnerabilities), result.Vulnerabilities[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFI_BaselinePatternDoesNotPoisonOtherPatterns(t *testing.T) {
|
||||
// baseline carries a static php snippet, but the passwd leak only appears
|
||||
// under a traversal payload. suppression must be per evidence class, not global.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body := "<html><?php // shared static snippet ?></html>"
|
||||
for _, vs := range r.URL.Query() {
|
||||
for _, v := range vs {
|
||||
if strings.Contains(v, "etc/passwd") {
|
||||
body += "\nroot:x:0:0:root:/root:/bin/bash"
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprint(w, body)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
result, err := LFI(srv.URL, 5*time.Second, 4, "")
|
||||
if err != nil {
|
||||
t.Fatalf("LFI: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("passwd disclosure under payload should still be detected despite a static php baseline")
|
||||
}
|
||||
var sawPasswd, sawPHP bool
|
||||
for _, v := range result.Vulnerabilities {
|
||||
switch v.Evidence {
|
||||
case "/etc/passwd content":
|
||||
sawPasswd = true
|
||||
case "PHP source code":
|
||||
sawPHP = true
|
||||
}
|
||||
}
|
||||
if !sawPasswd {
|
||||
t.Errorf("passwd leak (evidence class absent from baseline) should still fire, got %+v", result.Vulnerabilities)
|
||||
}
|
||||
if sawPHP {
|
||||
t.Errorf("static php present in baseline should be suppressed, got %+v", result.Vulnerabilities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFI_GenuineBase64PHPStillDetected(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.URL.Query().Get("file"), "convert.base64-encode") {
|
||||
|
||||
Reference in New Issue
Block a user