Home Security Phishing Redirect Abuse
Phishing Redirect Abuse Research / Writeup Informational

redirect_analyzer: A Free Tool to Trace Phishing Redirect Chains

redirect_analyzer: A Free Tool to Trace Phishing Redirect Chains

The three previous posts in this section laid out the techniques attackers use to deliver phishing through redirect chains. This post is about the tool we built to dissect them. It's free, it's open source, and it does the analysis that most email security products quietly skip.

What it does

redirect_analyzer.py takes a URL and walks the entire redirect chain. Not just HTTP 3xx redirects — every redirect mechanism we've documented in this research:

  • HTTP 3xx redirects (the obvious case)
  • Soft redirects — HTTP 200 OK responses that carry a Location header (the AMP CDN pattern)
  • Meta-refresh tags in HTML bodies (<meta http-equiv="refresh">)
  • JavaScript location patterns (window.location.replace, document.location.href)
  • URL parameter extraction (decodes ?q=, ?u=, ?url=, ?redirect_uri=, ?continue=, ?redirect_after_logout=, etc.)
  • Subdomain-encoded destinations (<dest-with-dashes>.cdn.ampproject.org<dest>.com)

For each hop, the tool records the URL, the HTTP status, the server header, and how the next hop was discovered. The output is either human-readable text or structured JSON.

Why this matters

Most email security products are HTTP-only scanners. They follow 3xx redirects, evaluate the final destination, and either pass or flag the email. That works for the simple case — bit.ly/abc redirects to a phishing URL, scanner follows it, scanner sees the phishing URL.

What HTTP-only scanners miss:

  • Soft redirects. Google's AMP CDN returns HTTP 200 with a Location header. A scanner following only 3xx status codes sees a 200 OK at cdn.ampproject.org and stops. The destination URL is in the response body (meta-refresh) and in a non-standard response header — neither of which the scanner parses.
  • Subdomain-encoded destinations. When the URL is https://victim-com.cdn.ampproject.org/c/s/victim.com, the destination domain victim.com is encoded in the subdomain with dots replaced by dashes. A scanner that does hostname reputation checks sees cdn.ampproject.org (Google, trusted) and never extracts victim.com to check it.
  • Meta-refresh redirects. Scanners that don't render HTML stop at the page and never see the <meta http-equiv="refresh"> tag in the body.
  • JS-obfuscated redirects. A page that computes its destination via atob() or eval() at runtime is opaque to any scanner that doesn't actually execute JavaScript in a real browser environment.

The tool handles all of these.

Sample run

Twitter open redirect:

$ python3 redirect_analyzer.py "https://twitter.com/logout?redirect_after_logout=https://example.com"chr(10)chr(10)[1] PARAM_EXTRACTchr(10)URL:    https://twitter.com/logout?redirect_after_logout=https://example.comchr(10)→       https://example.comchr(10)Note:   redirect param: redirect_after_logoutchr(10)chr(10)[2] HTTP_3XXchr(10)URL:    https://twitter.com/logout?redirect_after_logout=https://example.comchr(10)Status: HTTP 302chr(10)Server: cloudflare envoychr(10)→       https://example.comchr(10)chr(10)[3] FINALchr(10)URL:    https://example.comchr(10)Status: HTTP 200chr(10)chr(10)Risk score:       4chr(10)Risk signals:chr(10)- chain through trusted hop(s) → untrusted destination

Google AMP CDN with subdomain encoding:

$ python3 redirect_analyzer.py "https://example-com.cdn.ampproject.org/c/s/example.com"chr(10)chr(10)[1] SUBDOMAIN_DECODEchr(10)URL:    https://example-com.cdn.ampproject.org/c/s/example.comchr(10)→       https://example.com/chr(10)Note:   destination domain encoded in subdomainchr(10)chr(10)[2] FINALchr(10)URL:    https://example.com/chr(10)Status: HTTP 200chr(10)chr(10)Risk score:       2chr(10)Risk signals:chr(10)- destination encoded as subdomain (AMP/Translate pattern)

The tool catches both — it sees the redirect parameter in the Twitter URL, then follows the 302, then notes that the chain crossed a trusted domain to reach a non-trusted destination. For AMP CDN it decodes the subdomain encoding before any HTTP request, because the destination is already in the URL itself.

Deeper detection — TLS cert age and JS rendering

The --deep flag enables two additional checks.

--check-cert connects to the final destination via TLS, fetches the server certificate, and reports its notBefore date. Phishing infrastructure overwhelmingly uses freshly-issued Let's Encrypt certificates because attackers register new domains for each campaign. A certificate issued in the last 7 days is a strong signal that the destination is fresh infrastructure rather than an established business.

The implementation uses Python's built-in ssl module — no external API, no rate limits, no dependency on crt.sh (which is frequently overloaded).

TLS cert (final host):chr(10)subject:    suspicious-domain.comchr(10)issuer:     R10                 # Let's Encryptchr(10)not_before: May  9 2026 GMTchr(10)age:        1 days              # → +6 risk score

--render-js launches a headless Chromium instance via Playwright and renders the URL. The browser executes JavaScript, follows all redirects (including JS-driven ones), and reports the final URL. If the browser-final URL differs from the static-analysis-final URL, the chain contains an obfuscated redirect that static analysis missed.

We tested this against a deliberately obfuscated page that decoded its destination at runtime:

<script>chr(10)var d = atob("aHR0cHM6Ly9leGFtcGxlLmNvbQ==");chr(10)setTimeout(function() { window.location.href = d; }, 500);chr(10)</script>

Static analysis sees the page as a dead end (no obvious redirect parameters, no meta-refresh tag the parser recognizes). The browser executes the script, decodes the base64, navigates to example.com. The tool reports:

Browser ended at https://example.com/ but static analysis stopped atchr(10)http://localhost:8765/ — possible JS-obfuscated redirect

That's the kind of detection that defeats sandbox-evasion techniques. Even if the phishing page deliberately obfuscates its destination URL to defeat regex-based scanners, the browser still executes the code and follows through. The mismatch is the signal.

Installation

# Basic static analysischr(10)pip3 install requests beautifulsoup4chr(10)chr(10)# Plus deep modechr(10)pip3 install playwrightchr(10)playwright install chromium

Then:

chmod +x redirect_analyzer.pychr(10)chr(10)# Fast: static analysis onlychr(10)./redirect_analyzer.py "<url>"chr(10)chr(10)# Full: cert age check + JS renderingchr(10)./redirect_analyzer.py --deep "<url>"chr(10)chr(10)# JSON output for scriptingchr(10)./redirect_analyzer.py --json --deep "<url>"

Risk scoring

Each chain produces a numeric risk score plus a list of named signals. The scoring is intentionally conservative — a benign multi-hop legitimate chain might score 4 or 5, while an obvious phishing chain typically scores 10+. The signals matter more than the absolute number.

| Signal | Risk added | |---|---| | Chain has 3 or more hops | +n_hops (max +5) | | Meta-refresh redirect in chain | +2 | | JavaScript location redirect in chain | +2 | | Soft redirect (200 + Location) | +3 | | Subdomain-encoded destination | +2 | | Trusted hops → untrusted final | +4 | | Final cert issued in last 7 days | +6 | | Final cert issued in last 30 days | +3 | | Browser-vs-static URL mismatch | +5 |

A SOC analyst running the tool on a user-reported suspicious email gets immediate context: the chain shape, the redirect mechanisms used, the destination's cert age, and what the chain actually resolves to in a real browser. That's more information than most email security products surface in their incident workflows.

What it doesn't do

Honest about limitations:

  • It does not deobfuscate JavaScript before execution. It relies on the browser to actually run the code. A page that detects automated browser environments and refuses to execute won't be caught.
  • It does not authenticate. Some redirect endpoints only fire when the user is logged in. The tool can't follow those paths.
  • It does not do WHOIS lookups for domain registration date. Certificate age is a strong proxy for domain age in practice (phishing domains and their certs are usually created together), but it's not the same thing.
  • It does not take screenshots. This is intentional — screenshot capture is out of scope for the chain analysis, and there are other tools that do that better.
  • It does not handle cookie-based conditional redirects. A page that redirects only when certain cookies are set won't be detected.

Using it in practice

If you're a SOC analyst dealing with a user-reported phishing email, the workflow is:

  • Extract the URL from the email
  • Run ./redirect_analyzer.py --deep "<url>"
  • Read the chain output and risk signals
  • If risk score > 7, treat as high-confidence phishing and update blocklists / sandbox the destination

If you're a researcher analyzing a campaign, run the tool with --json and pipe to your data pipeline. The structured output includes every hop, every status code, every redirect mechanism, and the cert metadata.

If you're building a defensive tool, import the RedirectAnalyzer class as a library:

from redirect_analyzer import RedirectAnalyzerchr(10)chr(10)analyzer = RedirectAnalyzer(check_cert=True, render_js=True)chr(10)analyzer.trace("https://suspicious-url.example.com/")chr(10)chr(10)score = analyzer.score()chr(10)if score['risk_score'] > 7:chr(10)block(score['final_url'])

The planned LexLab AiTM Shield Chrome extension uses this same analysis logic client-side, warning users before they reach the phishing page rather than after.

Source

GitHub: lexlab/redirect-defense/tool

The tool is MIT licensed. Pull requests welcome — especially for additional redirect parameter names, additional encoding patterns, and additional bot/scanner User-Agent signatures.

The full research bundle (technique research, screenshots, validation scripts, this tool) is at lexlabtools.com/security/redirect-abuse. If you find a redirect technique we haven't documented, please open an issue with a reproducer.

Get the tool

Free, MIT licensed, runs on any Python 3 install. Pull requests welcome.

View on GitHub
Share:
Previous When Attackers Build Their Own Redirect Layer

More in Phishing Redirect Abuse