CTF中常见的 PHP 弱类型漏洞总结
作者:ZERO 所属团队:Arctic Shell
参考资料:
http://archimesan.me/2017/12/21/php%E5%BC%B1%E7%B1%BB%E5%9E%8B%E6%BC%8F%E6%B4%9E/
https://www.cnblogs.com/Mrsm1th/p/6745532.html
https://blog.spoock.com/2016/06/25/weakly-typed-security/
0x1::弱类型与强类型
通常语言有强类型和弱类型两种,强类型指的是强制数据类型的语言,就是说,一个变量一旦被定义了某个类型,如果不经过强制类型转换,这个变量就一直是这个类型,在变量使用之前必须声明变量的类型和名称,且不经强制转换不允许两种不同类型的变量互相操作。我们称之为强类型,而弱类型可以随意转换变量的类型例如可以这样:
$text=1; $text=”string”
也就是说php并不会验证变量的类型,可以随时的转换类型,估计开发者的意图是让程序员可以进行更高效的开发,所以在大量内置函数以及基本结构中使用了很多松散的比较和转换,防止程序中的变量因为程序员的不规范而报错,虽然提升了效率,但是引发了很多安全问题。
类型转换问题
类型转换最常见的就是int转String,String转int。
Int转String:
$num = 5;
方式1:$item = (string)$num;
方式2:$item = strval($num);
String转int:
intval() 函数。(取整函数)
主要问题就出现在这个intval()函数上了。
例子:
var_dump(intval(4))//4 var_dump(intval(‘1asd’))//1 var_dump(intval(‘asd1’))//0
上面三个例子说明了intval()函数在转换字符串的时候即使碰到不能转换的字符串的时候它也不会报错,而是返回0。
例子(来自于大佬的博客http://archimesan.me/2017/12/21/php%E5%BC%B1%E7%B1%BB%E5%9E%8B%E6%BC%8F%E6%B4%9E/)
<?php if($_GET[id]) { mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS); mysql_select_db(SAE_MYSQL_DB); $id = intval($_GET[id]); $query = @mysql_fetch_array(mysql_query("select content from ctf2 where id='$id'")); if ($_GET[id]==1024) { echo "<p>no! try again</p>"; } else{ echo($query[content]); } } ?>
主要问题就是你输入一个1024.1这样就可以利用取整性质进行绕过了。
0x2:比较操作符
在编程中类型转换是不可避免的一个事情,比如说网络编程中get方法或者post方法传入一个需要转换成int的值,再比如说变量间进行比较的时候,需要将变量进行转换,鉴于php是自动进行类型转换,所以会引发很多意想不到的问题。
“= =”与“= = =”比较操作符问题
php有两种比较方式,一种是“= =”一种是“= = =”这两种都可以比较两个数字的大小,但是有很明显的区别。
“= =”:会把两端变量类型转换成相同的,在进行比较。
“= = =”:会先判断两端变量类型是否相同,在进行比较。
这里明确说明,在两个相等的符号中,一个字符串与一个数字相比较时,字符串会转换成数值。
例如:
<?php var_dump("name"==0); //true var_dump("1name"==1); //true var_dump("name1"==1) //false var_dump("name1"==0) //true var_dump("0e123456"=="0e4456789"); //true ?>
观察上述代码,很有意思,按照我们前面讲的规则,name与0相比较的时候,因为name是字符串,所以说转换成数字如果是0的话,0与0相比较自然是true,那末问题来了,下一句中的1name正常来说也是字符串,按照上一句的成立方式,1name应该是0,与1相比较的时候应该为false才对,为什莫为true了呢?我们可以假设一下,带数字的字符串不会变成0,会变成1,这样前三条逻辑就解释的清楚了,但是到第四条就又错了,为此我查了一下php的官方文档,文档是这样说的:当一个字符串当作一个数值来取值,其结果和类型如下:如果该字符串没有包含'.','e','E'并且其数值值在整形的范围之内,该字符串被当作int来取值,其他所有情况下都被作为float来取值,该字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为0。
上述的代码为什莫出现那种奇怪的情况也就解释的清楚了。当不同类型的变量进行比较的时候就会存在变量转换的问题,在转换之后就有可能会存在问题。
hash比较操作符问题
在hash比较的时候也会出现问题例如:
"0e132456789"=="0e7124511451155" //true "0e1abc"=="0" //true4219903
当出现xex模式的时候代表科学计数法,比如1e3=1*10三次方,在进行比较运算时,如果遇到了0e\d+(意思就是0e就是0e,d+的意思是后面全部是数字)这种字符串,就会将这种字符串解析为科学计数法。所以上面例子中2个数的值都是0因而就相等了。如果不满足0e\d+这种模式就不会相等。这个题目在攻防平台中的md5 collision就有考到。
例子:《MD5碰撞》源于南邮攻防平台题目(https://cgctf.nuptsast.com/challenges#Web)
<?php if (isset($_GET['Username']) && isset($_GET['password'])) { $logined = true; $Username = $_GET['Username']; $password = $_GET['password']; if (!ctype_alpha($Username)) {$logined = false;} if (!is_numeric($password) ) {$logined = false;} if (md5($Username) != md5($password)) {$logined = false;} if ($logined){ echo "successful"; }else{ echo "login failed!"; } } ?>
这一段代码的大致意思是输入一个数字和一个字符串,并且让他们的MD5值相同,才可以得到successful, 上文提到过,0e在比较的时候会将其视作为科学计数法,所以无论0e后面是什么,0的多少次方还是0。所以我们只需要输入一个数字和字符串进行MD5加密之后都为0e的即可得出答案。md5('240610708') == md5('QNKCDZO')成功绕过!。
十六进制转换问题
首先我们看一下例子:
"0x1e240"=="123456" //true "0x1e240"==123456 //true "0x1e240"=="1e240"//false
php在接受一个带0x的字符串的时候,会自动把这行字符串解析成十进制的再进行比较,0x1e240解析成十进制就是123456,并且与字符串类型的123456和int型的123456都相同。
例子:《起名字真难》源自源于南邮攻防平台题目(https://cgctf.nuptsast.com/challenges#Web)
<?php function noother_says_correct($number) { $one = ord('1'); $nine = ord('9'); for ($i = 0; $i < strlen($number); $i++) { $digit = ord($number{$i}); if ( ($digit >= $one) && ($digit <= $nine) ) { return false; } } return $number == '54975581388'; } $flag='*******'; if(noother_says_correct($_GET['key'])) echo $flag; else echo 'access denied'; ?>
题目大致的意思就是输入一串key,key呢不可以是数字的形式,但是却要求与54975581388相等,看完题目就知道要求字符串和数字进行比较,想到的就是弱类型,54975581388与之匹配的十六进制的字符串是0xccccccccc。这就很巧了,全不是数字,自然就绕过了,得到flag。
布尔值转换问题
举例:
<?php If ( true=“name”){ echo “success”; }
布尔值可以和任何字符串相等。
0x3:总结
1、字符串和数字比较,字符串会被转换成数字。
2、混合字符串转换成数字,看字符串的第一个。
3、字符串开头以xex开头,x代表数字。会被转换成科学计数法(注意一定要是0e/d+的模式)。但是也有例外如:-1.3e3转换为浮点数是-1300。
4、0x开头的字符串会先解析成十六进制再进行比较
5、布尔值跟任意字符串都弱类型相等。
php内置函数的参数的松散性
主要意思就是php内部函数在调用时给函数传递函数无法接受的参数类型但是却没有报错的情况
json绕过(这个不符合松散型)
首先我们介绍一下什莫是json:JSON概念很简单,JSON 是一种轻量级的数据格式,他基于 javascript 语法的子集,即数组和对象表示。由于使用的是 javascript 语法,因此JSON 定义可以包含在javascript 文件中,对其的访问无需通过基于 XML 的语言来额外解析。
例子:
<?php if (isset($_POST['message'])) { $message = json_decode($_POST['message']); $key ="*********"; if ($message->key == $key) { echo "flag"; } else { echo "fail"; } } else{ echo "~~~~"; } ?>
输入一个数组进行json解码,如果解码后的message与key值相同,会得到flag,主要思想还是弱类型进行绕过,我们不知道key值是什莫,但是我们知道一件事就是它肯定是字符串,这样就可以了,上文讲过,两个等号时会转化成同一类型再进行比较,直接构造一个0就可以相等了。最终payload message={"key":0}。
MD5 ,sha1绕过
首先还是介绍一下这两个函数,这俩都是加密函数,分别进行的时给字符串进行MD5加密和计算字符串的 SHA-1 散列。
但是这个函数都有着缺陷,就是不能处理数组。
这样就很容易绕过了
例子:
<?php if (isset($_POST['a']) and isset($_POST['b'])) { if ($_POST['a'] != $_POST['b']) if (md5($_POST['a']) === md5($_POST['b'])) die('Flag: '.$flag); else print 'Wrong.'; } ?>
直接构造数组就可以绕过了payload: a[]=1&b[]=2
switch绕过
缺陷原理相同,绕过姿势相同,如果switch是数字类型的case的判断时,switch会将其中的参数转换为int类型。如下:
<?php $i ="3name"; switch ($i) { case 0: case 1: case 2: echo "this is two"; break; case 3: echo "flag"; break; } ?>
strcmp绕过这个时候程序输出的是,类型转换的i,结果为3返回flag
strcmp()函数在PHP官方手册中的描述是int strcmp ( string $str1 , string $str2 ),需要给strcmp()传递2个string类型的参数。如果str1小于str2,返回-1,相等返回0,否则返回1。strcmp函数比较字符串的本质是将两个变量转换为ascii,然后进行减法运算,然后根据运算结果来决定返回值。
例子:
<?php $password="*************** if(isset($_POST['password'])){ if (strcmp($_POST['password'], $password) == 0) { echo "Right!!!login success";n exit(); } else { echo "Wrong password.."; } ?>
在这个题目中我们需要自己输入一个password的值和$password相比较但是我们不知道这个password的值,有可能时字符串有可能时数字,这个时候怎末办呢,依然时相同的绕过姿势,试一试数组绕过假设如果传入一个数组会怎末样呢?我们传入password[]=xxx ,绕过成功。
原理是因为函数接受到了不符合的类型,将发生错误,函数返回值为0,所以判断相等。
array_search()、in_array()绕过
首先介绍一下什莫是array_search()函数, array_search() 函数在数组中搜索某个键值,并返回对应的键名。in_array() 函数搜索数组中是否存在指定的值。基本功能是相同的,也就是说绕过姿势也相同。Array系列有两种安全问题,一种是正常的数组绕过,一种是“= =”号问题。先讲第一个数组绕过。
举例:
<?php if(!is_array($_GET['test'])){exit();} $test=$_GET['test']; for($i=0;$i<count($test);$i++){ if($test[$i]==="admin"){ echo "error"; exit(); } $test[$i]=intval($test[$i]); } if(array_search("admin",$test)===0){ echo "flag"; } else{ echo "false"; } ?>
这段代码的意思就是先判断是不是数组,然后在把数组中的内容一个个进行遍历,所有内容都不能等于admin,类型也必须相同,然后转化成int型,然后再进行比较如果填入值与admin相同,则返回flag,如何绕过呢?
基本思路还是不变,因为用的是三个等于号,所以说“= =”号这个方法基本不能用,那就用第二条思路,利用函数接入到了不符合的类型返回“0”这个特性,直接绕过检测。所以payload:test[]=0。
第二个 “= =”的问题
在PHP手册中,in_array()函数的解释是bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ),如果strict参数没有提供或者是false(true会进行严格的过滤),那么in_array就会使用松散比较来判断$needle是否在$haystack中。当strince的值为true时,in_array()会比较needls的类型和haystack中的类型是否相同。
例子:
$array=[0,1,2,'3']; var_dump(in_array('abc', $array)); //true var_dump(in_array('1bc', $array)); //true
通过例子我们就知道了,这个松散的判断就是等于号,所以出现了“= =”号的特性“abc”==0、“1bc”==1,如果不加true的话就可以利用“= =”轻松绕过。array_search同理。
0x4:结束—时刻防备弱类型
作为一个程序员,弱类型确实提升了程序员书写代码的效率,但是也带来了严重的安全问题,可以说一切输入都是有害的,一切输入的类型也是可疑的,永远都要带有怀疑精神去看待弱类型的php下任何比较函数,任何数学运算!