
Exploit de prueba de concepto para CVE-2025-49132 que permite la ejecución remota de código sin autenticación en Pterodactyl Panel <= 1.11.10 mediante inyección del parámetro locale. Incluye modos de verificación y explotación.
Prueba de concepto para CVE-2025-49132 para Ejecución Remota de Código en Pterodactyl Panel <= 1.11.10
Este PoC toma referencias de
# Check if the web application is vulnerable
python3 poc.py -u <url> -m check
# Do something funny
python3 poc.py -u <url> -m exploit -p <path_to_pear_dir> -t <path_to_write_to>
Analizando el git diff de LocaleController.php entre v1.11.10 y v1.11.11 (link), podemos ver que el controlador acepta los parámetros locale y namespace sin ninguna validación ni sanitización.
Para entender por qué esto es peligroso, veremos el enrutamiento configurado en panel/routes/base.php. La ruta elimina explícitamente el middleware relacionado con la autenticación, permitiendo a usuarios no autenticados llamar a /locales/locale.json y proporcionar los parámetros de consulta requeridos como locale y namespace. Esto significa que estos parámetros ahora están controlados por el usuario.
Route::get('/locales/locale.json', Base\LocaleController::class)
// Remove authentication middleware for this endpoint
->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class]);
Volviendo a LocalController.php, la siguiente línea de código proporciona la magia de RCE.
$this->loader->load($locale, str_replace('.', '/', $namespace))
La función load proviene del framework de traducción de Laravel (aquí):
public function load($locale, $group, $namespace = null) # 1
{
if ($group === '*' && $namespace === '*') { # 2
return $this->loadJsonPaths($locale);
}
if (is_null($namespace) || $namespace === '*') { # 3
return $this->loadPaths($this->paths, $locale, $group); # 4
}
return $this->loadNamespaced($locale, $group, $namespace);
}
load toma 3 argumentos. Aunque confuso al principio, el valor str_replace('.', '/', $namespace) en realidad se pasa al segundo argumento $group.$namespace nunca será * ya que siempre será NULL. De manera similar, $group nunca será * ya que podemos controlar lo que se pasa aquí.$namespace es null, se tomará la rama resultando en la ejecución de loadPaths en #4.Entendiendo lo anterior, pasamos a analizar loadPaths donde ocurre la magia real
protected function loadPaths(array $paths, $locale, $group)
{
return (new Collection($paths))
->reduce(function ($output, $path) use ($locale, $group) {
if ($this->files->exists($full = "{$path}/{$locale}/{$group}.php")) { # 1
$output = array_replace_recursive($output, $this->files->getRequire($full)); # 2
}
return $output;
}, []);
}
.php.getRequire(), que ejecuta el archivo PHP y devuelve su array.Dado que controlamos el parámetro de URL namespace, podemos influir en qué archivo intenta cargar la aplicación. Al proporcionar un valor que corresponde a un script .php, podemos lograr que el servidor ejecute ese script. Por ejemplo, para cargar phpinfo.php, podemos establecer el parámetro en: namespace=phpinfo
El PoC intenta realizar escritura arbitraria de archivos a través de pearcmd.php. Este método ha sido utilizado en muchos PoCs similares y en investigaciones de seguridad
Tenga en cuenta que pearcmd.php puede estar ubicado en diferentes directorios como
/usr/share/php/pearcmd.php/usr/share/php/PEAR/pearcmd.php/usr/lib/php/pearcmd.phpy muchos más.
El archivo que se escribirá contendrá el típico código PHP malicioso.
Solo para pruebas autorizadas. No lo use en sistemas que no posea o para los que no tenga permiso explícito para probar.
Usted es responsable de cumplir con todas las leyes aplicables; los autores no asumen ninguna responsabilidad por el mal uso.
La biblioteca requests de Python es demasiado útil, ya que codifica caracteres en la URL proporcionada. Pasé mucho tiempo tratando de resolver este problema y terminé usando curl directamente. Agradecería si alguien puede brindar ayuda para solucionar este problema.