
Exploit de Execução Remota de Comandos em PHP-FPM
Execução Remota de Código em PHP-FPM
Screencast: https://youtu.be/d6benC5FVZM
Este exploit de zero-day em configurações comuns de PHP-FPM foi descoberto durante a competição Realworld CTF em 2019. Uma expressão regular é usada para analisar a URI solicitada, mas caracteres de nova linha %0a não são correspondidos. Isso aciona um bug no FastCGI que calcula o comprimento da query string incorretamente e escreve um byte nulo em um local antes do início do buffer pretendido. Por meio da seleção cuidadosa do comprimento da query string, um atacante pode usar este bug para sobrescrever variáveis internas do PHP no servidor e executar código shell arbitrário.
A implementação original deste exploit em Go pode ser encontrada aqui. Eu usei esta, um write-up e o relatório de bug original como recursos de aprendizado para implementar o exploit em Python.
Docker no Linux Execute sudo docker run --rm -ti -p 8080:80 reproduce-cve-2019-11043 para instanciar um servidor NGINX/PHP-FPM mínimo com um script vazio em /script.php. O Dockerfile para esta imagem está disponível aqui, embora não seja necessário para executar o comando mencionado.
Docker no Mac Execute sudo docker-compuse up -d a partir do diretório /php/CVE-2019-11043 do repositório vulhub. (O Compose está incluído no Docker for Mac.)
Execute o script do exploit com o comando python3 exploit.py http://localhost:8080/script.php (ou /index.php se a segunda opção foi usada). Após a execução bem-sucedida, um Web shell ficará acessível anexando comandos à URL depois de ?a= (por exemplo, http://localhost:8080/script.php?a=uname -a).
N.B. Eu tentei criar um playbook Ansible para esta tarefa, mas me deparei com um bug crítico documentado aqui. Não é possível iniciar serviços systemd em kernels Linux recentes (por exemplo, qualquer versão LTS do Ubuntu) com um playbook Ansible.
Os arquivos de configuração do PHP-FPM contêm uma regra para corresponder solicitações de URI recebidas a scripts PHP que geralmente se parece com isto:
location ~ [^/]\.php(/|$) {
...
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass php:9000;
...
}
Isso deveria corresponder a qualquer URI no formato /script.php/pathinfo, mas . na verdade não corresponde ao caractere de nova linha %0a. Se a URI contiver uma nova linha, isso acionará o seguinte bug na implementação do PHP:
1141 int ptlen = strlen(pt);
1142 int slen = len - ptlen;
1143 int pilen = env_path_info ? strlen(env_path_info) : 0;
1144 int tflag = 0;
1145 char *path_info;
1146 if (apache_was_here) {
1147 /* recall that PATH_INFO won't exist */
1148 path_info = script_path_translated + ptlen;
1149 tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
1150 } else {
1151 path_info = env_path_info ? env_path_info + pilen - slen : NULL;
1152 tflag = (orig_path_info != path_info);
1153 }
O problema aqui é que slen é calculado corretamente como o comprimento da URI menos o comprimento do caminho do recurso, mas pilen é erroneamente definido como 0. Isso define path_info para um valor negativo na linha 1151, resultando em um underflow de buffer. Imediatamente após esse erro de cálculo no mesmo arquivo, temos:
1159 FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
1160 old = path_info[0];
1161 path_info[0] = 0;
1162 if (!orig_script_name ||
1163 strcmp(orig_script_name, env_path_info) != 0) {
1164 if (orig_script_name) {
1165 FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
1166 }
1167 SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
1168 } else {
1169 SG(request_info).request_uri = orig_script_name;
1170 }
1171 path_info[0] = old;
Na linha 1161, um byte nulo é escrito no local de memória calculado incorretamente na etapa anterior. Isso pode ser aproveitado para explorar uma vulnerabilidade na linha 1165, onde o FastCGI escreve uma variável de ambiente. Ao escrever o byte nulo no ponteiro que controla a operação de escrita da variável de ambiente, podemos inserir variáveis PHP arbitrárias no ambiente com nossas solicitações HTTP.
As variáveis de ambiente no FastCGI são armazenadas em uma sequência compacta de pares de strings chave-valor na memória. O início e o fim do buffer que contém essas strings é chamado de _fcgi_data_seg. O membro pos aponta para o próximo local disponível para escrita. Se o buffer encher (pos > end), um novo é alocado e o membro next aponta para o antigo.
118 typedef struct _fcgi_data_seg {
119 char *pos;
120 char *end;
121 struct _fcgi_data_seg *next;
122 char data[1];
123 } fcgi_data_seg;
O FastCGI acessa variáveis de ambiente individuais usando uma tabela hash chamada _fcgi_hash.
125 typedef struct _fcgi_hash {
126 fcgi_hash_bucket *hash_table[FCGI_HASH_TABLE_SIZE];
127 fcgi_hash_bucket *list;
128 fcgi_hash_buckets *buckets;
129 fcgi_data_seg *data;
130 } fcgi_hash;
A ideia aqui é sobrescrever o byte menos significativo de pos para enganar o FastCGI e fazê-lo sobrescrever uma variável existente. O código deveria pegar a string anexada ao nosso caminho de URI e colocá-la no local de PATH_INFO. No entanto, queremos sobrescrever PHP_VALUE, porque esse valor é imediatamente recuperado e carregado nas configurações do PHP após o segmento de código vulnerável.
Como você pode ver em exploit.py, a premissa geral deste exploit é encontrar uma query de URI muito longa que alinhe o buffer de memória interno do FastCGI de uma forma que possamos abusar. A ideia é encontrar o número exato de caracteres necessário para que o FastCGI aloque um novo buffer _fcgi_data_seg. Quando isso ocorre, o FastCGI previsivelmente escreverá nosso PATH_INFO no novo buffer, seguido imediatamente por cada um de nossos cabeçalhos HTTP como novos valores de ambiente. Então, o próximo passo é descobrir quantos caracteres precisamos para preencher um cabeçalho HTTP arbitrário, a fim de alinhar a memória para nossos propósitos. Como estamos limitados a escrever um byte nulo em um local arbitrário, precisamos fazer pos apontar para um deslocamento previsível de PHP_VALUE para que, ao editar o byte menos significativo, possamos movê-lo para lá.
O desafio é que queremos sobrescrever PHP_VALUE, mas não sabemos onde isso está localizado na memória. Quando o FastCGI carrega essa variável, ele aplica hash à string PHP_VALUE para obter o endereço de memória real de acordo com um algoritmo simples:
31 #define FCGI_HASH_FUNC(var, var_len) \
32 (UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \
33 (((unsigned int)var[3]) << 2) + \
34 (((unsigned int)var[var_len-2]) << 4) + \
35 (((unsigned int)var[var_len-1]) << 2) + \
36 var_len)
Em vez de realmente modificar a tabela hash de alguma forma, tudo o que precisamos fazer é criar outra variável de ambiente com o mesmo comprimento de string e o mesmo hash que PHP_VALUE de acordo com esta função. Isso enganará a consulta de hash, fazendo-a ler nosso cabeçalho HTTP em vez da variável pretendida. O autor deste exploit observou, de forma inteligente, que um cabeçalho chamado EBUT será salvo como HTTP_EBUT no ambiente FastCGI, o que atende a esse requisito.
Para o ataque em si, enviamos solicitações GET contendo nosso cabeçalho EBUT e usamos o bug de sobrescrita de byte nulo para sobrescrever seu valor. Tentamos definir as variáveis de ambiente do PHP uma por vez com solicitações repetidas:
short_open_tag=1
html_errors=0
include_path=/tmp
auto_prepend_file=a
log_errors=1
error_reporting=2
error_log=/tmp/a
extension_dir=\"<?=`\"
extension=\"$_GET[a]`?>\"
A modificação bem-sucedida de todas essas variáveis permite uma nova query ?a= no servidor para execução arbitrária de código shell. O loop do ataque verifica o sucesso a cada iteração tentando executar which which. O atacante pode facilmente detectar se isso foi bem-sucedido lendo o resultado da resposta HTTP (por exemplo, /bin/which).