mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-12 11:01:24 -07:00
839c0a779c
four recon-flagged bugs, each with a focused test:
- dnslist fired both http and https per candidate and counted a "found"
on any non-error response (incl 404 and wildcard catch-all redirects),
so every host double-counted and a wildcard-dns host flooded results.
probe http then https with per-subdomain dedupe, gate on a meaningful
(2xx, non-redirect) status, and stop chasing redirects so a catch-all
301 reads as a redirect instead of a 200.
- fetchRobotsTXT recursed on every 301 Location with no depth limit and
no visited set, so an A->B->A loop blew the stack. bound it to a named
hop cap and a visited set, iteratively.
- framework cve lookup used best.version ("unknown" when the detector
only fingerprints the framework) and threw away the version
ExtractVersionOptimized dug out of the body, missing CVEs. reconcile
via resolveVersion, preferring the extracted concrete version.
- subdomain takeover flagged a dangling cname whenever a no-such-host
coincided with ANY cname (LookupCNAME echoes the host back for plain A
records). only flag when the cname points off-host at a known
takeoverable provider.
202 lines
6.2 KiB
Go
202 lines
6.2 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
package scan
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"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"
|
|
)
|
|
|
|
// dnsURL is a var so integration tests can repoint it at a fixture.
|
|
var dnsURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dnslist/"
|
|
|
|
// dnsTransport is a var so integration tests can route the per-host probes at a
|
|
// local server instead of resolving real DNS. nil keeps http.DefaultTransport.
|
|
var dnsTransport http.RoundTripper
|
|
|
|
const (
|
|
dnsSmallFile = "subdomains-100.txt"
|
|
dnsMediumFile = "subdomains-1000.txt"
|
|
dnsBigFile = "subdomains-10000.txt"
|
|
)
|
|
|
|
// dnsScheme labels which url won a subdomain so we don't probe the second
|
|
// scheme once the first already counted it.
|
|
type dnsScheme string
|
|
|
|
const (
|
|
dnsSchemeHTTP dnsScheme = "http"
|
|
dnsSchemeHTTPS dnsScheme = "https"
|
|
)
|
|
|
|
// meaningfulStatus reports whether a probe response is a real "this host
|
|
// exists" signal rather than a 404 or a wildcard catch-all redirect. a
|
|
// wildcard-DNS host answers every candidate with the same redirect/404, so
|
|
// gating on a successful, non-redirect status keeps it from flooding results.
|
|
func meaningfulStatus(code int) bool {
|
|
return code >= http.StatusOK && code < http.StatusMultipleChoices
|
|
}
|
|
|
|
// Dnslist performs DNS subdomain enumeration on the target domain.
|
|
func Dnslist(size string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
|
log := output.Module("DNS")
|
|
log.Start()
|
|
|
|
var list string
|
|
switch size {
|
|
case "small":
|
|
list = dnsURL + dnsSmallFile
|
|
case "medium":
|
|
list = dnsURL + dnsMediumFile
|
|
case "large":
|
|
list = dnsURL + dnsBigFile
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, list, http.NoBody)
|
|
if err != nil {
|
|
log.Error("Error creating request: %s", err)
|
|
return nil, err
|
|
}
|
|
resp, err := httpx.Client(timeout).Do(req)
|
|
if err != nil {
|
|
log.Error("Error downloading DNS list: %s", err)
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var dns []string
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
scanner.Split(bufio.ScanLines)
|
|
for scanner.Scan() {
|
|
dns = append(dns, scanner.Text())
|
|
}
|
|
|
|
sanitizedURL := stripScheme(url)
|
|
|
|
if logdir != "" {
|
|
if err := logger.WriteHeader(sanitizedURL, logdir, size+" subdomain fuzzing"); err != nil {
|
|
log.Error("Error creating log file: %v", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// per-host probe client. dnsTransport pins every dial at a fixture in
|
|
// integration tests; nil keeps the shared transport for real runs.
|
|
client := httpx.Client(timeout)
|
|
if dnsTransport != nil {
|
|
client.Transport = dnsTransport
|
|
}
|
|
// don't chase redirects: a wildcard catch-all that 301s every candidate to
|
|
// the same landing page must read as a redirect status, not a 200, so it
|
|
// gets gated out instead of counting as a found host.
|
|
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
|
|
progress := output.NewProgress(len(dns), "enumerating")
|
|
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
wg.Add(threads)
|
|
|
|
urls := make([]string, 0, 64)
|
|
for thread := 0; thread < threads; thread++ {
|
|
go func(thread int) {
|
|
defer wg.Done()
|
|
|
|
for i, domain := range dns {
|
|
if i%threads != thread {
|
|
continue
|
|
}
|
|
|
|
progress.Increment(domain)
|
|
|
|
charmlog.Debugf("Looking up: %s", domain)
|
|
|
|
// probe http first, then https - but a subdomain is recorded at
|
|
// most once. firing both schemes and appending on each is what
|
|
// double-counted every host on the old path.
|
|
host := domain + "." + sanitizedURL
|
|
foundURL, scheme := probeSubdomain(client, host)
|
|
if foundURL == "" {
|
|
continue
|
|
}
|
|
|
|
mu.Lock()
|
|
urls = append(urls, foundURL)
|
|
mu.Unlock()
|
|
|
|
progress.Pause()
|
|
log.Success("found: %s [%s]", output.Highlight.Render(host), scheme)
|
|
progress.Resume()
|
|
|
|
if logdir != "" {
|
|
_ = logger.Write(sanitizedURL, logdir, fmt.Sprintf("[%s] %s\n", scheme, host))
|
|
}
|
|
}
|
|
}(thread)
|
|
}
|
|
wg.Wait()
|
|
progress.Done()
|
|
|
|
log.Complete(len(urls), "found")
|
|
|
|
return urls, nil
|
|
}
|
|
|
|
// probeSubdomain tries http then https for one host and returns the resolved
|
|
// url + winning scheme on the first meaningful hit, or "" if neither scheme
|
|
// gave a real signal. trying https only when http didn't already count is the
|
|
// per-subdomain dedupe.
|
|
func probeSubdomain(client *http.Client, host string) (string, dnsScheme) {
|
|
schemes := []struct {
|
|
prefix string
|
|
label dnsScheme
|
|
}{
|
|
{"http://", dnsSchemeHTTP},
|
|
{"https://", dnsSchemeHTTPS},
|
|
}
|
|
|
|
for i := 0; i < len(schemes); i++ {
|
|
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, schemes[i].prefix+host, http.NoBody)
|
|
if err != nil {
|
|
charmlog.Debugf("Error %s: %s", host, err)
|
|
continue
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
charmlog.Debugf("Error %s: %s", host, err)
|
|
continue
|
|
}
|
|
code := resp.StatusCode
|
|
resolved := resp.Request.URL.String()
|
|
resp.Body.Close()
|
|
|
|
if meaningfulStatus(code) {
|
|
return resolved, schemes[i].label
|
|
}
|
|
charmlog.Debugf("skip %s [%s]: status %d", host, schemes[i].label, code)
|
|
}
|
|
|
|
return "", ""
|
|
}
|