Files
Vyntral 3a4c230aa7 feat: v2.0 full rewrite — event-driven pipeline, AI + Nuclei + proxy
Complete architectural overhaul. Replaces the v0.1 monolithic scanner
with an event-driven pipeline of auto-registered modules.

Foundation (internal/):
- eventbus: typed pub/sub, 20 event types, race-safe, drop counter
- module: registry with phase-based selection
- store: thread-safe host store with per-host locks + deep-copy reads
- pipeline: coordinator with phase barriers + panic recovery
- config: 5 scan profiles + 3 AI tiers + YAML loader + auto-discovery

Modules (26 auto-registered across 6 phases):
- Discovery: passive (26 sources), bruteforce, recursive, AXFR, GitHub
  dorks, CT streaming, permutation, reverse DNS, vhost, ASN, supply
  chain (npm + PyPI)
- Enrichment: HTTP probe + tech fingerprint + TLS appliance ID, ports
- Analysis: security checks, takeover (110+ sigs), cloud, JavaScript,
  GraphQL, JWT, headers (OWASP), HTTP smuggling, AI cascade, Nuclei
- Reporting: TXT/JSON/CSV writer + AI scan brief

AI layer (internal/ai/ + internal/modules/ai/):
- Three profiles: lean (16 GB), balanced (32 GB MoE), heavy (64 GB)
- Six event-driven handlers: CVE, JS file, HTTP response, secret
  filter, multi-agent vuln enrichment, anomaly + executive report
- Content-hash cache dedups Ollama calls across hosts
- Auto-pull of missing models via /api/pull with streaming progress
- End-of-scan AI SCAN BRIEF in terminal with top chains + next actions

Nuclei compat layer (internal/nucleitpl/):
- Executes ~13k community templates (HTTP subset)
- Auto-download of nuclei-templates ZIP to ~/.god-eye/nuclei-templates
- Scope filter rejects off-host templates (eliminates OSINT FPs)

Operations:
- Interactive wizard (internal/wizard/) — zero-flag launch
- LivePrinter (internal/tui/) — colorized event stream
- Diff engine + scheduler (internal/diff, internal/scheduler) for
  continuous ASM monitoring with webhook alerts
- Proxy support (internal/proxyconf/): http / https / socks5 / socks5h
  + basic auth

Fixes #1 — native SOCKS5 / Tor compatibility via --proxy flag.

185 unit tests across 15 packages, all race-detector clean.
2026-04-18 16:48:41 +02:00

228 lines
7.0 KiB
Go

// Package smuggling detects HTTP request smuggling (CL.TE and TE.CL
// variants) by sending ambiguous Content-Length / Transfer-Encoding
// combinations and timing-analyzing the responses.
//
// This is the non-destructive timing variant: we send a request crafted
// so that CL.TE or TE.CL parsing desync would cause the server to hold
// the connection waiting for more bytes, while the correct interpretation
// returns immediately. Large response time delta ⇒ likely smuggling.
//
// We do NOT attempt to actually smuggle follow-up requests — that could
// affect other users. This is safe for authorized testing.
package smuggling
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"net"
"strings"
"sync"
"time"
"god-eye/internal/eventbus"
"god-eye/internal/module"
"god-eye/internal/store"
)
const ModuleName = "vuln.http-smuggling"
type smModule struct{}
func Register() { module.Register(&smModule{}) }
func (*smModule) Name() string { return ModuleName }
func (*smModule) Phase() module.Phase { return module.PhaseAnalysis }
func (*smModule) Consumes() []eventbus.EventType { return []eventbus.EventType{eventbus.EventHTTPProbed} }
func (*smModule) Produces() []eventbus.EventType {
return []eventbus.EventType{eventbus.EventVulnerability}
}
// Opt-in: timing-based testing is slower and can be noisy. Bugbounty profile enables it.
func (*smModule) DefaultEnabled() bool { return false }
func (*smModule) Run(mctx module.Context) error {
if !mctx.Config.Bool("smuggling_scan", false) {
return nil
}
timeout := mctx.Config.Int("timeout", 10)
processed := make(map[string]struct{})
var mu sync.Mutex
shouldProcess := func(host string) bool {
mu.Lock()
defer mu.Unlock()
if _, ok := processed[host]; ok {
return false
}
processed[host] = struct{}{}
return true
}
var wg sync.WaitGroup
for _, h := range mctx.Store.All(mctx.Ctx) {
if h == nil || h.StatusCode == 0 {
continue
}
if !shouldProcess(h.Subdomain) {
continue
}
host := h.Subdomain
wg.Add(1)
go func() { defer wg.Done(); probe(mctx, host, timeout) }()
}
sub := mctx.Bus.Subscribe(eventbus.EventHTTPProbed, func(_ context.Context, e eventbus.Event) {
ev, ok := e.(eventbus.HTTPProbed)
if !ok || ev.StatusCode == 0 {
return
}
host := ev.Meta().Target
if !shouldProcess(host) {
return
}
wg.Add(1)
go func() { defer wg.Done(); probe(mctx, host, timeout) }()
})
defer sub.Unsubscribe()
select {
case <-time.After(500 * time.Millisecond):
case <-mctx.Ctx.Done():
}
wg.Wait()
return nil
}
func probe(mctx module.Context, host string, timeoutSec int) {
timeout := time.Duration(timeoutSec) * time.Second
// Baseline: normal request, measure response time.
baseline, err := sendRequest(host, baselineRequest(host), timeout)
if err != nil {
return
}
// CL.TE probe: Content-Length says more data coming, TE: chunked says "last chunk now".
// Vulnerable servers that read TE first return quickly; non-vulnerable
// servers that read CL wait for more bytes and hit the read timeout.
cltePayload := clteRequest(host)
clte, _ := sendRequest(host, cltePayload, timeout)
// TE.CL probe: reversed — server reads CL first (ignoring chunked), payload is poisoned.
teclPayload := teclRequest(host)
tecl, _ := sendRequest(host, teclPayload, timeout)
// Heuristic: if either probe hangs (duration >= timeout * 0.8) and baseline
// returned fast, it's a likely desync.
threshold := time.Duration(float64(timeout) * 0.8)
fastEnough := baseline.duration < timeout/3
if fastEnough && clte.duration > threshold {
emit(mctx, host, "CL.TE", "CL.TE HTTP Request Smuggling candidate", clte)
}
if fastEnough && tecl.duration > threshold {
emit(mctx, host, "TE.CL", "TE.CL HTTP Request Smuggling candidate", tecl)
}
}
type probeResult struct {
duration time.Duration
response string
}
func baselineRequest(host string) string {
return "GET / HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"User-Agent: god-eye-v2\r\n" +
"Connection: close\r\n" +
"\r\n"
}
// clteRequest crafts a CL.TE probe: the chunked body declares "0\r\n\r\n"
// which is the last chunk. If the server honors TE: chunked, the request
// completes immediately. If it honors Content-Length (say, 4), it waits for
// 4 more bytes.
func clteRequest(host string) string {
body := "0\r\n\r\n"
return fmt.Sprintf("POST / HTTP/1.1\r\n"+
"Host: %s\r\n"+
"User-Agent: god-eye-v2\r\n"+
"Content-Length: %d\r\n"+
"Transfer-Encoding: chunked\r\n"+
"Connection: close\r\n"+
"\r\n%s", host, 4, body) // CL=4 mismatches chunked body length
}
// teclRequest: TE: chunked, body ends with a chunk that declares non-zero
// remaining — CL says "done", TE says "more coming". Opposite desync.
func teclRequest(host string) string {
body := "12\r\n" +
"GPOST / HTTP/1.1\r\n" +
"\r\n0\r\n\r\n"
return fmt.Sprintf("POST / HTTP/1.1\r\n"+
"Host: %s\r\n"+
"User-Agent: god-eye-v2\r\n"+
"Content-Length: 3\r\n"+
"Transfer-Encoding: chunked\r\n"+
"Connection: close\r\n"+
"\r\n%s", host, body)
}
// sendRequest opens a raw TCP/TLS connection, writes raw HTTP bytes, and
// returns the time until the first response line is read (or timeout).
func sendRequest(host, payload string, timeout time.Duration) (probeResult, error) {
dialer := &net.Dialer{Timeout: timeout}
conn, err := tls.DialWithDialer(dialer, "tcp", host+":443", &tls.Config{
InsecureSkipVerify: true,
ServerName: host,
})
if err != nil {
return probeResult{}, err
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(timeout))
start := time.Now()
if _, err := conn.Write([]byte(payload)); err != nil {
return probeResult{duration: time.Since(start)}, err
}
br := bufio.NewReader(conn)
line, err := br.ReadString('\n')
return probeResult{duration: time.Since(start), response: line}, err
}
func emit(mctx module.Context, host, kind, title string, r probeResult) {
now := time.Now()
_ = mctx.Store.Upsert(mctx.Ctx, host, func(h *store.Host) {
h.Vulnerabilities = append(h.Vulnerabilities, store.Vulnerability{
ID: "http-smuggling-" + strings.ToLower(kind),
Title: title,
Description: kind + " desync candidate based on response-time delta (" + r.duration.String() + ").",
Severity: string(eventbus.SeverityHigh),
URL: "https://" + host,
Evidence: strings.TrimSpace(r.response),
Remediation: "Ensure front-end and back-end parse Content-Length and Transfer-Encoding identically. Reject requests with both headers.",
OWASP: "A06:2021-Vulnerable and Outdated Components",
FoundAt: now,
})
})
mctx.Bus.Publish(mctx.Ctx, eventbus.VulnerabilityFound{
EventMeta: eventbus.EventMeta{At: now, Source: ModuleName, Target: host},
ID: "http-smuggling-" + strings.ToLower(kind),
Title: title,
Description: "Timing-based " + kind + " desync candidate.",
Severity: eventbus.SeverityHigh,
URL: "https://" + host,
Evidence: strings.TrimSpace(r.response),
Remediation: "Align CL/TE parsing between front-end and back-end.",
OWASP: "A06:2021-Vulnerable and Outdated Components",
})
}