🐀Pyrat
License
The following post by anthonyjsaab is licensed under CC BY 4.0
0 - Introduction
Created by tryhackme, and josemlwdf
1 - Port Scans
1a - Discovery
1b - Versions and OS
2 - Port 8000
2a - Inspecting
When first trying to access port 8000 with an up-to-date Firefox browser, we get this:


2b - Crafting a "basic" request
It seems the server needs a more carefully crafted HTTP request. Copying the request as a cURL command from the Network Tab in Firefox's Developer Tools, we get this:
After gradually deleting all the headers, we get this:
It seems the server is trying to communicate with us using HTTP/0.9.
2c - HTTP0.9?
From https://http.dev/0.9, we can learn that:
HTTP/0.9 was the original release and it was extremely simple, supporting only the HTTP GET method. Only HTML files were included in HTTP responses, there were no HTTP headers, and there were no HTTP status codes.
An example provided on that same site:
Request:
Response:
After reading curl's manual pages, we notice that we can allow HTTP/0.9 response with a flag. Trying again:
This is strange. Why wouldn't this work? And why isn't the server responding with a proper HTML file? Even if my request is wrong, according to https://http.dev/0.9:
If an error occurred then the server will generate a situation-specific HTML file that the client will present to the user to describe the issue.
Whoever, this is not what we see. The server might not be using HTTP/0.9 after all. cURL might have misidentified it because no headers where being sent, which is only typical of HTTP/0.9.
2d - Python!
"name 'GET' is not defined" is a very familiar error. I had to be sure:
I am now almost certain that the server is executing my request in a Python console and returning the console outputs! Testing the theory:
2e - Foothold
Bingo! Knowing this, I went to revshells.com and got the simplest Python reverse shell command:
The command hanged!
3 - PrivEsc
3a - LinPEAS
To be able to read the outputs correctly, I will have to exfiltrate the www-enum.txt file to my Kali machine:
3b - SMTP?
From LinPEAS' output, we can see that an internal SMTP server and an internal DNS server are hosted:
Reading the emails:
Quickly looking at the enum output reveals that the 8000 server is the RAT mentioned above:

3c - Exfiltrating dev
Trying to read /root/pyrat.py or anything related to it in /proc have proved unsuccessful. However, we know that it is think@pyrat who wrote the code. Additionally, we have found an unusual folder in /opt belonging to think@pyrat (using LinPEAS). It might contain the source code!
Sadly, the folder is empty. Or is it? The dev folder is a local git repo since the .git folder is present. Let's check its commits:
Unfortunately, the protection mechanisms of Git aren't allowing us to interact with the .git folder through the git program. However, we still have read permissions inside!
This means we can compress the dev folder and exfiltrate it to kali where we can easily workaround the protection mechanism and see the commits!
We will exfiltrate the tar file in the same way we did with the www-enum.txt file:
3d - Will Git Get the Rat?
Reading pyrat.py.old:
It appears that the complete code is not saved in this repository; only a few lines are available. However, these lines provide a good understanding of what's happening, particularly why root is executing /root/pyrat.py even though we don’t have root privileges ourselves (because of the change_uid() function).
3e - Mysterious Endpoint
Trying out some inputs based on the observations from the source code snippet, we get this:
It seems the recovered part of the source code is mostly still valid. When we type 'shell', we actually get a shell like the code says!
However, the placeholder 'some_endpoint' seems to have been changed. After reading the comments in the code carefully, and with some trial and error, I discover that the 'some_endpoint' placeholder was replaced with 'admin'. However, we do not really know what this will do. We only know that it might give us something better. If we type anything else, we are sure to get a low privileged shell. Also, it turns out that there is still an obstacle left:
3f - Are we there yet?
Since the script is proving to be quite challenging, I thought it would be much easier if I had access to the entire code. That’s when I remembered the email we saw earlier:
Also, the git log command gave us great info about the author:
With this username in mind, I headed to the profile on GitHub:

The entire code can be seen here:
There was a hardcoded password, but it was a placeholder and was not the one actually used on our target. The most important part is here:
3g - Finally! ROOT
It seems the pyrat.py.old was outdated. The if statement that would trigger change_uid() has changed. Now, if str(client_socket) is in admins, root access won't be taken from me!
How can I do that? Well:
Since admins is a global variable, I will be able to change it because I will not be executing Python commands in a subshell, but rather in the actual original process (exec_python case)
I got root!
Note how if I sent 'shell' as the first command, it would be game over—I would end up in a subprocess with root access taken away for good. Instead, I allowed the first switch_case() call to reach exec_python. The second call then goes to shell() without stepping into change_uid() during the entire nc session.
Also, notice how I used the -p parameter in nc to ensure that I was using the same source port that had been previously added to the admin's list.
That's all! 🎉
Last updated
