Exercise: Exploiting a Buffer Overflow on Ellingson
In the Hack The Box machine Ellingson, the website has left a debugging console enabled that can be used to execute remote commands. This allows us to write an SSH key for the user hal and SSH onto the box as that user. Once on the box, we discover a backup of the shadow file that hal has access to that contains passwords that can be cracked using John The Ripper. This gives us a password for the user margo. As margo, we find a program called garbage that is owned by root and has the setuid bit set. garbage is vulnerable to a buffer overflow and using the Pwntools framework, we can write an exploit to do a buffer overflow attack and get a shell as root.
An nmap scan shows that SSH (port 22) and HTTP (port 80) are running. The website is for a company Ellingson Mineral Corp and the team information reveals a list of possible users.

Clicking on one of the buttons to Details, we get a page that has the url http://ellingson.htb/articles/1 . Changing the number to a high value causes an error that causes a development page showing details of the error and a Traceback which is the list of functions called that lead to the error (Figure 3-10). If you remember the list of the OWASP top 10 vulnerabilities, one category was Security Misconfiguration and specifically: "Leaving debugging or development options switched on in the application". What makes this worse is that the Werkzeug traceback interpreter allows access to a python console

Debug information after exception is thrown by application
Before moving onto the console, we can see from the traceback that the application is a Flask application which is a framework for building web applications in Python. Going into the console, we can execute bash commands using the code:
Looking at the contents of the .ssh folder, the authorized_keys file contains public keys that do not match the public key in the file id_rsa.pub. In any event, the id_rsa key which contains the private key you would need to use to ssh onto the machine is encrypted. The easiest path is to produce a key yourself using ssh-keygen and add the public key in the authorized_keys file:
On the ParrotSec machine, you can then use the private key to ssh onto the box as hal.
Looking at files that the adm has access to, one stands out and that is a backup of the shadow file which is where Linux keeps the hashed password information for users.
Copy the shadow.bak to your local directory with scp:
In the shadow file there are entries for the users theplague, hal, margo and duke. The hashes can be copied into a separate file for cracking with John The Ripper. Before you do that however, you can create a custom wordlist. On the website, a note from ThePlague suggests that the most common passwords are Love, Secret, Sex and God. So you can try to crack the passwords just using words from rockyou.txt that have these four words as part of them:
This returns a password iamgod$08 which can be tested for the 4 users with SSH to find out that it works with the user margo.
As the user margo, we can start our enumeration on the box looking for paths to privilege escalation. We cover this process in Chapter 8, but for the time being, if we run the find command, we can look for binaries that have the SUID bit set:
This will highlight a number of files but one that is not normally found on Linux boxes is the program /usr/bin/garbage that has the suid bit set binary. When you examine the permissions of a file, the x which stands for execute permission is replaced with s. This means that when the binary is run, it will run as the owner of the file, which in this case is root.
Running the application prompts for a password:
Enter access password:
If we run the command strings on the file to list all of the hard coded strings, part of the output is interesting because one of the strings looks like a password:
It looks like the password might be N3veRF3@r1iSh3r3! And this is confirmed running the program again and putting in the password:
It is time to copy the file back to the local box to look at it more closely. We can use scp again to copy the file back to our box:
We are now going to do a bit of reverse engineering using a tool called Ghidra which can be downloaded and installed from https://ghidra-sre.org. Run Ghidra and select Import File… from the File menu and choose the binary garbage. You will be promoted to confirm this and the dialog will report that the garbage file is an ELF Linux binary.

Go ahead and confirm until the original file dialog is shown and then double click on the garbage file to open it for analysis. You will be asked if you want to analyze the file and so select all options and say yes.

The layout of Ghidra is a set of windows that display information about the program. Referring to Figure 3-12, the Symbol Tree lists the functions in the program and their locally declared variables. The assembly of the entire program is displayed in the Listing window . Source code that is reverse engineered by Ghidra is displayed in the Decompile window . The overall structure of the program with the different sections is shown in the Program Trees window . Finally, the Data Types are shown in the Data Type Manager window . To bring up the decompilation of a function, double click the function in the Symbol Tree. Note that the assembly listing will change to the selected function.
Let us run through the first part of the code:
To explore this code, you can double click on the function names to display the decompiled code of that function. The function check_user() does a getuid() to get the current user's uid. It checks to see if that is either 0 which is root, 1000 which is theplague or 1002 which is margo. If not, then it prints the message "User is not authorized to access this application. This attempt has been logged." and exits. Note that it does not log the attempt. Otherwise the function returns the user id and the function sets_username() sets the global variable username from the user id. The next function called is auth() .
In this function, the main things to note is the gets(input_buffer) which gets the password entered by the user and puts it in a buffer that is 100 bytes char long. This is a vulnerability because the buffer can be overflowed by inputting a longer than 100 character string for the password. You can confirm this yourself:
In order to get a better understanding of what is going on, we can run the program garbage in a debugger, gdb. GDB allows us to step through code in a program and set and inspect variables as it runs. There is a set of additional commands to GDB that can be installed called GEF.
GEF provides some commands that are specifically designed to help with exploit development.
We can run garbage in gdb using the command:
Once it starts, we can use the GEF command pattern create 200 to create a non-repeating pattern of characters that we can use as input for the password. If we run the program with the run (or just r) command in GDB, the program will prompt for a password. We can then paste the text pattern we got from the pattern create and you should get an error of the type segmentation fault (SIGSEGV). This fault occurs when the program can't continue to run because it is trying to access memory or an instruction that is invalid. The error occurs in the function auth():
The output is a little daunting but in the stack section, the first line is what has been put into the register rsp ($rsp in GDB). rsp is the register that will hold the address that a function will return to after it completes. This means that our input has overflowed the buffer allocated to it and this particular pattern of text starting "raaaaaa…" has been loaded into the rsp register and the program thinks that this is the location of a valid instruction address. When the program tries to execute this instruction, it will cause a segmentation fault error as mentioned before. However, since we control what value gets placed in the rsp register, we can put the address of code that we control. First, we need to know the exact offset of the pattern that gets placed in the rsp register.
To find the offset within the pattern we created, we can use another GEF command pattern offset, passing it the section of the pattern in rsp. This will return the offset and you will find out that it is at position 136
We now have the essentials for a buffer overflow and can control the execution, but the question is; what can we run? This is where Return Oriented Programming (ROP) comes in. In ROP, we are looking for a sequence of assembly operations that will perform some specific action ant return to the next instruction in our sequence using a ret (return) instruction. The sequence of useful instructions, ending with a ret, is called a gadget. What ideally we are trying to do is to call useful functions like system or execve which are C functions that allow other commands to be run. System and execve are located in the library libc that is almost always used by C programs. Because we are constructing a sequence, or chain (called a ROP chain), of gadgets to run a function in libc, the attack is also referred to as return-to-libc or ret2libc.
Before we start however, we need to check what mitigations the program is using against buffer overflows using the checksec function in GEF
NX is enabled which means that we can't put executable code on the stack. That is ok because we are avoiding doing that by using ROP chains instead. The Partial Relocation Read-Only is also not an issue since this mitigates a technique that are not going to use but I will come back to later.
The more important issue is the value for ASLR (Address Space Layout Randomization) on the system which is a technique that attempts to make it more difficult to carry out buffer overflows by randomizing the position in memory space of the base address of the program, its libraries, stack and heap.
As ASLR is enabled (2) and so this means that we can't simply find the address of assembly instructions as they are moved around in memory when the application is run. However, in a running program, the randomization is fixed and functions are addressed as an offset to a base address. If we are interested in functions from libc, we can find out the offset of a known function and then calculate the relative offsets for other functions.
On your local machine, make sure that ASLR is enabled by editing the /proc/sys/kernel/randomize_va_space as root and setting it to 2
To illustrate what this does, we will create a one line application that simply prints the address of the puts function:
Here the printf statement uses the %p format character to print an address and the the address is of the function puts (&puts).
Compile and run the program a few times:
We use the GNU C compiler, gcc to compile the source code poc.c and create a 64 bit executable object poc u. You can see that the address changes every time it runs. Let us change the program to show how we are going to get a base address for libc and use that to calculate the address of other functions like system.
The program does 3 printf statements to print the addresses of two libc functions, puts (4) (which writes a string to stdout) and system (5). The third printf takes the difference between the addresses and prints that out using the formatting character %x (6). When you run the program poc, you will get the output:
Remember that even though the addresses of puts and system will change every time the program is run, the difference in the addresses will always be the same because they are always loaded from libc at the same relative position. What we need now is the actual offset of puts and system within libc and we can get that by using the command line program readelf. Readelf provides information about Linux programs which are in the ELF format. We run it with the argument -s which will get readelf to print the symbols of the particular version of libc our program is using (2). The output from this is then passed to the grep command to filter just the strings that contain the words system@@ and puts@@.
The difference between the offsets 76550 and 48DB0 is 2D7A0, the same number we got as the difference between the addresses when the program ran. Using the offset of puts in libc allows us to calculate the base address of libc. Getting the program to print the actual address of a function is called "leaking the address". Using the leaked address of puts can then be used to find the base address by simply subtracting the offset from that leaked address.
To write the exploit code, we are going to use a Python framework called pwntools (which can be installed from https://github.com/Gallopsled/pwntools\). The first stage is to leak the address of puts. Unfortunately, we don't have a convenient printf statement that can do this for us so we need to construct ROP gadgets that will do two things; the first is to load the address of puts into the register RDI, the second is to then call the function puts. Since the application garbage is a 64 bit program, parameters are passed to a function using a calling convention that uses the registers RDI, RSI, RDX, RCX, R8 and R9. In this case, we are passing the value of the address of the function puts to the function puts itself and so we only need to use the register RDI to do that.
Another problem we have to solve is to get the address of the function puts to call. When code is relocatable, you can't just call a function directly because the real address of this function is changed each time the application is run. Instead, to call a function, the address of an entry in a table called the Procedure Linkage Table (PLT) is used. This then has a pointer to an entry in the Global Offset Table (GOT) that has the real address.
In our ROP chain, if we want the real address of puts, we need the address of the puts entry in the GOT. To actually call puts to print that leaked address, we use the address of puts in the PLT.
To summarize, stage 1 involves leaking the address of puts by calling puts and passing the address of puts itself in the register RDI. To load RDI with the address of puts, we can search for assembly instructions that will do that for us followed by a ret instruction. To load a register, we can use the assembly instruction pop rdi. A pop instruction takes the address at the top of the stack and puts it into the specified register.
We can use the tool ropper, a program that will disassemble a program and print out gadgets useful for buffer overflow exploits, and search for a gadget that will suit our purposes:
We can start putting together the exploit code.
The address for got_puts and plt_puts is obtained by using the program objdump to disassemble the program garbage (using the -D flag) and passing the output to grep to filter for the word puts.
Pwntools handles handling input and output to the application using a range of calls to send and receive text. The leaked address of puts is formatted by stripping the newline character at the end and padding the address obtained, if necessary, with zeros.
Once the leaked address is obtained, we need to stay in the program and do a second buffer overflow, but this time invoking the system function to run a shell. The next instruction needs to be the return address which should be that of main (the main entry point of the program) that we can again get from objdump (note this was included in the code above at ).
We can add the address of main to the payload. In stage 2 of the exploit, we are going to overflow the buffer and use the leaked address to calculate the full address of the system function and the location of the string "/bin/sh" which is in libc. We also need another function, setuid to make sure that when the shell is run by system, it is run as root's user id which is 0. Using readelf to get the offsets of the functions in libc:
We can now calculate the base address of libc
The offsets of the functions puts, system and setuid and the string "/bin/sh" are added to the program . The base address of libc is then calculated as the difference between the leaked address of puts and its offset in libc . The actual addresses of system, setuid and "/bin/sh" can then be calculated . Finally, the ROP chain is constructed and sent to the program as input for the prompted password.
The result is a shell as root
Note that to become root, the program needs to have the setuid bit set and the owner of the program needs to be root:
The final step is to run the exploit on the Ellingson box. To do this, you need to change the addresses from libc to the addresses of the version that is on the Ellingson machine. You can find out what version of libc is being used on Ellingson using the ldd command:
You can then simply change the way the program is run from
to:
Run the program to get access:
And that is it. Pwntools could have done a lot more for us, saving the need to find the addresses of the functions within garbage and libc. It also has high level functions to construct ROP chains. However, doing it manually helps understand what is going on.
Buffer overflows on Windows
Apart from changing platform and the tools we can use to explore buffer overflows for Windows applications, we can also look at 32 bit applications that differ in significant aspects to the 64 bit applications we have just covered. The registers were covered in Table x and there is a equivalence between the 32 bit registers and those in 64 bit architectures. The main difference with registers is that 64 bit processors have the extra numbered registers than 32 bit processors.
The other main difference is the function calling convention. If you look at most books and examples of buffer overflows, the claim will be made that 32 bit programs always pass parameters to a function on the stack. Sadly, that is not the case as there are a number of different calling conventions that different compilers use in an attempt to improve the performance of function calls. The conventions called "cdecl" (pronounced see-dec-el) and stdcall do put everything on the stack in reverse order to their declaration (right to left). If we take the following code as an example:
The call to printVars from main u will be preceded with two push instructions pushing 7 and then 3 onto the stack. On a Windows machine the 32 bit assembly of this code looks like:
The other thing that happens during the call instruction that follows the variables being pushed onto the stack is that return address is pushed as well (4). On leaving the function printVars, the return address will be popped from the stack and loaded into EIP and so the exploit of buffer overflows in 32 bit programs is similar to what we have already covered with 64 bit.
Microsoft Windows implements a protection mechanism called Data Execution Prevention (DEP) that marks the stack, heap and other memory regions as non-executable. There are a number of ways of getting around this by calling Windows functions that either create new memory locations that are executable or by changing the protection level of the DEP protected memory. Once DEP is bypassed, or if it is not enabled in the first place, we can take advantage of this and do a buffer overflow that includes shell code that can execute directly from the stack.
We will go through this using a case study from Hack The Box called Buff.
Last updated
Was this helpful?