About WappaGo • Installation • Usage
WappaGo has been developed to assemble different features from tools like HTTPX, Naabu, GoWitness and Wappalyzer. To allow an efficient detection of technologies, it is necessary to open a browser and in order to avoid opening a browser for each target, WappaGo opens only one browser and uses the system of pages which allows to consume less resources and to carry out an analysis much more quickly.
Download the latest release or compile by yourself :
git clone https://github.com/EasyRecon/wappaGo
cd wappaGo && go build or
go install github.com/EasyRecon/wappaGo@latest
Building from source needs Go 1.26 or newer.
-
Chrome installed on the system.
-
A display. Chrome runs headed by default, because headless reports a fake 800x600 screen and brands its own User-Agent. On a desktop this is already the case; on a server, run wappaGo under a virtual display:
xvfb-run -a --server-args="-screen 0 1920x1080x24" ./wappaGo < domain.txt
Pass
-headlessinstead if you would rather not install one — it works, it is just easier to fingerprint. Without a display and without-headless, Chrome will not start and wappaGo tells you so. -
Outbound HTTPS to
raw.githubusercontent.com, to fetch the technology fingerprints at startup. A failure here is fatal rather than silently scanning with an empty database.
Usage of wappaGo:
-amass-input
Pip directly on Amass (Amass json output) like amass -d domain.tld | wappaGo
-chrome-threads int
Number of chromes threads in each main threads total = option.threads*option.chrome-threads (Default 5) (default 5)
-follow-redirect
Follow redirect to detect technologie
-headless
Run Chrome without a display. Off by default: headless fakes an 800x600
screen and brands its own User-Agent. Use xvfb-run on a server instead
-jitter int
Max random delay in ms added before each request (0 = none)
-no-progress
Disable the stderr progress bar
-port-timeout int
Timeout during port scanning in ms (default 2000)
-ports string
port want to scan separated by coma (default "80,443")
-proxy string
Use http proxy
-report
Generate HTML report (in addition to the JSON on stdout)
-resolvers string
Use specifique resolver separated by comma
-rps float
Max HTTP requests per second per host to stay under rate-based WAF rules (0 = unlimited)
-screenshot string
path to screenshot if empty no screenshot
-threads int
Number of threads to start recon in same time (default 5)
-user-agent string
User-Agent sent by both the HTTP probe and Chrome (blank keeps the built-in browser UA)
Results are written to stdout as one JSON object per line, so -report can be
combined with a redirect:
cat domain.txt | ./wappaGo -report > results.json # JSON on stdout + wappaGo_report.htmlThe technology fingerprints are downloaded at startup from Serizao/tech, parsed into memory, and the on-disk copy is deleted before the scan begins.
wappaGo presents one consistent browser identity to every host it scans. The
User-Agent is read from the installed Chrome at startup (with the
HeadlessChrome token rewritten) and installed together with its matching
Client Hints metadata, so navigator.userAgent, navigator.userAgentData and
the Sec-CH-UA-* headers all report the same build. The raw HTTP probe derives
its User-Agent, Sec-CH-UA-*, Accept and Accept-Language from that same
identity. --enable-automation is not passed, and WebGL is left enabled.
Chrome runs with a display by default. Headless is available behind
-headless, but it costs two things that then have to be papered over: it reports
an 800x600 screen whatever the window size, so a 1400x900 viewport claims to sit
on a smaller display, and it brands its own User-Agent. With a real display both
are simply correct — measured screen=1920x1200 and a clean UA — and no screen
override is issued at all. On a server, run it under a virtual display:
xvfb-run -a --server-args="-screen 0 1920x1080x24" ./wappaGo < domain.txtWithout a display and without -headless, Chrome will not start; wappaGo says so
and points at both options rather than failing with a bare error.
RTCPeerConnection leaks the host's real public address when -proxy is in use:
WebRTC negotiates over UDP and does not traverse an HTTP proxy, so a scanned page
can reach a public STUN server and read straight past the proxy. That is
deanonymisation rather than fingerprinting, so -proxy also enables
--webrtc-ip-handling-policy=disable_non_proxied_udp, which drops both the
server-reflexive and host candidates while leaving RTCPeerConnection defined —
closing the leak without substituting a "browser with no WebRTC" tell. Chrome
already hides the LAN address behind an mDNS name by default. The
--force-webrtc-ip-handling-policy spelling was measured to have no effect at all
on Chrome 150; only the older flag name works.
That identity is capped to the Chrome version the TLS handshake below can
actually imitate. uTLS ships no profile for the newest Chrome releases, and
claiming a browser newer than the handshake produces is a contradiction
bot-protection vendors correlate: against a DataDome-protected origin, a Chrome
133 handshake paired with a User-Agent claiming Chrome 150 was refused 4/4 with
403 and a captcha page, while the same handshake with a matching User-Agent
passed 2/2. The cost is that the browser reports an older version than its own
engine, which a site could catch by comparing the User-Agent against JS features
only present in the real build — a rarer and more expensive check than the
TLS/User-Agent correlation, and claiming older is the safe direction.
A measurement harness records what the browser actually gives away:
WAPPAGO_STEALTH_CHECK=1 go test ./cmd/ -run Stealth -vIt serves its own page locally, so the verdicts are reproducible, and it also
drives the hosted detector at bot-detector.rebrowser.net:
WAPPAGO_STEALTH_REMOTE=1 go test ./cmd/ -run RemoteDetector -vTwo known limits it reports rather than hides:
window.outerWidth/outerHeightare 0. This comes from the tab chromedp attaches to throughTarget.createTarget, which has no window of its own — not from headless, and not from the metrics override. Measured: every combination of headless/display, override/no override andBrowser.setWindowBoundsreports 0, while raw Chrome opening a URL itself reports the window size. Nothing reachable over CDP changes it, and the usual workaround redefines the accessor from an injected script, which leaves a getter that no longer reports[native code]— a broader tell than the one it fixes.- WebGL quality depends on the host. With a GPU, Chrome reports the real
adapter, the most plausible fingerprint available. On a GPU-less server it falls
back to software rendering and says so (
SwiftShader,llvmpipe,Microsoft Basic Render Driver), and the canvas hash shifts with it — a stable value that is a known value is still identifying. There is no CDP override for WebGL, so masking it would mean patching JS. Measured against the hosted detector, forcing SwiftShader changed no verdict at all: the same greens as a real GPU. That is a negative result from one detector rather than proof of safety — CreepJS and commercial vendors do fingerprint WebGL — so nothing is patched for now and the harness logs aNOTE:when software rendering is in use.
Canvas, WebGL and audio fingerprints are deliberately not randomised. The goal is to look ordinary, not unique: a fingerprint that changes between two reads is itself a signature of an anti-fingerprinting tool. The harness asserts those surfaces are present and stable instead.
The raw HTTP probe also handshakes like a browser. It runs before Chrome on every
target, so its TLS ClientHello is the first thing a host sees, and Go's
crypto/tls hello does not resemble one: different cipher list and ordering, and
no ALPN at all — which is what JA3/JA4 hashes key on. The probe now dials https
through uTLS with a Chrome
ClientHello, and carries the request over HTTP/2 when ALPN settles on it, so the
HTTP/2 SETTINGS/WINDOW_UPDATE fingerprint is a real h2 client's rather than a
downgrade to HTTP/1.1. Measured against a local server:
| cipher suites | first suite | ALPN offered | |
|---|---|---|---|
crypto/tls |
13 | 0xc02b |
(none) |
| uTLS Chrome | 16 | 0x5a5a (GREASE) |
h2, http/1.1 |
Two cases keep the standard transport on purpose: plain http, which has no
handshake to disguise, and -proxy, where the origin handshake happens inside a
CONNECT tunnel wappaGo does not drive.
Still outstanding: chromedp issues Runtime.enable on every target, which is
a reliable CDP detection signal. The usual fix routes evaluation through an
isolated world, but wappaGo's js fingerprints read page globals
(jQuery.fn.jquery, Stimulus.Application) and so require the main world.
You can either use wappaGo from a file containing a list of domains
cat domain.txt | ./wappaGoor from an Amass output (preferred)
amass enum -d example.com -ipv4 -json out.json
cat out.json | ./wappaGo -amass-inputYou can use wappaGo as a library in your own project.
type WrapperOptions struct {
Screenshot string
Ports string
Threads int
Porttimeout int
Resolvers string
FollowRedirect bool
ChromeTimeout int
ChromeThreads int
Proxy string
UserAgent string
Rps float64
Jitter int
Headless bool
}Any field left at its zero value falls back to the same default as the
corresponding CLI flag (Ports: "80,443", Threads: 5, ChromeThreads: 5,
Porttimeout: 2000), so you only have to set what you want to change.
Both entry points return an error, which is non-nil when the setup itself
failed — most often because the technology fingerprints could not be downloaded.
Ignoring it means scanning every host with an empty database and reporting zero
technologies everywhere.
package main
import (
"fmt"
"log"
"github.com/EasyRecon/wappaGo/structure"
"github.com/EasyRecon/wappaGo/wrapper"
)
func main() {
input := []string{"google.com", "twitter.com"}
options := structure.WrapperOptions{
Ports: "80,443",
Screenshot: "screenshots",
}
// Sync mode: blocks, then returns everything.
results, err := wrapper.StartReconSync(input, options)
if err != nil {
log.Fatal(err)
}
for _, result := range results {
fmt.Println(result)
}
} // Async mode: results are streamed on the channel, which wrapper closes
// when the scan is done. StartReconAsync blocks until then, so run the
// consumer in its own goroutine and wait for it to drain.
results := make(chan structure.Data)
done := make(chan struct{})
go func() {
defer close(done)
for result := range results {
fmt.Println(result)
}
}()
if err := wrapper.StartReconAsync(input, options, results); err != nil {
log.Fatal(err)
}
<-doneFor each url, you will receive a structure.Data which contains all the information about the target.
- Add robot technologie dectection
- Add xhr technologie dectection
This tool uses several ProjectDiscovery libraries
