Back to Articles

gowitness: How Chrome Headless Became a Recon Powerhouse for Security Teams

[ View on GitHub ]

gowitness: How Chrome Headless Became a Recon Powerhouse for Security Teams

Hook

While most developers use headless Chrome to test their own applications, security researchers realized they could turn it into a weapon for mapping attack surfaces across thousands of targets in minutes.

Context

Traditional security reconnaissance involved manually visiting web applications one by one, a tedious process that became impossible when scanning corporate networks with hundreds or thousands of HTTP services. Early tools like wkhtmltoimage or PhantomJS provided automation but lacked the rendering fidelity of real browsers, missing modern JavaScript-heavy applications entirely. Python-based solutions like EyeWitness emerged to fill the gap, but their performance couldn't keep pace with the scale of modern penetration tests where Nmap might identify thousands of web services across CIDR ranges.

gowitness emerged from SensePost, a security consultancy that needed a tool matching the performance characteristics of Go with the rendering accuracy of Chrome. The key insight was that security teams don't just need screenshots—they need context. Which technologies is the target running? What cookies are set? What console errors appear? Are there interesting HTTP headers that reveal backend infrastructure? By combining Chrome Headless with Go's concurrency primitives and a SQLite-backed web interface, gowitness transforms raw scan data into actionable intelligence.

Technical Insight

Parse targets

Concurrent tasks

Screenshot + metadata

HTML content

Screenshot bytes

Technologies

Perceptual hash

Headers/cookies/logs

Query results

Input Sources

URLs/CIDR/Nmap/Nessus

Scanning Engine

Worker Pool

Chrome Headless

chromedp/go-rod

Data Collector

wappalyzergo

Tech Fingerprinting

goimagehash

Screenshot Dedup

Storage Layer

SQLite/JSON/CSV

Web Viewer

chi + GORM

System architecture — auto-generated

At its core, gowitness orchestrates headless Chrome instances through two alternative automation libraries: chromedp and go-rod. This dual-backend approach provides resilience—if one library encounters issues with specific page behavior, users can switch to the other. The architecture separates concerns cleanly: input processing handles various formats (raw URLs, CIDR ranges, Nmap XML, Nessus files), a worker pool manages concurrent Chrome instances, and output handlers serialize results to SQLite, JSON Lines, or CSV.

The real power emerges in how gowitness chains data collection. When visiting a target, it doesn't just capture a screenshot. Here's a simplified example of the data collection workflow:

// Pseudo-code showing gowitness's data collection approach
type ScanResult struct {
    URL          string
    Screenshot   []byte
    Title        string
    Headers      map[string]string
    Cookies      []Cookie
    ConsoleLogs  []string
    Technologies []Technology // via wappalyzergo
    PerceptHash  string       // for deduplication
    NetworkLog   []NetworkEntry
}

func scanTarget(url string) (*ScanResult, error) {
    ctx := chromedp.NewContext(context.Background())
    
    var screenshot []byte
    var title string
    var headers map[string]string
    
    chromedp.Run(ctx,
        chromedp.Navigate(url),
        chromedp.WaitReady("body"),
        chromedp.Title(&title),
        chromedp.CaptureScreenshot(&screenshot),
        // Custom evaluators for headers, cookies, etc.
    )
    
    // Technology fingerprinting
    techs := wappalyzer.Analyze(headers, body)
    
    // Perceptual hashing for deduplication
    phash := goimagehash.PerceptionHash(screenshot)
    
    return &ScanResult{...}, nil
}

The technology fingerprinting integration via wappalyzergo is particularly clever. While capturing screenshots, gowitness analyzes HTTP headers, HTML patterns, JavaScript variables, and DOM structure to identify frameworks, CMS platforms, web servers, and JavaScript libraries. This happens in parallel with screenshot capture, adding minimal overhead while providing crucial intelligence about the technology stack.

gowitness's concurrency model leverages Go's goroutines to maintain a pool of Chrome instances. Rather than spawning a new browser for each URL (expensive in memory and startup time), it maintains a configurable number of persistent Chrome instances that workers feed URLs to. This design decision dramatically improves throughput—scanning 1,000 URLs might take hours with serial processing but completes in minutes with 10-20 concurrent workers.

The screenshot deduplication using perceptual hashing solves a common problem in security assessments: many services return identical default pages (think hundreds of Tomcat default pages or router login screens). Using goimagehash, gowitness computes a perceptual hash for each screenshot and groups visually similar pages together. This transforms unmanageable galleries of thousands of screenshots into organized clusters where unique interfaces stand out.

The built-in web interface deserves special mention. Rather than dumping screenshots to files, gowitness stores everything in SQLite and serves a chi-based web application for browsing results. The GORM integration provides structured queries—filter by HTTP status code, group by technology, search by title, or sort by similarity. This eliminates the context-switching of using external tools and keeps reconnaissance workflows contained in a single interface.

For penetration testers working with Nmap output, the integration is seamless:

# Scan a network with Nmap, extract HTTP services
nmap -p- -oX scan.xml 192.168.1.0/24

# Feed directly into gowitness
gowitness scan nmap -f scan.xml -t 25 --write-db

# Start the web interface to review
gowitness server -D gowitness.sqlite3

This pipeline takes raw network scan data and produces an interactive gallery with technology fingerprints, all without manual URL extraction or script writing.

Gotcha

Windows support remains a persistent pain point. The project documentation candidly states it's 'mostly working,' which translates to intermittent issues with Chrome path detection, process cleanup, and file handling. If your security team operates primarily on Windows workstations, expect to invest time troubleshooting or just run gowitness in WSL2 or Docker.

Memory consumption becomes significant at scale. Each Chrome instance consumes 100-200MB of RAM, and while gowitness limits concurrent instances, scanning thousands of targets still demands substantial resources. On memory-constrained environments like small cloud instances or Docker containers with tight limits, you'll need to carefully tune the concurrency flags or risk OOM kills. The Chrome dependency itself adds deployment friction—you can't just deploy a single binary; you need Chrome or Chromium installed, which complicates containerization and increases image sizes. The official Docker image addresses this but weighs in at several hundred megabytes due to bundling Chromium.

Verdict

Use gowitness if you're conducting security assessments, penetration tests, or bug bounty reconnaissance where you need to quickly visualize and catalog large numbers of web interfaces discovered through network scanning. It excels when you have Nmap/Nessus output to process, need technology fingerprinting alongside screenshots, want integrated reporting without building your own tooling, or operate primarily on Linux/macOS systems with adequate RAM. The performance gains from Go's concurrency and the integrated SQLite viewer make it the strongest choice for batch processing dozens to thousands of targets. Skip it for simple one-off screenshots where browser DevTools or a basic Puppeteer script suffices, when Windows is your primary platform without WSL2 available, in resource-constrained environments with less than 4GB RAM, or for non-security use cases where you don't need Nmap integration, technology fingerprinting, or other reconnaissance-specific features. For straightforward screenshot automation without security context, lighter alternatives make more sense.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/cybersecurity/sensepost-gowitness.svg)](https://starlog.is/api/badge-click/cybersecurity/sensepost-gowitness)