Em resumo: O utilizador do servidor web com que o PHP corre deve, segundo o princípio das permissões mínimas (Least Privilege), ler apenas o que o site precisa e escrever apenas onde o site guarda dados. Não tem shell de início de sessão, direitos sudo, cronjobs próprios nem chaves SSH, e a sua conta de base de dados só pode ler e alterar dados. Opções do systemd como ProtectSystem e PrivateTmp limitam adicionalmente o PHP-FPM.
A pergunta após a intrusão: o que pode este processo fazer?
O PHP2Shell trata da execução remota de código malicioso sem autenticação. Se for bem-sucedida, o código não corre no vazio, mas como um utilizador bem determinado: o utilizador do processo PHP. Tudo o que este utilizador pode fazer, o atacante passa também a poder fazer a partir desse momento.
Em muitos servidores isso é surpreendentemente muito. O utilizador é proprietário de todo o código do programa, tem uma shell, pode criar cronjobs próprios, e a conta de base de dados da aplicação tem todos os direitos sobre todas as bases de dados. Para um atacante, é o ponto de partida ideal para se instalar de forma duradoura.
Como atribuir corretamente ficheiros e pastas é descrito no guia Permissões de ficheiros no servidor web. Este artigo olha para a própria conta: início de sessão, direitos especiais, tarefas agendadas, base de dados e os limites que o systemd traça.
Inventário: quem é o utilizador do servidor web?
Primeiro, esclareça que utilizadores estão envolvidos. No PHP-FPM, cada pool define com user e group sob que utilizador correm os seus workers. No mod_php, o PHP corre como utilizador do Apache. Como o mestre e os workers do PHP-FPM interagem é explicado em PHP-FPM explicado: gestor de processos, pools e workers. Os comandos seguintes apenas leem:
# Com que utilizadores correm o servidor web e o PHP?
ps -eo user,group,cmd | grep -E "[p]hp-fpm|[n]ginx|[a]pache2|[h]ttpd" | sort | uniq -c
# Dados da conta: diretório home e shell (adaptar o utilizador)
getent passwd www-data
# O utilizador tem direitos sudo?
sudo -l -U www-data
# Cronjobs próprios?
sudo crontab -l -u www-data
# Chaves SSH no diretório home (no Debian/Ubuntu é /var/www)
sudo ls -la /var/www/.ssh 2>/dev/null
Anote os resultados de cada utilizador de pool. Se todos os sites correrem sob o mesmo utilizador, esse é o primeiro ponto da lista; como separá-los é descrito em Isolar pools PHP-FPM por site.
Permissões do utilizador do servidor web: o que a conta não precisa
O quadro seguinte mostra o que um utilizador do servidor web normalmente não precisa e como o retirar.
| Permissão | Porque é perigosa | Recomendação |
|---|---|---|
| Shell de início de sessão | possível início de sessão e uso interativo | shell /usr/sbin/nologin |
| entradas sudo | caminho para direitos de root | nenhuma entrada, nem para comandos isolados |
| crontab própria | porta dos fundos duradoura que sobrevive a qualquer reinício | nenhuma, limitar com /etc/cron.allow |
| chaves SSH | início de sessão a partir do exterior | sem authorized_keys |
| propriedade do código do programa | o código pode ser alterado | o proprietário é um utilizador de deploy |
| escrita fora das pastas de dados | colocação de código malicioso | apenas uploads, cache, temp |
| direitos totais na base de dados | bases de dados de outras aplicações, acesso a ficheiros através da base de dados | apenas a própria base de dados, apenas direitos de dados |
A shell define-se com usermod -s /usr/sbin/nologin www-data. Para o cron aplica-se o seguinte: se existir /etc/cron.allow, só os utilizadores aí indicados podem criar cronjobs próprios. Cronjobs recém-criados ou chaves SSH desconhecidas são um sinal típico de que alguém se fixou no sistema. Um sinal de alerta particularmente claro é descrito no artigo Sinal de alerta: o servidor web inicia uma shell.
A base de dados: limitar os direitos ao necessário
A conta de base de dados de um site é, na prática, um segundo utilizador do servidor web. As suas credenciais estão no ficheiro de configuração da aplicação e são legíveis por código introduzido. Por isso, só deve poder fazer o que a aplicação precisa em funcionamento.
-- Exemplo MySQL/MariaDB: conta própria por site, apenas direitos de dados
CREATE USER 'seite1'@'localhost' IDENTIFIED BY 'hier-ein-langes-Zufallspasswort';
GRANT SELECT, INSERT, UPDATE, DELETE ON seite1.* TO 'seite1'@'localhost';
-- Verificar direitos
SHOW GRANTS FOR 'seite1'@'localhost';
Direitos como FILE, SUPER ou GRANT OPTION e o acesso a *.* não são necessários a uma conta de aplicação. Muitos CMS precisam, para as atualizações, também de CREATE, ALTER, INDEX e DROP. É mais limpo ter uma conta separada para atualizações e migrações, usada apenas durante a manutenção. Verifique os requisitos na documentação do fabricante.
Limitar adicionalmente o PHP-FPM com o systemd
O systemd pode restringir a visão que um serviço tem do sistema. Para o PHP-FPM, algumas opções da documentação do systemd são particularmente adequadas. Também atuam sobre o processo mestre, que arranca como root.
# sudo systemctl edit php8.3-fpm
[Service]
# /usr, /boot e /etc só de leitura
ProtectSystem=full
# /home, /root e /run/user invisíveis
ProtectHome=true
# /tmp e /var/tmp próprios, só para este serviço
PrivateTmp=true
# sem elevação de privilégios através de setuid ou capabilities de ficheiros
NoNewPrivileges=true
Teste estas opções antes da utilização em produção. Se os sites estiverem em /home, o ProtectHome=true impede o acesso. O NoNewPrivileges=true pode perturbar programas chamados pelo PHP que mudam os seus direitos de grupo ao arrancar, por exemplo alguns programas de correio locais para a função mail() do PHP. Por isso, avance por etapas:
- Acrescentar uma opção, reiniciar o serviço e verificar com
systemctl status php8.3-fpm. - Testar as funções essenciais do site: início de sessão, formulários, envio de correio, uploads, cronjobs da aplicação.
- Analisar o registo de erros do PHP-FPM e da aplicação.
- Só então acrescentar a opção seguinte.
Por fim, o socket: segundo o manual do PHP, recebe por defeito as permissões 0660 para o utilizador e o grupo do pool. Defina listen.owner e listen.group de modo que apenas o servidor web se possa ligar, e não qualquer utilizador local.
As permissões mínimas são uma atitude de base, não um projeto pontual. Repita o inventário deste artigo após cada reestruturação e após cada novo site, pois os novos serviços trazem muitas vezes permissões por defeito generosas.
Conclusão: o atacante herda o que você permite
O Least Privilege parece teoria, mas é uma questão muito prática: o que pode fazer alguém que executa código como utilizador do servidor web? Num servidor com shell, entrada sudo, crontab própria e uma conta de base de dados com todos os direitos, a resposta é: quase tudo. Num servidor delimitado, o mais provável é um incidente num único site, que pode ser identificado e limpo.
A mudança não exige software novo, apenas um inventário cuidadoso e testes por etapas. Comece pelos pontos que dificilmente quebram alguma coisa, ou seja, shell, sudo e chaves SSH, e avance depois para os direitos da base de dados e as opções do systemd.
Como ajuda concretamente a PHP SOS?
- Verificar contas: Registamos, para cada pool e servidor web, o utilizador, a shell, as entradas sudo, os cronjobs, as chaves SSH e os direitos da base de dados.
- Reduzir permissões: Retiramos o que não é necessário e separamos as contas de deploy, de execução e de manutenção.
- Limitar serviços: Definimos as opções de proteção do systemd por etapas e testamos o site após cada passo.
- Manter de forma duradoura: No âmbito do reforço de segurança de servidores, documentamos o conceito de permissões para alterações futuras.
Perguntas frequentes
Com que utilizador corre o PHP no meu servidor?
Depende do modo de funcionamento. Com o mod_php, o PHP corre como utilizador do Apache, por exemplo www-data ou apache. Com o PHP-FPM, cada pool define o seu utilizador com user e group; o processo mestre corre normalmente como root, os workers como utilizador do pool. O comando ps mostra-o rapidamente.
O utilizador do servidor web precisa de uma shell de início de sessão?
Não. Nem o Nginx, nem o Apache, nem o PHP-FPM precisam de um início de sessão interativo para este utilizador. Uma shell como /usr/sbin/nologin impede que alguém inicie sessão com esta conta. Os deployments correm através de um utilizador próprio.
Porque é que o www-data não deve ter direitos sudo, nem sequer para um único comando?
Porque o código introduzido herda exatamente estes direitos. Mesmo um comando aparentemente inofensivo pode, se for mal autorizado, levar a direitos de root. As tarefas que exigem permissões elevadas devem ficar em serviços ou processos separados, fora do servidor web.
Isto ajuda contra o PHP2Shell?
Não fecha a vulnerabilidade; para isso são necessárias a atualização do PHP e o reinício de todos os workers. Permissões mínimas limitam, no entanto, o que um atacante pode fazer após uma exploração bem-sucedida: alterar ficheiros, fixar-se no sistema, chegar a outros sites.