Wiz CTF #8: Confession Booth

Wiz CTF stamp 8

No flags are included in this writeup.

Overview

Confession Booth looked like a web app challenge, but the actual bug was timing. The application promised a safe place for confessions, with admin moderation in the middle. The hint made the direction clear: if admin access had not happened yet, I had not tried enough times.

Field Value
Month January 2026
Points 30
Main area Go web app, PostgreSQL, race condition, privilege escalation

Source Review

The source code showed a Go application with JWT authentication, PostgreSQL, user registration, and admin-only confession approval routes.

The interesting registration flow had two separate operations:

  1. create the user row
  2. update the user permission level

Between those operations, the user existed with a NULL permission value. That tiny gap was the whole challenge.

Why NULL Became Admin

The app scanned the permission value into a normal Go integer. In Go, an uninitialized integer defaults to 0. The app’s permission model also used 0 as the admin value.

That created a dangerous chain:

  • user row is inserted
  • permission is briefly NULL
  • login during that window reads the user
  • NULL becomes 0
  • 0 means admin
  • JWT is issued with admin privileges

This is a beautiful and painful bug because every piece looks small. Together, they create privilege escalation.

Exploiting The Race

A single request is unlikely to hit the window. The exploit needed concurrency:

  • register a new random user
  • send many login attempts at the same time
  • inspect returned tokens
  • test whether any token had admin access
  • use admin access on the approval endpoint

Threading may not be enough if the timing is tight. Async HTTP is a better fit because it can fire many requests with less overhead and better coordination.

Root Cause

The root cause was unsafe state transition design:

  • user creation and permission assignment were not atomic
  • the database schema allowed NULL in a security-critical column
  • application code treated NULL like a normal integer
  • the most privileged role used the zero value

The fix is not just “make the race harder.” The fix is to remove the unsafe state entirely.

Takeaways

This challenge is one of my favorite web lessons from the series:

  • security-sensitive multi-step writes need transactions
  • permission fields should have safe defaults
  • admin should not be the zero value
  • use nullable database types deliberately, not accidentally
  • race conditions become practical when attackers can repeat attempts cheaply

Confession Booth was low-level in the best way. The exploit was not flashy; the bug was in a tiny mismatch between database state and language defaults.