phishing-identification.md (21705B)
1 --- 2 title: "Phishing Identification" 3 description: "Identify phishing sites and malicious links: URL/domain analysis, indicators and triage workflow." 4 category: web 5 tags: [web, phishing, osint, defense] 6 tools: [urlscan, VirusTotal] 7 difficulty: intermediate 8 updated: "2026-08-09" 9 source: "vault:Web/Phishing Site & Link Identification - Cheat Sheet.md" 10 --- 11 12 # Phishing Identification 13 14 ### Defensive triage for suspicious URLs, domains and landing pages 15 16 Covers: URL anatomy, lookalike domains, punycode/IDN, redirect chains, header and SPF/DKIM/DMARC checks, WHOIS and DNS, certificate transparency, safe fetching and detonation. 17 18 --- 19 20 ## Golden Rules 21 22 > **Warning — Handle every unverified URL as live malware.** 23 > - Never open a suspicious link in your daily-driver browser or on a host with credentials on it. Use a disposable VM, and route through a network you do not mind burning. 24 > - Fetching a URL leaks your IP and often a unique token embedded in the link, which confirms to the operator that the target is live. Prefer passive lookups first. 25 > - Judge the registrable domain, never the display text, the path, the favicon or the branding. 26 > - HTTPS and a padlock prove nothing. Free DV certificates mean the overwhelming majority of phishing sites are served over TLS. 27 > - If a page asks for credentials, MFA codes or a card number, navigate to the service yourself from a known-good bookmark instead. 28 29 --- 30 31 ## 1. URL Anatomy — Where to Actually Look 32 33 ```text 34 https://accounts.google.com.verify-login.ru:8443/signin?token=abc#/ 35 └─┬─┘ └──────────────┬──────────────────────┘└─┬┘└──┬─┘└───┬───┘ 36 scheme host (read RIGHT to LEFT) port path query 37 ``` 38 39 The only part that matters for identity is the registrable domain, the last two labels before the public suffix. Read the host from right to left, stopping at the first `/`. 40 41 | URL | Registrable domain | Verdict | 42 |---|---|---| 43 | `https://accounts.google.com/signin` | `google.com` | Legitimate | 44 | `https://accounts.google.com.verify-login.ru/` | `verify-login.ru` | Phish — brand is a subdomain | 45 | `https://google.com.evil.co/` | `evil.co` | Phish | 46 | `https://secure-google.com/` | `secure-google.com` | Phish — hyphenated lookalike | 47 | `https://google.com@evil.co/` | `evil.co` | Phish — everything before `@` is userinfo | 48 | `https://sites.google.com/view/login-x` | `google.com` | Legitimate host, abused hosting | 49 50 Extract the host programmatically rather than trusting your eyes: 51 52 ```bash 53 # Pull scheme, host, path out of a URL without fetching it 54 print -r 'https://accounts.google.com.verify-login.ru/signin' | \ 55 python3 -c 'import sys,urllib.parse as u; p=u.urlparse(sys.stdin.read().strip()); print("host:",p.hostname,"\nport:",p.port,"\npath:",p.path,"\nuser:",p.username)' 56 ``` 57 58 ```bash 59 # Registrable domain (eTLD+1) using the public suffix list 60 uv venv .venv && source .venv/bin/activate 61 uv pip install tldextract 62 python3 -c 'import tldextract,sys; e=tldextract.extract(sys.argv[1]); print(e.registered_domain)' \ 63 'https://accounts.google.com.verify-login.ru/signin' 64 ``` 65 66 > **Tip — Common obfuscations** 67 > - `@` userinfo trick: browser goes to whatever follows the `@`. 68 > - Decimal, octal or hex IPs: `http://2130706433/` is `127.0.0.1`. 69 > - Percent-encoding of the host or path to hide keywords. 70 > - Very long paths padding the real domain off the end of a mobile URL bar. 71 > - Data URIs and `blob:` URLs rendering a login form with no remote host at all. 72 73 --- 74 75 ## 2. Domain Red Flags 76 77 | Signal | Why it matters | How to check | 78 |---|---|---| 79 | Registered in the last 30 days | Phishing infra is disposable and short-lived | `whois` creation date | 80 | Brand name as a subdomain or in the path | Legitimate brands own their apex | Read host right to left | 81 | Hyphenated brand combos (`paypal-secure-login`) | Cheap way to look plausible | Visual | 82 | Unusual TLD for the brand (`.zip`, `.mov`, `.top`, `.cf`, `.xyz`) | Cheap or free registration | Visual | 83 | Free hosting or dev platform subdomains | Abused for zero-cost hosting with valid TLS | Check apex against known SaaS | 84 | Privacy-shielded WHOIS on a "corporate" login page | Real brands do not hide registrant data | `whois` | 85 | Wildcard DNS answering every subdomain | Per-victim subdomains | `dig random.$domain` | 86 | Hosting ASN mismatched with the brand | Bulletproof or cheap VPS ranges | `whois <ip>` | 87 | Open directory listing or `/.git` exposed | Sloppy kit deployment | Manual, in a VM | 88 89 Legitimate-but-abused hosting worth recognising: `*.web.app`, `*.firebaseapp.com`, `*.pages.dev`, `*.workers.dev`, `*.r2.dev`, `*.blob.core.windows.net`, `*.s3.amazonaws.com`, `*.weeblysite.com`, `*.glitch.me`, `sites.google.com/view/...`, `*.notion.site`, IPFS gateways. The apex is genuine, so reputation feeds often miss them. 90 91 --- 92 93 ## 3. Homoglyph & Punycode Detection 94 95 Internationalised domains let attackers register visually identical names. Browsers show punycode as `xn--` only in some cases, so decode explicitly. 96 97 ```bash 98 # Decode punycode to the real Unicode label 99 python3 -c 'print("xn--80ak6aa92e".encode().decode("idna"))' # -> аррӏе (Cyrillic) 100 101 # Encode a suspect Unicode host to see its punycode form 102 python3 -c 'print("аррӏе.com".encode("idna").decode())' 103 ``` 104 105 ```bash 106 # Flag any non-ASCII characters in a host, and name the script of each 107 python3 - <<'PY' 108 import unicodedata 109 host = "аррӏе.com" 110 for ch in host: 111 if ord(ch) > 127: 112 print(f"{ch!r} U+{ord(ch):04X} {unicodedata.name(ch)}") 113 PY 114 ``` 115 116 Mixed-script hosts (Latin plus Cyrillic or Greek in one label) are almost always hostile. Classic swaps to watch for: 117 118 | Looks like | Actually | Codepoint | 119 |---|---|---| 120 | `a` | Cyrillic а | U+0430 | 121 | `e` | Cyrillic е | U+0435 | 122 | `o` | Cyrillic о | U+043E | 123 | `p` | Cyrillic р | U+0440 | 124 | `i` / `l` | Cyrillic ӏ, Turkish ı | U+04CF, U+0131 | 125 | `rn` | reads as `m` at small sizes | ASCII only | 126 | `vv` | reads as `w` | ASCII only | 127 | `1` / `l` / `I` | font-dependent confusion | ASCII only | 128 129 Generate and check typosquats around a brand you protect: 130 131 ```bash 132 # dnstwist enumerates permutations and resolves the live ones 133 uv pip install dnstwist 134 dnstwist --registered --mx --format cli example.com 135 ``` 136 137 --- 138 139 ## 4. Unwrapping Redirects & Shorteners 140 141 Resolve the chain without executing anything. Prefer `HEAD` and never follow blindly into a download. 142 143 ```bash 144 # Show every hop, headers only, no body, no auto-follow of unsafe schemes 145 curl -sIL --max-redirs 10 --max-time 15 -A 'Mozilla/5.0' 'https://short.link/abc' \ 146 | grep -Ei '^(HTTP/|location:)' 147 ``` 148 149 ```bash 150 # One hop at a time, so you can bail out 151 curl -sI 'https://short.link/abc' | grep -i '^location:' 152 ``` 153 154 Many shorteners expose a preview or API that avoids touching attacker infra at all: 155 156 | Service | Preview method | 157 |---|---| 158 | bit.ly | append `+` to the URL | 159 | tinyurl.com | `https://preview.tinyurl.com/<code>` | 160 | ow.ly, buff.ly | Bitly-family, `+` often works | 161 | t.co | `curl -sI` returns `location` without rendering | 162 163 Unwrap corporate link-rewriting so you see the real destination: 164 165 ```bash 166 # Proofpoint URLDefense v3, Microsoft Safe Links, Barracuda etc. all URL-encode the original 167 python3 -c 'import sys,urllib.parse as u; q=u.parse_qs(u.urlparse(sys.argv[1]).query); print(q.get("url",[""])[0])' \ 168 'https://eur01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fevil.co%2Flogin&data=...' 169 ``` 170 171 > **Warning —** Every link in a phish is usually unique per recipient. Fetching it tells the operator your address is live and may burn the sample before analysis. 172 173 --- 174 175 ## 5. Email Header & Auth Triage 176 177 Get the original headers, not a forward. In Gmail use "Show original", in Outlook "View source", and save the `.eml` intact. 178 179 What to read, in order: 180 181 1. `From:` display name versus the actual address in angle brackets. 182 2. `Return-Path:` / envelope sender. A mismatch with `From:` is normal for mailing lists but suspicious for a bank. 183 3. `Reply-To:` pointing somewhere unrelated is a strong lure signal. 184 4. `Authentication-Results:` for SPF, DKIM and DMARC verdicts. 185 5. Earliest `Received:` hop, which shows the true origin before the receiving infra. 186 6. `Message-ID` domain matching the sending domain. 187 188 ```bash 189 # Pull the auth verdicts and the sender fields out of a saved .eml 190 grep -Ei '^(authentication-results|received-spf|dkim-signature|from|reply-to|return-path|message-id):' sample.eml 191 ``` 192 193 ```bash 194 # Parse an .eml properly, including nested parts and URLs in the body 195 python3 - <<'PY' 196 import email, re 197 from email import policy 198 m = email.message_from_file(open("sample.eml"), policy=policy.default) 199 for h in ("From","Reply-To","Return-Path","Subject","Date","Authentication-Results","Message-ID"): 200 print(f"{h}: {m.get(h)}") 201 body = "".join(p.get_content() for p in m.walk() if p.get_content_type() in ("text/plain","text/html")) 202 for url in sorted(set(re.findall(r'https?://[^\s"\'<>)]+', body))): 203 print("URL:", url) 204 PY 205 ``` 206 207 Interpreting the verdicts: 208 209 | Result | Meaning | Weight | 210 |---|---|---| 211 | `spf=fail` + `dkim=fail` + `dmarc=fail` | Spoofed sending domain | Strong | 212 | `spf=pass` on an attacker-owned lookalike domain | Auth passes for *their* domain, proves nothing about the brand | Neutral, common | 213 | `dkim=pass` with `d=` not matching the `From:` domain | Unaligned DKIM, DMARC will not pass on it | Suspicious | 214 | `dmarc=pass` | Aligned and authenticated for the `From:` domain | Reassuring, not conclusive if the account is compromised | 215 216 ```bash 217 # Check what the claimed domain publishes 218 dig +short TXT example.com | grep -i spf 219 dig +short TXT _dmarc.example.com 220 dig +short TXT selector1._domainkey.example.com 221 ``` 222 223 > **Note —** Business email compromise sends from a genuinely owned, fully authenticated mailbox. Auth passing is not innocence. Weight the request itself: payment redirection, urgency, secrecy, out-of-band contact. 224 225 --- 226 227 ## 6. WHOIS & DNS Checks 228 229 ```bash 230 # Registration age is the single highest-signal indicator 231 whois evil-login.co | grep -Ei 'creation|created|registered|registrar|registrant|name server' 232 ``` 233 234 ```bash 235 # Resolution and infrastructure 236 dig +short A evil-login.co 237 dig +short NS evil-login.co 238 dig +short MX evil-login.co # MX present = capable of receiving replies 239 dig +short TXT evil-login.co 240 241 # Wildcard test: does a random subdomain resolve? Per-victim subdomains are a kit tell 242 dig +short "$(openssl rand -hex 6).evil-login.co" 243 244 # Who owns the hosting 245 whois "$(dig +short A evil-login.co | head -1)" | grep -Ei 'orgname|netname|country|origin' 246 ``` 247 248 ```text 249 # Passive DNS style pivot: what else is on that IP (use a service, do not scan) 250 # See section 11 for tooling. Shared cheap hosting will show hundreds of unrelated domains. 251 ``` 252 253 Age heuristic worth internalising: a "Microsoft account security" page on a domain created 4 days ago with a privacy-shielded registrant and a Let's Encrypt certificate issued the same day is phishing until proven otherwise. 254 255 --- 256 257 ## 7. TLS Certificate & CT Logs 258 259 ```bash 260 # Inspect the presented certificate without loading the page 261 echo | openssl s_client -connect evil-login.co:443 -servername evil-login.co 2>/dev/null \ 262 | openssl x509 -noout -subject -issuer -dates -ext subjectAltName 263 ``` 264 265 What to read: 266 267 | Field | Phishing tell | 268 |---|---| 269 | `notBefore` | Issued hours or days ago | 270 | Issuer | Free DV CA on a page impersonating a bank | 271 | Subject | `CN` is the lookalike domain, no organisation details | 272 | SAN list | Dozens of unrelated brand-ish hostnames on one cert | 273 274 Certificate Transparency is a free, passive early-warning source for lookalikes of a domain you own: 275 276 ```bash 277 # All certs ever issued for a domain and its subdomains, from CT logs 278 curl -s 'https://crt.sh/?q=%25.example.com&output=json' \ 279 | python3 -c 'import sys,json; [print(r["name_value"].replace("\n",","), r["not_before"]) for r in json.load(sys.stdin)]' \ 280 | sort -u | head -50 281 ``` 282 283 Search CT for brand permutations (`example-secure`, `examp1e`, `example-login`) to catch infrastructure before the campaign launches. 284 285 --- 286 287 ## 8. Safe Fetching of Page Content 288 289 Passive first. If you must fetch, do it from an isolated VM or a cloud sandbox, never your host. 290 291 ```bash 292 # Headers only, no body executed, short timeout, no cookies stored 293 curl -sI --max-time 10 -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' 'https://evil-login.co/' 294 ``` 295 296 ```bash 297 # Fetch the raw HTML to a file for offline inspection, do not open it in a browser 298 curl -s --max-time 15 -A 'Mozilla/5.0' 'https://evil-login.co/' -o page.html 299 file page.html && wc -c page.html 300 ``` 301 302 ```bash 303 # Extract form targets, external scripts and iframes from the saved HTML 304 python3 - <<'PY' 305 import re 306 h = open("page.html", encoding="utf-8", errors="replace").read() 307 for label, pat in [("FORM ACTION", r'<form[^>]*action=["\']([^"\']+)'), 308 ("SCRIPT SRC", r'<script[^>]*src=["\']([^"\']+)'), 309 ("IFRAME SRC", r'<iframe[^>]*src=["\']([^"\']+)'), 310 ("INPUT NAME", r'<input[^>]*name=["\']([^"\']+)')]: 311 for m in sorted(set(re.findall(pat, h, re.I))): 312 print(f"{label}: {m}") 313 PY 314 ``` 315 316 > **Tip —** The form `action` is the payoff. A login page whose form posts to an unrelated domain, a raw IP, a `.php` on cheap hosting, or a Telegram bot API endpoint is conclusive. 317 318 Server-side cloaking is standard, so a plain `curl` often returns a benign decoy. Attacker kits filter on User-Agent, `Referer`, geolocation, ASN (blocking known security vendors) and sometimes require the unique token from the original link. Getting a harmless page back does not clear the URL. 319 320 --- 321 322 ## 9. Landing Page Tells 323 324 Observed in an isolated VM, or from saved HTML. 325 326 - Form posts to a different domain than the one in the address bar. 327 - Credentials submitted, then a redirect to the real site's genuine login page, so the victim assumes a mistyped password. 328 - Password field with autocomplete disabled and no "forgot password" or account-creation flow that actually works. 329 - Requests for data the real service would never ask for together: password plus MFA code plus card number plus mother's maiden name. 330 - MFA relay kits (Evilginx, EvilProxy, Tycoon) proxy the real site live, so the page is pixel-perfect and the TLS is valid. The domain is your only reliable tell. 331 - Right-click, view-source or devtools disabled via JavaScript. 332 - Blocked or broken links for everything except the login form. 333 - Base64 or heavily obfuscated inline JavaScript that assembles the form at runtime. 334 - The brand logo hotlinked from the genuine CDN while everything else is local. 335 - Fake browser chrome drawn in HTML, a "browser in the browser" popup simulating an OAuth window. Try to drag it outside the page, a real window can leave, a fake one cannot. 336 - QR codes in the email body ("quishing") to move the click onto an unmanaged mobile device. Decode offline before scanning: 337 338 ```bash 339 uv pip install "qreader" opencv-python-headless 340 python3 -c 'import cv2; d=cv2.QRCodeDetector(); print(d.detectAndDecode(cv2.imread("qr.png"))[0])' 341 # or 342 zbarimg --quiet --raw qr.png 343 ``` 344 345 --- 346 347 ## 10. Attachment Triage 348 349 Static inspection only, in a VM, never double-click. 350 351 ```bash 352 file suspicious.* 353 sha256sum suspicious.* # hash first, then look it up rather than uploading 354 ``` 355 356 ```bash 357 # Office documents: check for macros and embedded objects 358 uv pip install oletools 359 olevba -a suspicious.docm 360 oleid suspicious.doc 361 ``` 362 363 ```bash 364 # PDFs: look for JavaScript, auto-actions and embedded launches 365 uv pip install pdfid pdf-parser 366 pdfid.py suspicious.pdf # /JS /JavaScript /OpenAction /Launch /EmbeddedFile counts 367 ``` 368 369 ```bash 370 # Archives: list contents without extracting, watch for double extensions and LNK/ISO/IMG 371 unzip -l suspicious.zip 372 7z l suspicious.iso 373 ``` 374 375 High-risk containers used to defeat mark-of-the-web: `.iso`, `.img`, `.vhd`, `.7z`, password-protected `.zip` with the password in the email body, `.lnk`, `.chm`, `.one`, `.svg` with embedded script, `.html` smuggling attachments that rebuild a payload client-side. 376 377 > **Warning —** Hash first and search the hash. Uploading a targeted sample to a public multi-scanner makes it public and tips off the operator. 378 379 --- 380 381 ## 11. Reputation & Sandbox Services 382 383 | Service | Use | Notes | 384 |---|---|---| 385 | urlscan.io | Renders a URL, screenshots, DOM, request chain | Set scan to private for targeted phish. Public scans are searchable by anyone, including the attacker | 386 | VirusTotal | URL, domain, IP and file reputation | Search by hash before uploading. Uploads are shared with vendors | 387 | Hybrid Analysis / Joe Sandbox / ANY.RUN | Full detonation | Free tiers make results public | 388 | crt.sh | Certificate transparency search | Passive, free, no attacker contact | 389 | Shodan / Censys | Host and cert fingerprinting, pivot on kit artefacts | Passive | 390 | PhishTank / OpenPhish | Community phish feeds | Good for known campaigns, weak on fresh ones | 391 | Google Safe Browsing / Microsoft Defender SmartScreen | Browser-level blocklists | Lag of hours to days on new infra | 392 | Have I Been Pwned | Assess exposure after a credential submission | Post-incident | 393 394 Absence of detections means nothing on a domain registered this morning. Reputation feeds are lagging indicators. Registration age plus form target plus domain reading beat any single verdict. 395 396 --- 397 398 ## 12. Triage Workflow 399 400 ```text 401 1. PRESERVE Save the original .eml and the raw URL. Do not click anything. 402 2. PARSE Extract host, registrable domain, and every URL in the body. 403 3. READ DOMAIN Right to left. Decode punycode. Check for mixed scripts. 404 4. AGE IT whois creation date. Under ~30 days is a strong signal on its own. 405 5. AUTH SPF / DKIM / DMARC alignment against the claimed From: domain. 406 6. INFRA dig A/NS/MX, ASN owner, wildcard test, cert notBefore and issuer. 407 7. REPUTATION Hash and domain lookups. Passive sources first. 408 8. UNWRAP Resolve redirect chain with curl -sIL from an isolated host. 409 9. DETONATE Only if needed, in a VM or private urlscan. Note cloaking. 410 10. VERDICT Weight registration age + form target + domain reading above all else. 411 11. RESPOND Report, block, hunt for other recipients, rotate any exposed credentials. 412 ``` 413 414 If a credential was submitted, treat it as compromised immediately: change the password from a different device, revoke active sessions and refresh tokens (MFA relay kits steal the session cookie, so a password change alone is insufficient), re-enrol MFA, and check mailbox rules and OAuth app grants for attacker persistence. 415 416 --- 417 418 ## 13. Quick Reference Table 419 420 | Check | Command | 421 |---|---| 422 | Extract host from URL | `python3 -c 'import sys,urllib.parse as u;print(u.urlparse(sys.argv[1]).hostname)' "$URL"` | 423 | Registrable domain | `python3 -c 'import tldextract,sys;print(tldextract.extract(sys.argv[1]).registered_domain)' "$URL"` | 424 | Decode punycode | `python3 -c 'print("xn--...".encode().decode("idna"))'` | 425 | Redirect chain | `curl -sIL --max-redirs 10 "$URL" \| grep -Ei '^(HTTP/\|location:)'` | 426 | Domain age | `whois "$DOM" \| grep -Ei 'creation\|created'` | 427 | DNS records | `dig +short A "$DOM"; dig +short NS "$DOM"; dig +short MX "$DOM"` | 428 | Wildcard DNS test | `dig +short "$(openssl rand -hex 6).$DOM"` | 429 | Hosting owner | `whois "$(dig +short A "$DOM" \| head -1)" \| grep -Ei 'orgname\|netname'` | 430 | Cert details | `echo \| openssl s_client -connect "$DOM":443 -servername "$DOM" 2>/dev/null \| openssl x509 -noout -subject -issuer -dates` | 431 | CT log history | `curl -s "https://crt.sh/?q=%25.$DOM&output=json" \| jq -r '.[].name_value' \| sort -u` | 432 | SPF / DMARC published | `dig +short TXT "$DOM" \| grep -i spf; dig +short TXT "_dmarc.$DOM"` | 433 | Email auth verdicts | `grep -Ei '^(authentication-results\|received-spf\|from\|reply-to\|return-path):' sample.eml` | 434 | Save page HTML | `curl -s --max-time 15 -A 'Mozilla/5.0' "$URL" -o page.html` | 435 | Form targets | `grep -oEi '<form[^>]*action="[^"]+"' page.html` | 436 | Typosquat sweep | `dnstwist --registered --mx example.com` | 437 | File type + hash | `file f; sha256sum f` | 438 | Macro check | `olevba -a f.docm` | 439 | PDF actions | `pdfid.py f.pdf` | 440 | Decode QR | `zbarimg --quiet --raw qr.png` | 441 442 --- 443 444 ## 14. Reporting & Takedown 445 446 | Where | How | 447 |---|---| 448 | UK, general public | Forward the email to `report@phishing.gov.uk` (NCSC SERS). Suspicious texts to `7726` | 449 | UK, financial loss | Action Fraud, `actionfraud.police.uk` or 0300 123 2040. In Scotland, report to Police Scotland on 101 | 450 | Google Safe Browsing | `safebrowsing.google.com/safebrowsing/report_phish/` | 451 | Microsoft | `microsoft.com/wdsi/support/report-unsafe-site`, or the Report Phishing add-in | 452 | APWG | `reportphishing@apwg.org` | 453 | Hosting provider | `abuse@` for the ASN owner found via `whois <ip>` | 454 | Registrar | Abuse contact from `whois <domain>` | 455 | CDN in front of the site | Cloudflare and similar have their own abuse forms, they will pass to origin | 456 | Impersonated brand | Most banks and large SaaS publish a phishing reporting address | 457 458 Include the full URL, the original headers, timestamps with timezone, and the file hashes. Do not include live credentials. 459 460 --- 461 462 ## External References 463 464 - [NCSC — Phishing attacks: defending your organisation](https://www.ncsc.gov.uk/guidance/phishing) 465 - [RFC 7489 — DMARC](https://datatracker.ietf.org/doc/html/rfc7489) 466 - [Public Suffix List](https://publicsuffix.org/) 467 - [crt.sh — Certificate Transparency search](https://crt.sh/) 468 - [urlscan.io](https://urlscan.io/) 469 - [dnstwist](https://github.com/elceef/dnstwist) 470 - [oletools](https://github.com/decalage2/oletools)