

Email Phishing Eng. · 4: Param Polymorphism
From fixed Gophish RID fingerprints to blind RID search, wildcards, and 404 camouflage.
From fixed Gophish RID fingerprints to blind RID search, wildcards, and 404 camouflage.
~4,100 Chinese characters in the original; about 21 minutes to read
This is Part 4 of the Email Phishing Engineering series. This post answers “what is being accessed”: the URL parameter structure itself is the most visible fingerprint. Gophish’s decade-unchanged
?id=Ab7Xk2Qis a perfect regex signature for WAFs. This article walks from RID code details to the engineering of blind-search algorithms and the fine points of 404 camouflage.
Author: 可惜夜 (Kexi Ye)
First published on WeChat: Yofune Security Research
0x01 The Parameter Name Is the Fingerprint#
Gophish tracking URLs struggle to survive a full attack–defense cycle under today’s conditions.
One obvious reason: the URL parameter structure itself is a fingerprint. Gophish’s ten-year ?id=Ab7Xk2Q pattern can be blocked with near-zero false positives by a single rule in ModSecurity Core Rule Set, FortiWeb, AWS WAF, or Cloudflare WAF:
SecRule ARGS:id "^[a-zA-Z0-9_-]{7}$" "id:100001,deny"bashWhat makes this rule lethal is not that it is a “known attack signature,” but that it naturally excludes legitimate business traffic. No normal site puts an exactly 7-character random string in an id parameter. Gophish RIDs use a 64-character alphabet ([a-zA-Z0-9_-]), length 7—from a detection viewpoint, a perfect static feature.
Firewall and WAF rules matching “short random-string parameters” are ready-made playbooks for defenders. Any enterprise gateway with default protection can block Gophish-default phishing links with essentially no extra configuration.
Gophish architecture also binds the phishing landing page and the management backend to the same HTTP server—almost unacceptable OpSec. Hitting the root route / without a configured landing page exposes the Gophish admin login. Domestic scanning platforms (FOFA, ZoomEye, Hunter / Yingtu) have long ingested Gophish admin HTML structure, the X-Gophish-Contact response header, and the fixed favicon hash into rule libraries.
These three defects—fixed parameter name, exposed admin UI, and no OpSec-oriented design—were among the reasons to rebuild on top of (and beyond) Gophish.
0x02 Blind RID Search: Kill the Parameter-Name Convention#
The core idea is direct: abolish any fixed parameter-name contract. Sender and receiver pre-agree on no parameter name; the receiver “blind-searches” the RID by enumeration plus validation.
2.1 Algorithm#
This is the real logic of extractRIDFromRequest in taigong’s controllers/phish.go (simplified):
HTTP request arrives
│
▼
First try the standard parameter name (RecipientParameter): take value → strip TransparencySuffix (+)
│ └─ ridExistsInStorage(id)? exists → claim it, done
▼
Else: iterate all query keys (sorted by key for determinism)
├── for each value: normalizeRID → isRIDCandidate(regex)
│ no match → skip
│ match → dedupe (seen map) → ridExistsInStorage(id)
│ exists → add to candidate set
│
▼
Candidate set decision:
├── exactly 1 → claim as this request's RID
├── > 1 → ambiguous; reject outright (log warn, return camouflaged 404)
└── 0 → fall back to POST body; still none → treat as probe, return camouflaged 404bashThe key change: RID recognition shifts from “lookup by parameter name” to “lookup by parameter value + existence check in storage.” Parameter names can be generated arbitrarily—even differently per target.
Blue teams can no longer block with SecRule ARGS:id, because the parameter name is an unpredictable dynamic. Detection would require enumerating all parameter names and running entropy analysis on every value—performance most mail security gateways cannot afford.
2.2 Routing Layer#
Gophish registers fixed routes in route.go:
r.HandleFunc("/landing", landingHandler) // fixed route + fixed parameter namebashtaigong uses wildcard routing: path matching and parameter parsing are fully decoupled. All non-static paths share one handler; the RID is blind-extracted inside the handler. That lets red teams craft fully legitimate-looking business URLs:
-
https://phish-domain.com/salary/detail?token=XyZ123A -
https://phish-domain.com/auth/callback?code=XyZ123A&state=abc -
https://phish-domain.com/notice?session_id=XyZ123A
Parameter names are randomly chosen at send time from a lexicon: token, sid, auth_state, session_key, code, ticket, nonce, sign. The lexicon is extensible and can match a target company’s internal naming habits—e.g. ticket for Alibaba Cloud–style stacks, session_state for Microsoft-style stacks.
2.3 Engineering Boundaries#
Blind search has several boundaries that must be handled. Examples from the implementation:
Ambiguity means reject. If multiple parameter values pass the regex and exist in the store, extractRIDFromRequest does not guess—it logs something like RID parse ambiguity: multiple candidates hit; request rejected and returns 404. That is a more robust anti-injection stance than “truncate to the first N parameters”: an attacker dumping many candidate RIDs to collide the store gets uniform rejection. Enumeration cost is also bounded by this reject path.
Database indexes are a hard requirement. The r_id column must have a UNIQUE INDEX. In SQLite, WHERE r_id = ? without an index degrades to full table scan—hundreds of milliseconds per blind search at million-scale result rows; with an index it is O(log n), microseconds. That is the production-readiness watershed.
Regex + store dual validation. isRIDCandidate constrains charset and length, but an ordinary form value that happens to be 7 characters (e.g. half a password) will occur statistically—so store existence is the post-regex backstop: regex pass but not in the store still does not count as a RID.
Blind search removes the parameter-name feature but opens a new gap: parameter-value brute force. Craft many requests with candidate RIDs; each triggers a store lookup. UNIQUE INDEX prevents melting the DB, but bulk enumeration itself is a traffic anomaly. Countering that relies on the IP / rate / UA scoring stack from the previous article—not on the blind-search algorithm alone.
0x03 Parameter Polymorphism and Path Camouflage#
3.1 Gophish’s Fixed-Path Legacy#
In controllers/route.go, Gophish registers a batch of fixed phishing routes: / (Landing), /track (open tracking), /report (credential submit), /robots.txt, /{path}, /static/{filename}. These paths are pre-registered and immutable. Blue team or WAF blocks on /track and /report alone can cut data callback—a handful of rules.
3.2 Hand Almost the Entire Path Space to Blind Search#
taigong folds nearly all paths into the blind-search logic. Aside from a few static assets (/static/), the tracking pixel (pixel.png), robots.txt, /report, /download, and similar necessary endpoints registered separately, everything else goes through phishHandler.
URL structure space opens up immediately:
-
Compatible form:
/?id=XyZ123A(backward compatible with Gophish) -
Token path:
/salary/detail?token=XyZ123A -
OAuth style:
/auth/callback?code=XyZ123A&state=abc -
RESTful:
/api/v1/notice/XyZ123A(RID embedded in the path) -
Hash routing:
/#/verify/XyZ123A(SPA; parameter in the fragment)
The last form is especially effective against mail-gateway URL extraction: most URL parsers do not extract the fragment, while React/Vue-style SPAs parse the RID from the fragment on the client normally.
3.3 Malformed Parameters#
SANS ISC’s Xavier Mertens documented a widely used parameter-evasion technique in a February 2026 diary (Broken Phishing URLs): deliberately craft malformed parameters that violate HTTP norms, for example:
https://phish-domain.com/?dC=handler@domain&*(Dfbash&*(Df is an invalid parameter name under HTTP rules, but all major browsers silently ignore it and still load the page. WAFs, regex detectors, and IOC extraction pipelines cannot parse *(Df as a key=value pair; they may mark the whole URL “abnormal” and skip it—exactly what the attacker wants.
The essence is the asymmetry of “browser tolerance vs. detector strictness.” Browsers forgive everything; security tools reject everything; the middle ground is full of room to work. taigong’s r.ParseForm() also ignores illegal parameters (Go standard library behavior), so malformed parameters can act as decoy parameters: detectors fixate on the invalid ones and overlook the real RID sitting in a legitimate parameter.
3.4 Business Parameter Noise#
Go further: inject a pile of legitimate business parameters into the URL to dilute the RID’s statistical signature:
https://phish-domain.com/report/download
?lang=zh-CN
&tz=Asia%2FShanghai
&device=mobile
&version=2.4.1
&_t=1749283200000
&sid=ZqW8kPmbashOn this URL only sid=ZqW8kPm is actually parsed; the rest are high-frequency legitimate business parameter names and values. When blue teams run parameter entropy analysis, a single high-entropy value (ZqW8kPm ≈ 6.0 bits/char) is diluted by a stack of low-entropy values (zh-CN, Asia/Shanghai, mobile), pulling overall statistics toward legitimate traffic.
0x04 Nginx 404 Camouflage Details#
4.1 Why Nginx 404#
When automated scanners probe suspicious URLs (root hits, nonexistent paths, invalid parameters), the HTTP response is a critical identity-exposure window.
Nginx has long held roughly a third of the global web-server market (Netcraft Web Server Survey, multi-year). A phishing server that returns an Nginx-style 404 behaves like most legitimate sites and does not trip product-signature detection. Gophish’s default 404 exposes two features at once: Gophish page structure in the body, and a Server: Gophish response header.
4.2 taigong’s Actual Implementation#
The camouflaged-404 function in taigong is renderUnknownResponse in controllers/phish.go—only a few lines:
func (ps *PhishingServer) renderUnknownResponse(w http.ResponseWriter, r *http.Request) {
if redirectURL := ps.currentPhishConfig().UnknownRedirectURL; redirectURL != "" {
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, `<html>
<head><title>404 Not Found</title></head>
<body bgcolor="white">
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>`)
}bashKey points:
-
It first checks
UnknownRedirectURL; if set, 302 away; only then returns the 404 body. -
The template is stock Nginx 404—including the legacy
<body bgcolor="white">. Do not “optimize” it; any change can become a fingerprint. -
The
<center>tag and all-lowercasenginxhave been Nginx’s signature since the 0.x era; Apache returnsApache, IIS returnsMicrosoft-IIS/10.0.
That said, the current implementation only clones the response body and does not explicitly set Server: nginx or Content-Type: text/html. Go’s net/http sniffs Content-Type from content, so the body is fine; but Server is empty. A detector that compares header sets can still spot this—an area that can be hardened further.
Another small WAF detection trick: some rules compare whether the 404 body size matches a standard Nginx 404 exactly. One extra space or missing newline can trip “near-match.” Keep the template’s byte length identical to real Nginx; do not casually add comments or indentation.
4.3 Apache and IIS Scenarios#
Different target environments may need different pseudo-server templates:
-
Apache 404:
<address>Apache/2.4.41 (Ubuntu) Server at ...</address>, specificServerheader formats, larger bodies (extra CSS) -
IIS 404:
Server: Microsoft-IIS/10.0,.aspxpath hints, distinctive HTML structure -
CDN camouflage: add Cloudflare-style headers such as
CF-Ray,CF-Cache-Status
Server camouflage is not about inventing fakes from scratch—there is no need to fabricate something new. Capture a real Nginx 404 as the template and do not “improve” it.
0x05 Three Strategies for Non-Target Access#
In taigong Campaign config, the NonTargetAction field controls non-target access. In code it is a switch (controllers/phish.go):
switch ptx.NonTargetAction {
case "404":
http.NotFound(w, r)
return
case "redirect":
if ptx.NonTargetURL != "" {
http.Redirect(w, r, ptx.NonTargetURL, http.StatusFound)
return
}
http.NotFound(w, r) // fall back to 404 if URL unset
return
case "show":
// continue showing the normal page
}bashThree strategies:
-
show: show the original landing page to all visitors, target or not. Lowest stealth, but useful for A/B testing landing conversion, internal training where employees self-visit, and low-risk targets. -
404: return 404 for non-target access. Note this uses Go’s standardhttp.NotFound(plain-text “404 page not found”), a different path fromrenderUnknownResponse’s Nginx-camouflaged 404. One path is for “platform mismatch” (e.g. a Windows visitor hits a macOS-only page); the other is for “RID not found.” These two remain separate today. -
redirect: 302 to a legitimate site (corporate homepage, Baidu home, etc.). In practice redirect often works better than 404—seeing a 302 to a normal site is more likely to be treated as a false positive than seeing a 404.
Beyond this non-target decision, there is an earlier, live mechanism: Cloudflare edge injection of a custom encrypted string; if backend verification fails, 302 to https://www.baidu.com (see Chapters 1 and 2). That is the first non-target disposal at the deployment layer, earlier than application-level NonTargetAction.
A “gradient response” was discussed earlier: first scan 302, second 404, third TCP RST silent drop. Checking the code, the three-level gradient (especially TCP RST) is not implemented today; multi-level gradient remains a reasonable evolution path.
0x06 From Signature Matching to Behavioral Profiling#
Red-team technical evolution forces blue teams toward more macroscopic dimensions. Parameter-level evasion kills static features but leaves statistical and infrastructure-layer detectable dimensions.
6.1 Statistical Fingerprint of Parameter Values#
Blind search removes fixed parameter names, but parameter values leave statistical signatures. A 7-character random RID over a 64-character alphabet has information entropy:
bits
Per-character entropy:
bits/character
Compared with normal business parameters:
-
session_id=user123→ ≈ 2.1 bits/char -
page=2&limit=10→ ≈ 1.5 bits/char -
token=Ab7Xk2Q→ ≈ 6.0 bits/char (near the theoretical max for a 64-char alphabet)
The right approach is a conditional combination:
high-entropy parameter (>5.5 bits/char)
AND domain age < 90 days
AND cert = Let's Encrypt
AND domain first CT Log appearance < 7 days ago
→ high-priority alertbashAND across four conditions cuts false positives sharply: legitimate sites rarely satisfy “high-entropy param + newly registered domain + free cert + no CT history” at once.
6.2 DNS Timing#
The first hop before a phishing link is clicked is DNS. Blue teams run anomaly detection on internal DNS:
| Signal | Meaning |
|---|---|
| Same domain queried by many distinct internal IPs in a short window | Concentrated clicks after a mass mail |
| DNS query TTL = 1 | Common in Let’s Encrypt validation setups |
| Wildcard DNS to CDN edge | Hides origin IP |
| DNS queries clustered outside business hours | Non-normal business pattern |
6.3 Turning Tracking Pixels Against You#
Both Gophish and taigong use transparent tracking pixels (1×1 PNG) to detect opens. Countermeasures are simple: SEGs strip tracking pixels on inbound; mail clients default to not loading remote images (Outlook/Thunderbird); even with dynamic parameter names, the tracking-pixel path pattern itself remains a potential feature.
0x07 Where Parameter Evasion Stops Evolving#
Parameter evasion follows a clear law: when one feature disappears, more features appear—red/blue never ends. Rough generations:
| Generation | Red team | Blue team |
|---|---|---|
| Early | Fixed param name ?id= | Regex match on id |
| Blind search | Blind RID, random param names | Parameter-value entropy analysis |
| Path | RESTful path-embedded RID | Path structure + domain lifecycle combo |
| SPA | Hash routing (RID in fragment) | Traffic timing + DNS anomalies |
| Noise | Malformed params + business noise | Multi-dimensional correlation (CA + ASN + registrar + behavior) |
| Current | Aged domain + AI-generated URLs | Behavioral baselines & infrastructure-linked anomalies |
| Trend | WebSocket / SSE real-time push | Protocol-layer behavioral baseline deviation |
Current state sits between “noise” and “current.” Traditional WAF signatures already fail against blind RID search; the future is correlational anomalies between infrastructure and traffic metadata, not single-template fingerprint hits.
6.4 The Other Side of AI-Generated URLs#
Unit 42 has demonstrated LLM runtime assembly of attacks: the client calls an LLM API live, generating malicious JS in the victim browser—different syntax each time, same function, no static payload left behind. Port that idea to URLs and an LLM generates path and parameter names that match the target’s internal naming norms; once every component is style-customized, feature-based detection largely fails.
But this route has its own tell: LLM-generated URLs are too “perfect.” Real business is full of randomness and mess—typos, inconsistent naming, missing version numbers. A URL polished too clean by an LLM can be flagged because it is too clean. That is the GREASE spirit (RFC 8701) echoing in anti-detection: you must deliberately leave bug-like traits to blend into the environment.
6.5 Closing#
Once parameter-level detection fully fails, focus migrates in two directions.
Infrastructure clustering. No matter how the URL is camouflaged, the phishing server’s underlying infrastructure (ASN, hoster, IP range, cert issuance pattern, DNS config) forms stable cluster features. Infrastructure shared across campaigns cannot be covered by parameter camouflage. This is the highest-value detection dimension—see contemporary PhaaS analysis.
Behavioral baseline. A domain under attacker control will deviate from its historical baseline. Even a 10-year-old domain will, somewhere, deviate in DNS resolution patterns, cert renewal patterns, or page response patterns from its original purpose.
To borrow Deleuze: To become is to deterritorialize.
Series Navigation