$ cat writeup.md…
$ cat writeup.md…
avitoctf
Task: A non-stripped ELF runs uploaded jobs in a persistent worker sandbox whose timeout path mishandles privileged resources. Solution: Leak an inherited shadow descriptor, crack the root DES hash, and run a flag reader as root.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Улей запускает «СотоLambda» — облачную систему исполнения небольших задач. Загрузите программу, выберите ресурсы и дедлайн, и рой воркеров исполнит её. Даны тестовые учётные данные и демо-бинарник. Есть подозрение, что воркер не всегда очищает служебные следы. Доберитесь до
/root/flag.txt.
The challenge supplied a non-stripped x86-64 worker binary, sandbox_worker.elf, plus a web interface for uploading ELF programs and invoking them with credentials and sandbox limits. The wording about uncleared service “traces” was literal: a timed-out request left a privileged file descriptor in a persistent worker, and a later low-privilege program inherited it.
No memory corruption, namespace escape, or direct authentication bypass was required.
The worker was a dynamically linked, PIE x86-64 ELF with NX, partial RELRO, no stack canary, and symbols intact. Its imports immediately exposed the important subsystems:
getpwnam, fgetspent, and crypt;setitimer, sigsetjmp, and siglongjmp;fork, setresgid, setresuid, and execv;pipe2, poll, and waitpid.The named handle_request function and global child/pipe variables also suggested that one privileged process served multiple requests. That made cross-request resource lifetime more important than conventional memory-corruption checks.
The supplied binary can be checked with:
sha256sum sandbox_worker.elf # f33ea64f449a32aed3a2aaf27dbeda24d7da31b1abe5809363ab4e0718df8a28 file sandbox_worker.elf readelf -hW sandbox_worker.elf readelf -sW sandbox_worker.elf | less objdump -d -M intel sandbox_worker.elf | less
handle_request installed the SIGALRM handler and saved a recovery point with sigsetjmp around 0x2492..0x24d0. For each request, it then opened /etc/shadow around 0x24f6..0x2506 as follows:
shadow_fd = open("/etc/shadow", O_RDONLY);
There are two crucial properties here:
O_CLOEXEC is absent, so the resulting descriptor survives execv unless explicitly closed....
$ grep --similar