PHP 特性总结笔记
知不知道 PHP 语言的含金量啊?
不定期更新, 主要记录 CTF 刷题过程中的各种 PHP 特性
反序列化和命令执行的相关 tricks 会单独写在其它笔记中
类型转换
PHP 是一个弱类型语言, 变量之间进行比较时, 若两个变量类型不一致, 会进行强制类型转换
https://www.php.net/manual/zh/language.types.type-juggling.php
|
|
非数字开头的字符串转换成 int 类型时会变成 0, 数字开头的字符串转换 int 类型后会保留开头的数字
string 类型的转换会从最左边开始, 直到遇到非数字的字符时停止
含 e 的字符串转换成 int 类型时会被当做科学计数法处理, 123e456
表示 123 的 456 次方
0e123
表示 0 的 123 次方, 总是等于 0
0e456
同理
另外在数字开头加入 \f \t \s \n \r
等返回 true
|
|
进制转换
一个十进制数与十六进制/八进制数比较时, PHP 会将十六进制/八进制数转换为十进制数
十六进制数以 0x
开头, 八进制数以 0
开头
|
|
以上结果均返回 true
== 与 ===
https://www.php.net/manual/zh/language.operators.comparison.php
== 弱类型比较, 仅要求两边变量类型转换后的值相等
=== 强类型比较, 不仅要求两个变量的值相等, 还要求变量的类型相同
同理 != 是弱类型比较, 而 !== 是强类型比较
|
|
preg_match()
单行/多行匹配
几种模式修饰符
https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php
默认单行匹配匹配换行符
/m 多行匹配不匹配换行符
一个通过这种方式绕过的实例: Apache 换行解析漏洞 (CVE-2017-15715)
https://blog.csdn.net/qq_46091464/article/details/108278486
https://www.leavesongs.com/PENETRATION/apache-cve-2017-15715-vulnerability.html
传递数组
函数传入数组时会返回 false
正则回溯绕过
正则表达式溢出 回溯绕过
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
intval()
|
|
当 $base = 0
时, intval 会检测 value 的格式来决定使用的进制, 可以使用八进制或者十六进制绕过
另外 intval 可以取整 (去除小数点后的部分) 和截断 (去除数字后的字符串, 包括科学计数法)
在数字前加空格也能正常执行 intval
|
|
以上结果均返回 1146
strpos()
strpos('01234', 0)
返回的结果是 0 对应的索引 0, 也就是 false
如果是 !strpos()
这种则会返回 true
代码使用了 if(!strpos($str, 0))
对八进制进行过滤, 可以在字符串开头加空格绕过
strpos() 遇到数组返回 null
strrpos() stripos() strripos() 同理
is_numeric()
识别科学计数法
0123e4567
返回 true
包含非 e 字母返回 false
可以尝试利用 base64 + bin2hex 找到一些只含 e 和数字的 payload
在数字开头加入空格 换行符 tab 等特殊字符可以绕过检测
|
|
in_array()
将待搜索的值的类型自动转换为数组中的值的类型
|
|
ereg()
存在截断漏洞
%00
后面的字符串不解析
trim()
不过滤 \f 换页符, url 编码后是 %0c36
md5 sha 1 0e漏洞
本质上是科学计数法和类型转换的问题, 0 的任何次方都是 0
加密比较的时候用的必须得是 ==
, 而不是 ===
|
|
md5() sha1() 返回的都是字符串类型
md5 0e payload
|
|
sha1 0e payload
|
|
其它有时候可能用到的 payload
|
|
md5 sha1 数组绕过
md5 加密数组时返回 null
|
|
sha1 同理
路径穿越
通过绝对路径/相对路径绕过正则对文件名的检测, 例如 preg_match('/flag.php/', $str)
|
|
利用 Linux 下的软链接绕过
https://www.anquanke.com/post/id/213235
|
|
伪协议
https://www.php.net/manual/zh/wrappers.php
常见的 php://filter php://input data:// 都很熟悉了
下面是一些不是很常见的 payload
|
|
php://filter 遇到不存在的过滤器会直接跳过, 可以绕过一些对关键字的检测
运算符优先级
https://www.php.net/manual/zh/language.operators.precedence.php
例如
|
|
实际上是先赋值
|
|
然后是这种奇怪的形式
|
|
&& 和 || 的顺序也要注意
位运算
https://www.php.net/manual/zh/language.operators.php
通过 & | ^ ~
来构造无字母数字的 webshell
原理是 PHP 7 支持以 ($a)($b)
的形式调用函数并传参
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html
python 脚本
|
|
运算方式可以自己改
按位取反 ~
的话直接在 PHP 里面写就行了
三目运算符
有时候构造不带分号 payload 时需要用到三目运算符
|
|
1 永远为 true, 于是正常执行 phpinfo
函数与数字运算
https://www.php.net/manual/zh/language.operators.php
在 PHP 中, 函数与数字进行运算的时候, 函数能够被正常执行
|
|
+ - * / & |
都行, 另外还有 && ||
变量覆盖
几种形式
|
|
思路是 $_GET
$_POST
$_COOKIKE
相互转换
或者利用 $GLOBALS
输出所有的全局变量
另外 parse_str() 接受数组传参 支持数组内变量覆盖
|
|
非法变量名转换
变量名与 PHP 中其它的标签一样遵循相同的规则。一个有效的变量名由字母或者下划线开头,后面跟上任意数量的字母,数字,或者下划线。 按照正常的正则表达式,它将被表述为:’
^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$
’。
非法的字符 (点, 空格, 括号等) 会被转换成下划线
例如过滤了 _
如何构造 __CTFSHOW__
?
答案是传参的时候参数名写成 ..CTFSHOW..
另外有一个绕过转换的 trick, PHP < 8 有效
https://blog.csdn.net/mochu7777777/article/details/115050295
[
被转换为_
, 而后面的 .
不转换
例如, 构造 CTF_SHOW.COM
的变量名可以传参 CTF[SHOW.COM
在变量覆盖的时候会很有用
不加引号的字符串
PHP 会自动帮我们推断对应值的类型
例如下面的代码执行后会爆 Warning, 但能正常输出 flag_give_me
|
|
反引号内引用变量
|
|
反引号内能够引用变量, 与 "$var"
类似
https://blog.csdn.net/qq_46091464/article/details/109095382
$GLOBALS 和 get_defined_vars()
https://www.php.net/manual/zh/reserved.variables.globals
https://www.php.net/manual/zh/function.get-defined-vars
$GLOBALS
引用全局作用域中可用的全部变量
get_defined_vars() 返回由所有已定义变量所组成的数组
有时候可以从这里面查看 $flag
$_SERVER[‘argv’] 与 $_SERVER[‘QUERY_STRING’]
同样都是 GET 传参, 截取 ? 之后的部分
$_SERVER['argv']
是数组, $_SERVER['QUERY_STRING']
是字符串
$_SERVER['argv']
用空格分割数组内容
session.upload_progress
https://www.cnblogs.com/litlife/p/10748506.html
简单来说就是我们可以通过这个机制来上传任意文件到缓存目录 (默认是 /tmp), 并且在缓存目录下产生我们自定义的 session 文件
缓存文件的格式一般是 php[六位随机大小写字母]
, session 的格式是 sess_xxx
(xxx 是 Cookie 中 PHPSESSID 的值)
可以配合命令执行的通配符, 或者进行 session 文件包含来 getshell, 有时候需要条件竞争
上传 payload
|
|
不含字母数字的函数
_()
为 gettext() 别名, 类似于 echo 输出
https://www.php.net/manual/zh/function.gettext
https://www.cnblogs.com/lost-1987/articles/3309693.html
|
|
以上命令可以返回所有已定义变量
原生类列目录/RCE
一般都是 echo new $v1($v2('xxx'))
或者 eval($v('ctfshow'))
的形式, 有时候可以跳出来执行其它代码
ReflectionClass 和 Exception 里面可以执行其它函数
FilesystemIterator 列目录
https://www.php.net/manual/zh/filesystemiterator.construct.php
|
|
无参数函数读文件/RCE
无参数函数指形如 a(b(c()))
这种不需要参数或者只需要一个参数, 并且对应的参数可以通过另一个函数的返回值来获取的函数
例如在当前目录仅存在 index.php flag.php 的情况下, 无参数读取 flag.php 的内容
|
|
或者是配合 get 传参进行 rce
|
|
https://www.freebuf.com/articles/system/242482.html
https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE
静态调用方法
两种方式
通过 ::
访问
|
|
通过 call_user_func($_POST['ctfshow'])
以数组形式调用静态方法
|
|
https://www.php.net/manual/zh/language.oop5.static.php
call_user_func()
|
|
调用回调函数, 通常用来做免杀, 不过也可以调用类里面的方法
静态方法
|
|
传递数组 (动态/静态)
|
|
create_function()
|
|
创建匿名函数, 不过这个比较特别, 第一个位置传递的是匿名函数的参数 (有时候可以绕过过滤)
闭合括号执行任意代码
|
|
参考文章 https://paper.seebug.org/755/
根命名空间 \ 绕过过滤
上面的参考文章里也提到了
PHP 的命名空间默认为
\
, 所有的函数和类都在\
这个命名空间中, 如果直接写函数名 function_name() 调用, 调用的时候其实相当于写了一个相对路径; 而如果写 \function_name() 这样调用函数. 则其实是写了一个绝对路径. 如果你在其他 namespace 里调用系统类, 就必须写绝对路径这种写法.
有时候可以绕过一些正则, 比如执行的代码不允许以字母开头
|
|
__autoload 和 class_exists()
|
|
__autoload
是为了定义 PHP 加载未知类的时候进行的操作, 之前强网杯也出过 spl_autoload
相关的题 (spl_autoload
是 __autoload
的默认实现)
这里需要注意的是用 class_exist
检查一个类是否存在的时候, 默认会自动调用 __autoload
方法
属性类型不敏感
在 PHP 7.1 + 的版本中, 对属性类型 (public protected private) 不敏感
因为 protected 和 private 反序列化后的结果中含有 %00
, 部分题目会禁止这种字符, 可在构造 payload 时将属性全部改成 public 来绕过限制