Late night coding session with terminal

Late Night Hunt: Three Targets, Two SSRFs, and the One That Matters

A late-night bug bounty session across three targets. Two SSRF findings, one dead end, and the lesson that bugs hide where two teams' assumptions diverge.

12:30 AM and the Chai is Cold Again

There is a particular kind of silence at 12:30 AM when the house is asleep and you are staring at curl output scrolling past, hunting for the one line that does not belong. Tonight was one of those nights.

I started the evening with a simple goal: find a real bug, on a real program, that pays real money. Not a theoretical finding. Not an overclaimed CVSS score. Something that would survive triage.

I hit three targets. Two fought back hard. One gave up a secret.

Target 1: The Documentation Platform

Started with Mintlify. A documentation platform used by Perplexity, Vercel, Replit. Self-published bug bounty, CVSS 4+ guaranteed payout. Looked promising.

160 subdomains. 20 live hosts. Express.js API behind Cloudflare and AWS ALB. Every single API endpoint returned 401 Unauthorized. Stytch authentication, Algolia search keys properly scoped to analytics only. I found their chat widget API key hardcoded in client-side JavaScript, got excited for about thirty seconds, then realized it was org-scoped with no deployments. By design.

Dead end. Ninety minutes of recon for exactly zero findings. This is the part they don't put in the writeups.

Target 2: The Vector Database

Qdrant Cloud. A vector database for AI applications. Free tier, no credit card. Connect-RPC API. I created an account using a disposable email, automated the Auth0 passwordless login, and got into the dashboard.

I tested everything. IDOR on accounts, clusters, roles, quotas. Every single one returned 403 permission denied. JWT algorithm none attack. Rejected. Common secret brute-force. Rejected. Stored XSS in account names, collection names, vector metadata payloads. All escaped by React's default rendering. Even the Qdrant cluster dashboard UI properly escaped data.

Then I found the snapshot recovery endpoint. PUT /collections/{name}/snapshots/recover accepts a location parameter with a URL. The server fetches it and tries to parse it as a tar archive.

I pointed it at localhost:6333. The error message came back: Failed to download tar from http://127.0.0.1:6333/collections: status - 401 Unauthorized.

The server made the request. SSRF confirmed.

I mapped the internal services. Port 6333 was the Qdrant API. Port 9090 returned actual data that the server tried to parse as tar. The /cluster endpoint leaked internal Kubernetes pod DNS names. I used those DNS names to target the internal metrics service via SSRF.

But I could not exfiltrate the actual content. The response went into a tar parser and all I got back was error messages. Blind SSRF with port scanning capability but no data exfiltration. I submitted it knowing it was Medium severity at best.

Target 3: The File Upload Platform

Uploadcare. A file upload and CDN platform used by Meta, Mozilla, Soundcloud. Self-published bug bounty. The attack surface made my pulse quicken: file uploads, URL fetching, image processing, webhooks.

I tested the obvious vector first. The from_url upload endpoint that fetches files from URLs. Private IPs? Only public IPs are allowed. Hex IP bypass? Blocked. Decimal? Blocked. Redirect to internal via httpbin? They validate the redirect destination too. Every bypass I knew was already patched.

I tested SVG upload with embedded JavaScript. Stored on the CDN with Content-Type: image/svg+xml, rendered inline. But the Content-Security-Policy header killed it: sandbox; script-src none. The script tag was there in the DOM but it could never execute.

HTML uploads? Served with Content-Disposition: attachment. The browser downloads instead of rendering. Every angle was covered.

Then I looked at webhooks.

The Inconsistency

The webhook creation endpoint. POST /webhooks/ with a target_url parameter. I set it to http://169.254.169.254/latest/meta-data/iam/security-credentials/ and hit enter.

200 OK. Webhook created.

No IP validation. No private range check. No nothing.

The same codebase that carefully validates every URL in from_url, that blocks hex IPs and decimal IPs and redirect chains and IPv6 mapped addresses, does absolutely nothing when you create a webhook.

I created a webhook pointing to webhook.site to confirm delivery. Uploaded a file. Three seconds later, a POST request arrived from 52.7.34.189. An AWS IP. Uploadcare's production server, dutifully sending webhook payloads to whatever URL you give it.

The same delivery system handles all webhooks. The one pointing to webhook.site. The one pointing to 169.254.169.254. The one pointing to 127.0.0.1. No discrimination.

The Validation

Before I got excited, I ran my kill gate. Five questions I force myself to answer before presenting any finding.

Is this by design? No. The from_url endpoint proves they know about SSRF and actively prevent it. The webhook gap is an oversight.

Is it already public? No.

Can an attacker cause real harm right now? The webhook delivery system makes real HTTP POST requests to the target URL. If the AWS metadata endpoint is reachable from the webhook workers, an attacker could obtain temporary IAM credentials.

I ran it through a second opinion. The assessment came back: valid, CVSS 7.7, submit immediately.

I submitted the report. Then I deleted every internal-targeting webhook I had created.

What I Learned Tonight

The bug was not in the obvious place. The file upload API was locked down tight. The CDN had proper CSP. The image processor validated all inputs. The SSRF protection on from_url was textbook.

But security is a chain. The webhook system was built by a different team, at a different time, with different assumptions. The URL validation that protects from_url was never applied to webhook target URLs. One feature got the security review. The other did not.

This is always where the bugs are. Not in the front door. In the side entrance that was added six months later by someone who did not know about the front door's locks.

Three targets. Two SSRFs found. One that actually matters. The chai is cold and it is almost 1 AM and I think that is enough for tonight.

The truth is a lion. You don't have to defend it. Let it loose. It will defend itself.

Good night.

-- Seetha

No comments yet