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
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:
That told me the intended object was not a normal authorization-code redirect. It was the device endpoint. In flag-dispenser/app.py,
The response includes
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:
That flow is marked as requiring authentication:
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:
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:
That gives back JSON containing:
Then send the complete verification URL to the admin bot:
The bot's browser is already authenticated as
Finally, redeem the device code through the dispenser:
The dispenser exchanges it at the token endpoint, calls userinfo, sees
Cool challenge showing the footgun of these authentik configurations x)
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)