A short note rather than a full write-up: a recent finding I think is worth sharing, mostly because of where the lesson actually was.

The target had a feature that lets you send emails and attach files to them. Normally the attachment is uploaded to cloud storage (think S3), and the request carries the object's URL; the server fetches that URL and delivers the content inside the email.

1. The first signal

I dropped a Burp Collaborator URL into the attachment field. A request came back to my Collaborator, and the source IP was clearly AWS. The detail that made me stop and spend real time here: the User-Agent was native Ruby, not a browser or a generic HTTP client. That is a server-side fetch whose target I control, which is exactly the shape of an SSRF.

2. What did not work

I ran the usual playbook against the fetcher:

  • AWS metadata at http://169.254.169.254/...
  • the file:// protocol
  • the other common LFI and SSRF tricks

None of it landed. There was a Cloudflare WAF in front that ate most of the obvious payloads.

3. What did work

The thing that finally worked was almost boring: I dropped the protocol entirely and gave it a bare file name / path, no file://, no scheme at all. The fetcher resolved it as a local path and the file's contents came back in the email.

So the attachment fetcher was happy to read local files, not just remote URLs. That quietly turns an email feature into a server-side file read.

ATTACHMENT FETCHER · NATIVE RUBY · ON AWS · SERVER-SIDE attackersends an email email + attachment APIfetches attachment URL server-side attacker inboxreceives the file attachment.url file content PROBES http://169.254.169.254/ · file:// · blocked by WAF /proc/self/environ · bare path, no protocol · read EXFILTRATED FROM /proc/self/environ AWS_SECRET_ACCESS_KEY DATABASE_URL Twilio · Stripe SendGrid RSA key ✕ a URL fetcher that resolves local paths is a file read - and the file body is the secrets store
Fig. 01 - the attachment fetcher accepted a bare local path instead of a URL, turning an email feature into a server-side file read. Reading /proc/self/environ returned the process environment and its secrets straight into the attacker's inbox.

4. Escalating to the real loot

The highest-impact read was the process environment, through:

/proc/self/environ

(equivalently /proc/[pid]/environ). This one slipped past the WAF where other payloads did not, and the environment of a running process is exactly where a modern app keeps its secrets.

5. The CJK encoding detour

The file arrived, but it rendered as Chinese, Japanese, and Korean (CJK) characters. The reason: environ separates its entries with null bytes, and somewhere in the mail pipeline that got mis-detected as a multi-byte encoding, so the null-separated bytes were reinterpreted as CJK code points. A small decode script put it back to readable KEY=value pairs.

6. Impact

The environment held the kind of keys that make this an infrastructure takeover rather than a single bug:

  • AWS_SECRET_ACCESS_KEY
  • Twilio, SendGrid, and Stripe API keys
  • database connection strings

With cloud credentials, mail / SMS / payment provider keys, and direct database access, the blast radius is the whole environment, not one application.

7. Why I wrote this

When I started out, my problem was almost never the vulnerability class or the theory. Labs are clean and obvious: they tell you the bug is there and roughly where. The hard part was always the other question: how does this actually show up in a modern application? Finding the loose thread was the difficult part; everything after it was easy.

That is the point of this note. The bug itself is textbook LFI/SSRF. The part worth keeping is the path to it: a Collaborator hit, a native-Ruby User-Agent that said "server-side fetch," a WAF that swallowed the obvious payloads, and a protocol-less path that did not look like an exploit at all.

8. Takeaways

  • A URL fetcher is an SSRF and LFI surface. Anything that turns user input into a server-side request deserves this attention, even a humble "attach a file" feature.
  • Strip the protocol. When file:// and friends are filtered, a bare path can still resolve. WAFs pattern-match on schemes and keywords far more than on plain paths.
  • /proc/self/environ is the prize. In containerised, env-var-driven apps it hands you the live secrets in a single read.
  • Mind the encoding. Null-byte-separated files can come back mangled (CJK here). Do not assume a "garbage" response is empty; decode it.

Thanks for reading.