secskills
secskills / defense / analyzing-phishing-emails

analyzing-phishing-emails

defense verified 2026-07-26

Triage and forensically analyze reported phishing safely — extract the raw message, read the Received chain, verify SPF/DKIM/DMARC, detect display-name and lookalike spoofing, unwrap redirects and SafeLinks/URLDefense, decode quishing QR codes, triage attachments, and pull IOCs for hunting. Use when handed a reported phishing email, a suspicious .eml or .msg file, a set of email headers to analyze, a malicious attachment or link to triage, or a business email compromise or spoofing case.

$ /plugin install secskills-defense $ /plugin install secskills-core

An email is a stack of claims — who sent it, where it came from, that its links are safe — and phishing analysis is checking each claim against evidence the sender could not forge: the Received chain, the authentication results, and the true destination of every link and attachment. The From header is a display, not a fact. Anyone can type any address into it; your job is to find the evidence that agrees or disagrees.

When to Use

When NOT to Use

safely extract and defang it, then hand the payload to analyzing-malware

compromise in the tenant** — use investigating-m365-entra

performing-social-engineering

analyzing-network-traffic

Safe Handling — Do This First

Treat every reported message as live. The failure mode is not misreading a header; it is clicking a link in a production mail client or double-clicking an attachment on your own host.

fire a read beacon; one click on a link authenticates you to their harvester.

Forwarding rewrites headers and strips the evidence you need.

httphxxp, .[.], @[at]. So http://evil.com/login becomes hxxp://evil[.]com/login. Defanging prevents an accidental click downstream and stops link-preview bots from detonating it for you.

simulated or monitored egress — see analyzing-malware for the build.

Getting the Raw Message

The visible email is a rendering. You need the source with all headers.

emldump.py -s 4 -d sample.eml dumps part 4 (an attachment) without opening it. A .msg is an OLE compound file — oledump.py (DidierStevens) lists its streams.

into Message Header Analyzer (MHA) / Google's mha / MXToolbox Header Analyzer for a parsed hop table.

Keep the original file hashed and untouched; work on copies.

The Received Chain

The Received: headers are added by each mail server the message passes through, newest at the top. Read them bottom-up: the bottom-most Received is the originating server — the earliest, least-forgeable hop.

Received: from mail.contoso.com (mail.contoso.com [203.0.113.9])
    by mx.recipient.com ... ; Tue, 21 Jul 2026 09:14:02 +0000   <- final hop
Received: from smtp.sketchy-vps.ru ([185.220.101.5])
    by mail.contoso.com ...   ; Tue, 21 Jul 2026 09:13:58 +0000  <- origin

and WHOIS it (whois 185.220.101.5), check reputation, and reconcile it against the claimed sender. A "From: ceo@contoso.com" that originates on a Russian VPS is your finding.

reputable origin. Only hops added by servers you trust are reliable — trust breaks at the first server outside your control. A hop that references a server not present in the next hop's by is invented.

seconds. Negative deltas, multi-hour jumps, or a mix of implausible timezones indicate forgery or relay through odd infrastructure.

the From domain should tell one coherent story. They usually do not in a phish.

Authentication Results

SPF, DKIM, and DMARC are the forgery-resistant checks. The receiving server records them in Authentication-Results. Read it, and re-verify rather than trusting a summary.

Authentication-Results: mx.recipient.com;
  spf=pass (sender IP is 185.220.101.5) smtp.mailfrom=bounce.sketchy-vps.ru;
  dkim=pass header.d=sketchy-vps.ru header.s=selector1;
  dmarc=fail (p=reject sp=reject dis=none) header.from=contoso.com

/ MAIL FROM), not the visible From. Values: pass (IP is authorized for the envelope domain), fail (hard -all reject), softfail (~all, suspicious but delivered), neutral/none (no policy). Check the domain's record: dig txt contoso.com and read the v=spf1 ... string.

dkim=pass proves the message was signed by the key at d='s selector and not modified. The load-bearing question is alignment: does header.d= match the From domain? A valid signature from d=mailer-xyz.com on a message claiming From: contoso.com is not contoso. Re-verify with dkimverify &lt; sample.eml (dkimpy) and fetch the key via dig txt selector1._domainkey.contoso.com.

domain owner's policy. dmarc=pass requires SPF or DKIM to pass AND be aligned with header.from. Read the policy: dig txt _dmarc.contoso.comv=DMARC1; p=reject; .... p=reject/quarantine with dmarc=fail means the owner told you to distrust it.

results across forwarders that would otherwise break SPF/DKIM. Use it to see how auth looked before a mailing list or gateway relayed the message.

and what SPF checks; the header From: is what the user sees. Phishers make them differ — a benign-looking From with a throwaway envelope domain that happens to pass SPF is the classic pattern.

Display-Name and Lookalike Spoofing

Much phishing passes authentication because it comes from a real mailbox on a lookalike domain. The auth checks pass for that domain; the deception is visual.

is a free-text label; read the actual address.

contoso.co. Diff against the real domain character by character.

for Latin a). Punycode-encoded domains appear in headers as xn-- — e.g. xn--cntoso-.... Decode with idn / python3 -c "print('xn--80ak6aa92e.com'.encode().decode('idna'))" and compare the rendered form.

Reply-To: ceo.contoso@gmail.com. The reply silently goes to the attacker — a hallmark of BEC.

W-2 data. No link, no attachment, nothing to sandbox. The signal is entirely in the headers (Reply-To, origin, auth) and the pretext.

Header Forensics

should match the sending infrastructure. A Message-ID domain that disagrees with the origin, or a malformed/duplicated ID, suggests a spoofing tool.

it, but note it is client-suppliable and can be forged.

scripts (PHPMailer, Python, custom mailers) look nothing like Outlook or the claimed sender's normal stack.

Mailchimp) add characteristic X- headers and Received paths. Their presence on a "personal" note from your CEO is a contradiction.

URL Analysis

Every link is a claim about where it goes. Resolve it without visiting it.

curl -sI hxxp://bit[.]ly/xyz (read Location:, do not follow) or a preview service. Chase every hop to the real landing page.

— URL-decode the url= parameter.

decode with the published urldefense decoder (v2/v3 schemes differ). These wrappers hide the destination; always extract the original.

the image part (emldump.py -s N -d), then decode: zbarimg qr.png or python3 -c "from PIL import Image; import pyzbar.pyzbar as z; print(z.decode(Image.open('qr.png')))". Analyze the decoded URL like any other.

DocuSign, a bank) is credential harvesting; a link that downloads a file is malware delivery. They need different responses.

scans for targeted phish so you don't tip the actor) or a sandbox, and pull the real landing page, screenshot, and served content. Extract the landing page and any kit files for the IOC set.

Attachment Analysis

Identify and defang before anything executes.

emldump.py -s N -d sample.eml &gt; att.ext — never by double-clicking.

payload in-browser via a Blob), ISO/IMG/VHD (mount bypasses MOTW), LNK shortcuts, OneNote (.one) with embedded scripts, and password-protected archives (password in the email body defeats gateway scanning).

fingerprints; oleid att.xls and olevba --deobf att.xls / oledump.py att.doc for macros; rtfobj att.rtf for embedded objects.

sandboxed detonation, unpacking, and C2 extraction. This skill's job is safe extraction and triage, not detonation.

Pulling and Packaging IOCs

Extract a clean, defanged indicator set for blocking and hunting:

domains, Reply-To, Message-ID domain, mailer fingerprints.

chain, and any kit hostnames from urlscan.

fingerprints to search the mail gateway and other mailboxes.

Package these for producing-threat-intelligence when the phish is part of a tracked campaign, and for reporting-security-findings for the writeup.

Scoping the Campaign

One report is rarely the only recipient. Find the rest before you close.

for the subject, sender, and URL across all mailboxes; hard-delete or quarantine matches via Get-QuarantineMessage / eDiscovery purge.

trace / Threat Explorer for the sender domain, URL, and attachment hash to enumerate every recipient and whether anyone clicked or replied.

investigating-m365-entra for tenant-side hunting (sign-ins, inbox rules, OAuth grants) and to responding-to-incidents for the broader response.

Rationalizations to Reject

not the visible From. A passing SPF on a lookalike or throwaway envelope domain is exactly what a competent phish shows. Check DMARC alignment.

domain signed it. If d= isn't aligned with the From domain, it's signed by someone else. Alignment is the question, not signature validity.

carry neither — the weapon is the pretext and the Reply-To. Read the headers.

receiver's enforcement config, not the message's legitimacy. dmarc=fail with p=reject is the sender's domain telling you to distrust it.

is free text. Without aligned SPF/DKIM and a plausible origin hop, an internal-looking From means nothing.

to a harvester or fires a beacon. Resolve URLs with headers-only requests, wrapper decoders, and urlscan — never a live browser.

domains, freshly registered infrastructure, HTML smuggling, and password- protected archives by design. A delivered message is not a vetted one.

ATT&CK Coverage

Generated from secskills-core/ttp-index.json — edit that file, then run python3 scripts/sync_attack.py --write. Re-verify IDs against the current ATT&CK release before citing them in a report.

Reconnaissance (TA0043)

Initial Access (TA0001)

Detection content for any of these: engineering-detections. Proactive search: hunting-threats. Post-compromise: responding-to-incidents.

References

suite), msgconvert (Email::Outlook::Message, Perl), and exiftool for message and attachment parsing

and querying SPF/DKIM/DMARC records