DEV Community

Cover image for RIFFHACK: Black Market Break-In CTF Writeup
Advik Kant
Advik Kant

Posted on

RIFFHACK: Black Market Break-In CTF Writeup

RIFFHACK: Black Market Break-In ran from 19th to 22nd June 2026, four days of digging through challenges across categories. Went in without much of a plan, just picked off whatever looked interesting, and ended up clearing 4 of them by the end. Mixed bag of categories, mixed bag of pain. Writeups for each one below.

Moon Prism Packet Secrets

Description

A late-night idol rehearsal leak includes one selfie and a whisper of intercepted packets. The photo looks harmless, but the gradient refuses to keep the guardians’ secret quiet.

we are given two files- one pcap and one image file

captionless image

there is trailer data after ending so I did binwalk and got this, a diary_entry.txt file.

captionless image

after opening the diary_entry.txt file, I got the flag.

captionless image

Aperture Science Test Chambers

Description

GLaDOS has locked the flag behind twenty Aperture Science test chambers. Each chamber is an illuminated panel grid — pressing any panel toggles it and all its immediate neighbours. Find the sequence of presses that extinguishes every panel in each chamber, and she will release what you’ve earned.

After connecting via nc, I saw this

captionless image

basically we needed to solve 20 of these chambers according to the algorithm given in the description and it will get us the flag so I just vibe coded a python script to do all the stuff and we got the flag.

"""
Automated solver for the GLaDOS Aperture Lights Out CTF.
Connects via raw socket, solves each chamber, submits, loops through all 20.
"""
import socket
import re
import sys
import time
HOST = "IP_ADDRESS"
PORT = PORT_NUMBER
def solve_lights_out(grid, n):
    size = n * n
    A, b = [], []
    for r in range(n):
        for c in range(n):
            mask = 1 << (r * n + c)
            for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):
                nr, nc = r + dr, c + dc
                if 0 <= nr < n and 0 <= nc < n:
                    mask |= 1 << (nr * n + nc)
            A.append(mask)
            b.append(grid[r][c])
    pivot_row_for_col = {}
    cur = 0
    for col in range(size):
        pivot = next((r for r in range(cur, size) if (A[r] >> col) & 1), None)
        if pivot is None:
            continue
        A[cur], A[pivot] = A[pivot], A[cur]
        b[cur], b[pivot] = b[pivot], b[cur]
        for r in range(size):
            if r != cur and (A[r] >> col) & 1:
                A[r] ^= A[cur]
                b[r] ^= b[cur]
        pivot_row_for_col[col] = cur
        cur += 1
        if cur == size:
            break
    for r in range(cur, size):
        if A[r] == 0 and b[r] == 1:
            raise ValueError("Unsolvable grid, check parsing")
    x = [0] * size
    for col, row in pivot_row_for_col.items():
        x[col] = b[row]
    return [(i // n, i % n) for i in range(size) if x[i]]
def recv_until(sock, buf, markers, timeout=10):
    """Read until any marker string appears in the accumulated buffer, or timeout."""
    sock.settimeout(timeout)
    start = time.time()
    while True:
        if any(m in buf for m in markers):
            return buf
        try:
            chunk = sock.recv(65536)
            if not chunk:
                return buf
            buf += chunk.decode(errors="replace")
        except socket.timeout:
            return buf
        if time.time() - start > timeout:
            return buf
def parse_grid(text):
    m = re.search(r"GRID\s+(\d+)\s*\n((?:[01](?:\s+[01])*\s*\n)+)", text)
    if not m:
        return None, None
    n = int(m.group(1))
    rows_text = m.group(2).strip().splitlines()
    rows_text = rows_text[-n:]
    grid = [[int(x) for x in row.split()] for row in rows_text]
    return grid, n
def main():
    sock = socket.create_connection((HOST, PORT), timeout=15)
    full_buf = ""
    chamber = 0
    while chamber < 20:
        full_buf = recv_until(sock, full_buf, ["AWAITING SOLUTION", "FLAG", "flag{"], timeout=10)
        print(full_buf[len(full_buf) - full_buf[::-1].find("\n--\n"[::-1]) if False else 0:], end="")
        if "flag{" in full_buf.lower() or "FLAG" in full_buf:
            print("\n[+] FLAG FOUND")
            break
        grid, n = parse_grid(full_buf)
        if grid is None:
            print("[-] Could not parse grid, stopping")
            break
        chamber += 1
        presses = solve_lights_out(grid, n)
        print(f"\n[+] Solved chamber {chamber}: {n}x{n} grid, {len(presses)} presses")
        out_lines = [f"PRESSES {len(presses)}"]
        out_lines.extend(f"{r} {c}" for r, c in presses)
        payload = ("\n".join(out_lines) + "\n").encode()
        sock.sendall(payload)
        full_buf = ""  # reset buffer, next recv_until will catch the response to this submission
    # drain whatever's left (flag usually shows right after final OK)
    final = recv_until(sock, "", ["FLAG", "flag{"], timeout=5)
    if final.strip():
        print(final)
    sock.close()
if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode


captionless image

Vault-Tec Overseer Terminal

Description

A RobCo Termlink for Vault 101 lets residents personalize a terminal greeting. Enroll as an ordinary resident and recover the Overseer’s sealed directive.

As we can see if we inject basic {{ 7*7 }} SSTI payload, the output shows 49 meaning its vulnerable to SSTI.

captionless imagecaptionless image

Let’s test the below SSTI payload and see what output we get.

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('ls').read() }}
> app.py requirements.txt
Enter fullscreen mode Exit fullscreen mode

there are two files app.py and requirements.txt. The flag is most probably going to be in app.py so lets cat app.py and see if we can find the flag.

captionless imagecaptionless image

There we go, we have found the flag in app.py file.

Ghostbusters Template Possession

Description

Spengler’s Oscillation Translator is warping the containment HUD in impossible ways. The console still seems eager to reveal what the filters were meant to hide.

This challenge is also similar to the previous ones where we have to input something inside {{ }} and it will render and show the output.

So, ofcourse I tested again for SSTI and low and behold it was once again vulnerable to SSTI. The difference here was that in this some keywords like “self” and others were blocked. So in order to leak the files present in the directory via SSTI we will use another keyword that shouldn’t be blocked that is “cycler”.

Let’s see that by using the “id” command. As we can see, the “id” command worked and we are getting

uid=0(root) gid=0(root) groups=0(root)
Enter fullscreen mode Exit fullscreen mode


captionless image

Let’s test the below “ls” SSTI payload and see what output we get.

captionless image

Again there are two files app.py and requirements.txt. The flag is most probably going to be in app.py so lets cat app.py and see if we can find the flag.

captionless image

And there we go, we have found the flag.

Conclusion

That wraps up RIFFHACK: Black Market Break-In. 4 solves out of however many I attempted. For me CTFs are less about the flag and more about the detour, and this one had plenty. Thanks for reading;)

captionless image

Top comments (0)