Boston Key Party 2015 - pwn 275: Broadway

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 : 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:

  • nginx doesn’t use system(), so we have to use execve()
  • execve() needs more arguments
  • Also we need to redirect stdio (for instance, using dup2) to our socket, so we can actually use the shell
  • we need to guess the socket FD (technically, this may not have been necessary, but we were lazy) — it was 3…

So 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.