web安全-PHP特性
md5 & sha1
弱相等(==)绕过
加密后形成0e开头的科学记数法,则值相等(都为0)
md5 payload:
1 | QNKCDZO |
sha1 payload:
1 | aaroZmOk |
强相等(===)绕过
md5和sha1都无法处理数组,所以可以通过传入数组绕过
强类型绕过
1 | (string)$_POST['a1']!==(string)$_POST['a2'] |
可以构造两个MD5值相同的不同字符串
1 | #1 |
sha1同理
intval()
intval(mixed $value, int $base = 10): int
当 $base = 0 时, intval 会检测 value 的格式来决定使用的进制, 可以使用八进制或者十六进制绕
intval 可以取整 (去除小数点后的部分) 和截断 (去除数字后的字符串, 包括科学计数法)
在数字前加空格也能正常执行 intval
以下结果都为1146
1 | intval('1146.0'); |
is_number()
判断是否为数字(识别科学计数法)
在数字开头加入空格 换行符 tab 等特殊字符可以绕过检测
1 | is_numeric(' 36'); // true |
in_array()
判断元素是否在数组中
1 | #以下,'7eee'被强制转换成整型 7 |
同理:
1 | $v = in_array(0, array('s')); |
ereg()
一种正则函数
存在截断漏洞
%00 后面的字符串不解析
strpos()
strpos()函数查找字符串在另一字符串中第一次出现的位置。
strpos(‘01234’, 0) 返回的结果是 0 对应的索引 0, 也就是 false
代码使用了 if(!strpos($str, 0)) 对八进制(0x)进行过滤, 可以在字符串开头加空格绕过
strpos() 遇到数组返回 null
strrpos() stripos() strripos() 同理
trim()
trim() 函数移除字符串两侧的空白字符或其他预定义字符。
1 | 规定从字符串中删除哪些字符。如果被省略,则移除以下所有字符: |
1 | 移除字符串两侧的字符("Hello" 中的 "He" 以及 "World" 中的 "d!"): |
不过滤 \f 换页符, url 编码后是 %0c36
$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
1 | <form action="http://xxx/" method="POST" enctype="multipart/form-data"> |
静态调用方法
https://www.php.net/manual/zh/language.oop5.static.php
两种方式
通过 :: 访问
1 | $ctfshow::getFlag(); |
通过call_user_func($_POST['ctfshow'])
以数组形式调用静态方法
1 | ctfshow[]=ctfshow&ctfshow[]=getFlag |
call_user_func()
call_user_func(callable $callback, mixed ...$args): mixed
调用回调函数, 通常用来做免杀, 不过也可以调用类里面的方法
静态方法call_user_func('myclass::static_method')
传递数组 (动态/静态)
1 | call_user_func(array('myclass', 'static_method')); |
三目运算符
有时候构造不带分号 payload 时需要用到三目运算符
return 1?phpinfo():1;
1 永远为 true, 于是正常执行 phpinfo
escapeshellarg() & escapeshellcmd()
escapeshellarg()
将给字符串增加一个单引号并且能引用或者转义任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含exec()、system()和执行运算符。
变量覆盖
extract() parse_str()
extract() 函数使用数组键名作为变量名,使用数组键值作为变量值,当变量中有同名的元素时,该函数默认将原有的值给覆盖掉。这就造成了变量覆盖
POST方法传输进来的值通过extrace()
函数处理,直接传入以POST的方式传入pass=1&thepassword_123=1
就可以进行将原本的变量覆盖,并且使两个变量相等即可。
还有就是这两个函数如果结合起来使用,也会造成变量覆盖
代码中同时含有parse_str和extract($_POST)
可以先将GET方法请求的解析成变量,然后再利用extract()
函数从数组中将变量导入到当前的符号表,故payload为:
?_POST[key1]=36d&_POST[key2]=36d
$$变量覆盖
$$符号在php中叫做 可变变量可以使变量名动态设置。
$$变量覆盖要具体结合代码来看,可能会需要借助某个参数进行传递值,也有可能使用$GLOBALS(引用全局作用域中可用的全部变量)来做题,例如:
1 |
|
可以看到在这里${$a}
等同于$hello
接着我们再来看怎么来进行变量覆盖在第二行中遍历了全局变量$_GET
,第三行将key当作变量名,把value赋值。那么我们传入http://127.0.0.1/1.php?auth=1
时会将$auth
的值覆盖为1
1 | $auth=0; |
import_request_variables
将 GET/POST/Cookie
变量导入到全局作用域中,如果你禁止了 register_globals
,但又想用到一些全局变量,那么此函数就很有用。那么和register_globals
存在相同的变量覆盖问题。
1 | $auth ='0'; |
同样传入http://127.0.0.1/1.php?auth=1
时会将$auth
的值覆盖为1,输出over!
preg_match()
正则匹配函数遇到数组会返回false
前面加上%0a
字符转换
当我们传入参数时,php首先会删除空白符
之后php会自动把一些不合法的字符([ 空格 + .
四种字符)转换为下划线(php8以下),且转换只会发生一次
例如让我们传入e_v.a.l
,为了防止参数后面的小数点被转换,可以传入e[v.a.l
,这样非法字符提前出现,后面的小数点就不会被转换了,且转换后结果一致
不加引号的字符串
PHP 会自动帮我们推断对应值的类型
例如下面的代码执行后会爆 Warning, 但能正常输出flag_give_me
1 | $fl0g = flag_give_me; |
属性类型不敏感
在PHP 7.1 +
的版本中, 对属性类型(public protected private)不敏感
因为protected
和private
反序列化后的结果中含有%00
, 部分题目会禁止这种字符, 可在构造payload时将属性全部改成public
来绕过限制
PHP精度绕过缺陷
详解:http://www.haodaquan.com/12
简单的说因为PHP通常使用IEEE 754
双精度格式而且由于浮点数的精度有限的原因。除此之外取整而导致的最大相对误差为1.11e-16
,当小数小于10^-16
后,PHP对于小数就大小不分了,如下图:
正则回溯
详解:https://www.freebuf.com/articles/web/339976.html
部分内容参考文献
https://www.anquanke.com/post/id/231507#h2-9
https://exp10it.io/2022/08/php-%E7%89%B9%E6%80%A7%E6%80%BB%E7%BB%93%E7%AC%94%E8%AE%B0/#strpos