[PlfanzenCTF] [WEB] Unauthentische Rache

This was three HTTP calls because the consent step was missing twice.

The exploit looked almost too small at the end, but the interesting part was figuring out why it was allowed at all.

The challenge had three moving pieces. Authentik was the identity provider. The flag dispenser owned the flag and only returned it for akadmin in the authentik Admins group. The admin bot was already logged into Authentik as that user and exposed one endpoint that visited any URL I gave it.

That setup immediately made me look for an OAuth or SSO flow where simply visiting a URL has side effects. Device Code flow is exactly that kind of thing if the confirmation page is misconfigured.

The clue trail

The flag dispenser's root route was basically a hint disguised as product requirements:

Code:
"Please create a nice looking frontend for this device code login flow. Display the code and also a QR code that the user can scan to immediately log in."

That told me the intended object was not a normal authorization-code redirect. It was the device endpoint. In flag-dispenser/app.py, /start calls Authentik's device endpoint and returns the whole response:

Code:
DEVICE_ENDPOINT = f"{AUTHENTIK_URL}/application/o/device/"

The response includes device_code and verification_uri_complete. The second one is the dangerous value. It is the verification page URL with the user code already filled into the query string.

On a properly cautious setup, that should still require a logged-in user to approve the device. Here it did not.

The misconfiguration

The key file is data/blueprints/device-code-flow.yaml.

First, the brand is configured to use the custom device flow:

Code:
flow_device_code: !KeyOf default-device-code-flow

That flow is marked as requiring authentication:

Code:
authentication: require_authenticated

At first glance, that sounds safe. The user must be logged in. But the admin bot already is logged in, so this becomes exactly the condition I want.

The more important line is the OAuth provider's authorization flow:

Code:
authorization_flow: !Find [ authentik_flows.flow, [slug, default-provider-authorization-implicit-consent] ]

That is the bad bit. The provider uses implicit consent. So after the device page sees a valid authenticated user, there is no real "yes, authorize this device" moment. There is also no separate OAuth consent screen later.

So the browser visit is the approval.

The actual exploit

The solve script in solve.py is tiny because the bug is configuration, not payload cleverness.

First, ask the flag dispenser to start a device flow:

Code:
POST /start

That gives back JSON containing:

Code:
device_code
verification_uri_complete

Then send the complete verification URL to the admin bot:

Code:
POST /visit
{"url":"http://server:9000/device?code=12345678"}

The bot's browser is already authenticated as akadmin. It loads the device page, Authentik accepts the prefilled code, and the implicit-consent provider finishes the authorization without asking the user anything.

Finally, redeem the device code through the dispenser:

Code:
POST /flag
{"device_code":"..."}

The dispenser exchanges it at the token endpoint, calls userinfo, sees akadmin and authentik Admins, then returns the flag.

Cool challenge showing the footgun of these authentik configurations x)
 
Back
Top