Scout: When Your Web Fuzzer Needs to Fit in a Single Binary
Hook
Most web fuzzers ship with a README that says 'Step 1: Download a wordlist.' Scout said 'no thanks' and baked everything into the binary itself—no dependencies, no setup, just run.
Context
Web reconnaissance tools have a chicken-and-egg problem: you need wordlists to find hidden directories, but carrying wordlists around breaks portability. Penetration testers often work in restricted environments—air-gapped networks, locked-down jump boxes, or ephemeral containers where downloading SecLists isn't an option. Traditional tools like dirb and dirbuster require external wordlist files, forcing you to bundle them separately or hope they're pre-installed.
Scout emerged from this friction in the CTF and penetration testing space. Built in Go for cross-platform compilation, it takes a controversial approach: embed a curated wordlist directly in the binary using Go's embedding capabilities. The result is a single executable that performs both URL fuzzing (discovering hidden files and directories) and VHOST enumeration (finding virtual hosts on a single IP address). No installation scripts, no wordlist management, no external dependencies—just copy and run.
Technical Insight
Scout's architecture revolves around three core components: an embedded wordlist system, a concurrent HTTP request engine, and a dual-mode fuzzer. Let's examine how each works and why the design choices matter.
The embedded wordlist is Scout's most distinctive feature. Instead of reading from external files, Scout uses Go's string constants or embedded file features to include wordlist data directly in the compiled binary. This means the wordlist becomes part of the executable itself. Here's the conceptual approach:
// Simplified example of embedded wordlist pattern
package wordlist
var DefaultWordlist = []string{
"admin",
"config",
"backup",
"test",
"dev",
// ... hundreds more entries
}
// In the fuzzer code
func (s *Scanner) LoadWordlist() []string {
if s.CustomWordlist != "" {
return readExternalFile(s.CustomWordlist)
}
return wordlist.DefaultWordlist
}
This design trades flexibility for portability. You can't easily inspect or modify the wordlist without recompiling, but you gain a zero-dependency deployment model. For CTF environments where you're uploading a single binary to a compromised system, this tradeoff makes sense.
The HTTP request engine uses goroutine pools for concurrent fuzzing. Scout spawns multiple workers that pull from a shared channel of targets to test:
// Conceptual fuzzing engine structure
func (s *Scanner) Fuzz(baseURL string, wordlist []string) {
results := make(chan Result, 100)
targets := make(chan string, len(wordlist))
// Populate target channel
for _, word := range wordlist {
for _, ext := range s.Extensions {
targets <- baseURL + "/" + word + ext
}
}
close(targets)
// Spawn worker goroutines
var wg sync.WaitGroup
for i := 0; i < s.Concurrency; i++ {
wg.Add(1)
go s.worker(&wg, targets, results)
}
// Process results
go func() {
wg.Wait()
close(results)
}()
for result := range results {
if s.shouldReport(result) {
s.output(result)
}
}
}
func (s *Scanner) worker(wg *sync.WaitGroup, targets <-chan string, results chan<- Result) {
defer wg.Done()
client := s.makeHTTPClient()
for target := range targets {
resp, err := client.Get(target)
if err != nil {
continue
}
results <- Result{
URL: target,
StatusCode: resp.StatusCode,
Size: resp.ContentLength,
}
resp.Body.Close()
}
}
The worker pattern allows Scout to control parallelism—critical when fuzzing production systems where too many concurrent requests trigger rate limiting or WAF blocks. The configurable concurrency level lets operators balance speed against stealth.
Scout's dual-mode operation handles two distinct use cases. URL fuzzing tests paths against a known domain, trying each wordlist entry with optional file extensions. VHOST enumeration flips the approach: it tests different Host headers against a single IP address to discover virtual hosts. This is powerful for pentesting because web servers often host multiple sites on one IP, with some only accessible through specific hostnames:
// VHOST enumeration approach
func (s *Scanner) EnumerateVHOSTs(targetIP string, wordlist []string) {
for _, subdomain := range wordlist {
hostname := subdomain + "." + s.BaseDomain
req, _ := http.NewRequest("GET", "http://"+targetIP, nil)
req.Host = hostname // Key difference: custom Host header
resp, err := s.client.Do(req)
if err != nil {
continue
}
// Different response sizes/codes indicate valid VHOSTs
if s.isInterestingResponse(resp) {
s.output(hostname, resp)
}
}
}
The VHOST mode exploits how HTTP virtual hosting works—the server routes requests based on the Host header, not just the IP. By testing thousands of potential hostnames, Scout discovers hidden admin panels, staging environments, or forgotten subdomains that don't appear in DNS.
Status code filtering prevents output spam. Scout typically ignores 404s and only reports 200s, 301s, 403s, and other interesting codes. The configurable filtering lets you adapt to applications that return 200 for everything (requiring content-length analysis instead) or use non-standard status codes.
Gotcha
Scout's biggest limitation is its apparent abandonment. The repository shows signs of being a point-in-time project—Travis CI badges, no recent commits addressing modern web fuzzing challenges. This means you won't find features that have become standard in actively maintained tools: no smart retry logic when requests fail, no wildcard detection to handle servers that return 200 for every path, no rate limiting to avoid detection, and no integration with modern authentication methods.
The embedded wordlist design, while portable, creates real practical problems. You can't easily audit which paths Scout tests without reading the source code. When you need a specialized wordlist for API endpoints, cloud storage buckets, or framework-specific paths, you're forced to either recompile with your custom list or fall back to the external wordlist option (which defeats the portability advantage). Tools like ffuf and gobuster let you swap wordlists instantly, mix multiple lists, or generate custom patterns—capabilities that are non-trivial to replicate with Scout's architecture. For serious engagements where wordlist selection determines success or failure, this inflexibility becomes a blocker.
Verdict
Use Scout if you're working in restricted environments where portability trumps features—think CTF challenges, air-gapped networks, or quick reconnaissance on compromised systems where uploading a single binary is easier than transferring wordlists. It's also valuable as a learning tool for understanding web fuzzing mechanics and Go concurrency patterns. Skip Scout if you need a maintained tool for professional penetration testing, want advanced features like intelligent crawling or WAF evasion, require flexible wordlist management, or need integration with modern security workflows. For active projects, ffuf or feroxbuster deliver better performance, more features, and ongoing development. Scout works as a minimalist utility or educational project, but serious security work demands more robust tooling.