September 26, 2026
Crashing a Webhook Endpoint by Disguising an IP Address โ $100 Bounty
What This Bug Is, in Plain Words

By Tonmoydatta
5 min read
What This Bug Is, in Plain Words
redacted.com has a feature called webhooks. A webhook is basically: "hey server, whenever something happens, send a message to this URL." Users get to type in that URL themselves.
That's dangerous if not handled carefully, because a bad actor could type in an internal URL โ like the server's own private network address โ and trick the server into calling itself or another internal system it shouldn't be able to reach. This attack is called SSRF (Server-Side Request Forgery). Because of that, redacted.com blocks obviously dangerous addresses, like:
127.0.0.1(localhost)192.168.1.1(a private network address)169.254.169.254(a special address cloud servers use to store secret credentials โ a favorite SSRF target)
They also blocked sneakier tricks people commonly use to disguise these addresses, like writing the IP as a plain number (2852039166) instead of the normal dotted format.
But there's one format they forgot: IPv6-mapped IPv4 addresses. This is a standard, official way of writing a regular IPv4 address, just wrapped in IPv6 formatting โ it looks like ::ffff:169.254.169.254 instead of 169.254.169.254. Same address, different outfit.
When I put that "disguised" address into the webhook URL field, their server didn't reject it politely like it did with every other trick. It crashed instead โ every single time.
How I Actually Found This (The Boring-But-Important Part)
I didn't guess this on day one. I went down a checklist of every disguise trick I know for sneaking a blocked IP address past a filter, and tried each one against the webhook feature, one at a time. Here's roughly what that looked like:
What I triedWhat happenedPlain private IP (192.168.1.1)Blocked โ clean error: "Invalid Hostname"Cloud metadata IP (169.254.169.254)Blocked โ same clean errorlocalhostBlocked โ different error: "resolves to a private address"A domain that secretly points to a private IP (DNS rebinding, e.g. 169.254.169.254.nip.io)Blocked โ same "resolves to private address" errorIP written as a plain number (2852039166)BlockedIP written in octal (0251.0376.0251.0376)BlockedIPv6 localhost (::1)Blocked โ "not found" errorIPv6-mapped IPv4 (::ffff:169.254.169.254)โ Not blocked. Server crashed instead.
Everything on that list got a normal, expected rejection message โ except the very last one. That's the moment worth paying attention to: not when something works, but when something fails differently than everything else. A different error usually means you've found a different code path โ one nobody tested.
I'll be honest โ keeping 8+ near-identical test requests straight, and noticing that one of them returned a genuinely different kind of error instead of the usual clean rejection, is easy to mess up when you're doing it by hand and moving fast. I used Claude throughout this process to help me log and compare each request/response pair side by side, which is really what let that one "odd one out" stand out instead of getting lost in a pile of near-identical failed attempts.
The Clear Proof of Concept (PoC)
Here's the exact shape of the request that triggered it (target replaced with redacted.com, token and IDs replaced with placeholders):
Request:
bash
curl -X POST "https://redacted.com/api/1.0/webhooks" \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"data": {
"resource": "<SOME_RESOURCE_ID>",
"target": "https://[::ffff:169.254.169.254]"
}
}'curl -X POST "https://redacted.com/api/1.0/webhooks" \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"data": {
"resource": "<SOME_RESOURCE_ID>",
"target": "https://[::ffff:169.254.169.254]"
}
}'Response (every single time):
json
{
"errors": [{
"message": "Oops! An unexpected error occurred while processing this request.
The input may have contained something the server did not know
how to handle.",
"phrase": "24 boring urchins hunt terribly brightly"
}]
}{
"errors": [{
"message": "Oops! An unexpected error occurred while processing this request.
The input may have contained something the server did not know
how to handle.",
"phrase": "24 boring urchins hunt terribly brightly"
}]
}Two details made this a solid, provable PoC rather than a one-off fluke:
- It was 100% repeatable. I sent this exact request many times, and it crashed every time โ not a random glitch.
- The
phrasevalue changed on every request. That random phrase is an internal error-tracking ID the server generates fresh each time something crashes. Seeing it change on every attempt proved I was hitting a live server and causing a real, fresh crash each time โ not just getting a cached error page shown to everyone.
To be extra thorough, I tried the same trick with a few other disguised addresses to confirm it wasn't a one-address fluke:
PayloadResulthttps://[::ffff:169.254.169.254]500 crashhttps://[::ffff:169.254.169.254]/latest/meta-data/500 crashhttps://[::ffff:127.0.0.1]500 crashhttps://[::ffff:192.168.1.1]500 crash
Same crash, every time, for every "disguised" private address. That confirmed the format itself was the problem, not something specific to one address.
The One Test That Mattered Most: Is Anything Actually Being Sent?
This is the step beginners most often skip, and it's the most important one. A crash is one thing โ but if the server was also secretly reaching out to that address before crashing, this becomes a much more serious bug (a real SSRF, capable of stealing cloud credentials).
So I set up my own personal server (a simple listener) using a public URL I controlled, and sent the same "disguised" trick pointing at that server instead:
bash
curl -X POST "https://redacted.com/api/1.0/webhooks" \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"data": {
"resource": "<SOME_RESOURCE_ID>",
"target": "https://[::ffff:103.99.129.60]"
}
}'curl -X POST "https://redacted.com/api/1.0/webhooks" \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"data": {
"resource": "<SOME_RESOURCE_ID>",
"target": "https://[::ffff:103.99.129.60]"
}
}'I got the same 500 crash โ but nothing ever arrived at my listener. That told me exactly where the bug lives: the crash happens before the server tries to make any outbound connection. Good news for redacted.com today, but still a real gap worth reporting (more on why below).
Why This Matters (Business Impact)
1. It's a hidden hole in their SSRF defenses. The whole point of blocking these addresses is to stop the server from being tricked into contacting places it shouldn't. This "disguised" format slips past both of their safety checks completely undetected. It's harmless today only because the crash happens first โ but it means their filter has a blind spot nobody knew about. If the code is ever changed so the connection attempt happens even slightly earlier, that blind spot becomes a working attack path straight to internal systems or cloud credentials, with no extra bypass work needed.
2. It's a free "break this feature" button. Any regular signed-up user โ no special access needed โ can send this one request and reliably crash the webhook registration feature on demand. That's a denial-of-service: legitimate users trying to set up webhooks would get blocked by these crashes, and it creates unnecessary alert noise for the engineering team, over something that isn't even a real infrastructure problem.
3. Small information leak. Each crash message includes that unique tracking phrase. On its own it's harmless, but it confirms externally that a real production server was reached and crashed โ which is a small piece of internal information a company generally wouldn't want an outsider to be able to confirm from the outside.
The Fix
The core problem: their filter checks "is this a plain private IP?" and separately "does this hostname resolve to a private IP?" โ but it never converts a "disguised" IPv6-wrapped address back into its plain IPv4 form before checking. The fix is to always unwrap that disguise first, then run the exact same private-address check they already have. In plain terms: normalize the address into one standard shape before deciding whether to trust it, instead of trying to recognize every possible disguise separately.
Why This Was Rated Low-Medium, Not Critical
It's natural to see "AWS metadata IP" and assume this is a huge deal. It's not โ yet โ for one specific reason: I proved no actual outbound connection happens. Severity in bug bounty is judged on confirmed impact, not worst-case theory. If a future update accidentally lets that outbound call go through, this exact bug becomes a credential-stealing SSRF overnight โ which is exactly why it's worth reporting and fixing now, even though today it "only" crashes a server.
Takeaways for Beginner Hunters
- A checklist beats a lucky guess. This bug wasn't found by being clever once โ it was found by trying every known IP-disguise trick, one by one, until one of them behaved differently.
- Pay attention to different failures, not just failures. Nine payloads getting rejected the same way is boring. The tenth payload getting rejected in a new way is the one worth chasing.
- Always check "did anything actually happen" before reporting. Pointing the same trick at a server you personally control is a simple, safe way to prove (or disprove) that something more dangerous is happening behind the scenes โ and it massively changes how serious your report is.
- Good bookkeeping is half the work. A lot of real bug hunting is just carefully tracking a dozen near-identical requests and responses so you actually notice the one that's different. Don't be afraid to use tools (I used Claude) to help you keep that organized.
If you're testing any feature that accepts a URL, add "IPv6-mapped IPv4" (::ffff:x.x.x.x) to your bypass checklist, right alongside decimal IPs, octal IPs, and DNS rebinding tricks. It's an easy one for developers to miss.