
CTF pwn challenge writeup exploiting CVE-2021-4034 (pkexec) via heap manipulation, with Ghidra-based reverse engineering and a custom shelly.so helper binary.
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 directory in this repo. The chal binary implements the CTF challenge and the shelly.so is a helper binary.
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:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1.flag.txt file owned by root:root in the current directory.challenge folder to the current directory.chal as the unprivileged user.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:
Order number: 0x7ffde93681f0 which seems to print some location on the stack?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.
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:
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:
...
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:
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.