lets pretend that first CDN didnt happened! can you get a shell, there is a second flag in the root of the fs! It is up at http://54.88.83.98/ : 275
Sidenote: I highly recommend reading my other writeup to Andrew first, as they cover the same binary. I’ll assume that you’ve read it.
After getting the first flag, there wasn’t much left to analyze in the binary, besides the standard nginx functions. I’ve spent some time looking at minjs(), but did not come up with anything. But a quick look at minhtml() made it pretty obvious. Looking at the stack layout, we see a bunch of uninteresting stuff happen. But at the very top of the stack, there is a u_char ret[1024] allocated. This looks promising!
Just like leetify, minhtml(u_char *str, int *data_size)
gets called with the output of the CURL functions if the url ends with “.html”. This gets important now. minhtml first does a strlen() on the char pointer it gets, and only continues if it is less than 1023 characters long. However, the copy-and-minify loop isn’t bound to any limit. It is, essentially, a while(TRUE) which breaks once it iterated data_size times.
And this is where things go wrong, horribly. CURL does not care for null bytes in the recieved content. Sending a crafted message which contains a null byte in the first 1023 bytes, while itself being larger than 1024 byte, will overflow the buffer. Nice! This way, we can easily override the return address and jump wherever we want.
Or, can we? Actually, sadly not. A quick readelf reveals why:
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
So the stack isn’t executable - time for rop’n’roll!
nginx provides us with more than enough ROP gadgets, but there are still a few caveats:
dup2
) to our socket, so we can actually use the shellSo basically, we want to call the following in order:
dup2(3, 0);
dup2(3, 1);
dup2(3, 2);
execve("/bin/sh", {"/bin/sh", NULL}, {NULL});
Using the countless useful gadgets from the nginx binary, we built a ROP chain
performing these calls. As an interesting side note: we found it convenient to
write the data structures for execve
somewhere into writable memory. We
settled for the 0x6810xx address range. Testing the shellcode, we found that
the last return, to execve
, somehow set the %rip
to NULL
after all the
rest of our ROP chain ran without problems. Imagine the odds: We choose the
exact address of execve
’s .got.plt
entry — the only location in the
entire program’s address space whose value we would ever need again.
After a reasonable laugh and adapting the chain to use different writable
memory addresses as scratch space, we obtained a shell.
$> cat /flag.
Pwniepie!