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
there is trailer data after ending so I did binwalk and got this, a diary_entry.txt file.
after opening the diary_entry.txt file, I got the flag.
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
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()
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.
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
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.
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)
Let’s test the below “ls” SSTI payload and see what output we get.
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.
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;)












Top comments (0)