Challenge: Volatile Component · CTF: GPN CTF 2026 · Category: Miscellaneous · Difficulty: Medium

Summary

A GitHub Actions workflow leaks a FLAG secret but (1) masks it in logs, (2) redacts every GPNCTF{...} it finds on the filesystem, and (3) interpolates an untrusted issue/comment body straight into a run: shell block. The injection gives RCE; the flag survives only in the long-lived Runner.Worker process memory (the “volatile component”), which is read via sudo. Base64 of a secret is auto-masked by GitHub, so the flag is exfiltrated through the run logs as hex.

Solution

Step 1: Find the bug in the workflow

The instancer hands you a private repo containing a single workflow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
on:
  issue_comment: { types: [created] }
  issues:        { types: [opened] }
jobs:
  process-comment:
    steps:
      - run: echo "Flag: ${{ secrets.FLAG }}"                 # masked as *** in logs
      - run: |                                                # redacts GPNCTF{...} on disk
          sudo grep -rlP 'GPNCTF\{(?!\.\*\})[^}]+\}' /home/runner/ | while read -r f; do
            sudo sed -i -E 's/GPNCTF\{[^}]+\}/GPNCTF{REDACTED}/g' "$f"; done || true
      - run: echo "Processing comment: ${{ github.event.comment.body || github.event.issue.body }}"

Three observations:

  • Step 3 drops github.event.issue.body raw into a shell command → classic script injection / RCE.
  • Step 2 proves sudo is passwordless, but it only rewrites files.
  • The Runner.Worker process must keep the secret value resident in memory to keep masking output — so the unredacted flag lives in /proc/<pid>/mem even after disk redaction.

Step 2: Bypass log masking with hex, not base64

The flag is masked (***) in logs. GitHub also auto-registers the base64 form of every secret for masking, so base64(flag) prints as *** too. Hex encoding is not auto-registered and passes straight through.

Step 3: Inject, dump memory, exfiltrate as hex

Open one issue whose body breaks out of the echo and runs a sudo memory scanner. The clean flag is recovered from process memory and printed as hex, then read back from the run logs (pull/read access is granted to the player). See solve.py.

Test the payload on a personal fork (with a dummy FLAG secret) first — the live instance burns limited GitHub Action minutes and the organizers warn against testing on it.

Flag

1
GPNCTF{dID_yoU_KnOw_7hat_d14L1y1_dISulFIDE_peN3tRAteS_ThROugh_mOsT_cOMmERCI4l_GL0ve_TYPeS_CAUSinG_64RliC_41lergY_wHICh_M0s7_Of73n_affec75_ch3F5_4nd_0ther_P30PLe_Th4t_HAnD1e_g4RliC_OuU3zsjB}