一、PHP弱类型==的漏洞

1、原理

注:这些漏洞适用于所有版本的PHP

  先来复习一下基本的语法:php中有如下两种比较符号:两个等号和三个等号(这一点和Javascript)有些类似

$a==$b
$a===$b

  我们来一下php官方手册的说法

如果类型转换后 $a 等于 $b,则 $a == $b 等于 TRUE。如果 $a 等于 $b,并且它们的类型也相同,则 $a === $b 全等 TRUE

  明确的看到,两个等于号的等于会在比较的时候进行类型转换的比较

如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换为数值并且比较按照数值来进行。此规则也适用于 switch 语句。当用 === 或 !== 进行比较时则不进行类型转换,因为此时类型和数值都要比对.

  明确的写出了,如果一个数值和一个字符串比较,那么会将字符串转换为数值(而不是相反,将数值转化为字符串)

  然而,php是如何将一个字符串转化为数值的呢,我们继续查看php手册

当一个字符串被当作一个数值来取值,其结果和类型如下:如果该字符串没有包含 ‘**.’,’e’ 或 ‘E’ 并且其数字值在整型的范围之内(由 PHP_INT_MAX 所定义),该字符串将被当成 integer 来取值。其它所有情况下都被作为 float 来取值。该字符串的开始部分决定了它的值**。如果该字符串以合法的数值开始,则使用该数值。否则其值为 0(零)。合法数值由可选的正负号,后面跟着一个或多个数字(可能有小数点),再跟着可选的指数部分。指数部分由 ‘e’ 或 ‘E’ 后面跟着一个或多个数字构成。

  这是官方手册上面的几个例子

<?php
$foo = 1 + "10.5"; // $foo is float (11.5)
$foo = 1 + "-1.3e3"; // $foo is float (-1299)
$foo = 1 + "bob-1.3e3"; // $foo is integer (1)
$foo = 1 + "bob3"; // $foo is integer (1)
$foo = 1 + "10 Small Pigs"; // $foo is integer (11)
$foo = 4 + "10.2 Little Piggies"; // $foo is float (14.2)
$foo = "10.0 pigs " + 1; // $foo is float (11)
$foo = "10.0 pigs " + 1.0; // $foo is float (11)
?>

我们大概可以总结出如下的规则:当一个字符串被转换为数值时

  • 如果一个字符串为 “合法数字+e+合法数字”类型,将会解释为科学计数法的浮点数

  • 如果一个字符串为 “合法数字+ 不可解释为合法数字的字符串”类型,将会被转换为该合法数字的值,后面的字符串将会被丢弃

  • 如果一个字符串为“不可解释为合法数字的字符串+任意”类型,则被转换为0! 为0…为0

<?php
'a'==0 // true
'12a'==12 //true
'1'==1 //true
'1aaaa55sss66'==1 //true

  当然,上面的那些等式对于===都是false的,原本一些应该用===的地方误用了==,导致了可以注入的地方。

2、示例代码1:利用转为数字后相等的漏洞

<?php
if (isset($_GET['v1']) && isset($_GET['v2'])) {
$logined = true;
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if (!ctype_alpha($v1)) {$logined = false;} //ctype_alpha函数检查提供的字符串,文本中的所有字符是否都是字母,是则返回True,否则返回False,因此这里要全是字母
if (!is_numeric($v2) ) {$logined = false;} //如果指定的变量是数字和数字字符串则返回 TRUE,否则返回 FALSE,因此必须包含且只能包含数字/数字字符串
if (md5($v1) != md5($v2)) {$logined = false;}//如果md5值不相等,则返回True,因此这里要返回相等的md5

if ($logined){

// continuue to do other things
} else {
echo "login failed"
}
}
?

  这是一个CTF的题目,非常有趣,可以看到,要求给出两字符串,一个是纯数字型,一个只能出现字符,使两个的md5哈希值相等,然而这种强碰撞在密码学上都是无法做到的。但是我们看到,最终比较两者的哈希的时候,使用的是等于 而不是 全等于 ,因此可以利用一下这个漏洞

  再回头看一 md5()函数

string md5 ( string $str [, bool $raw_output = false ] )

  可以知道,第二个参数为true的时候,显示16位的结果,而为false和没有第二个参数时,为32位的16进制码(16位的结果是把32位的作为ASCII码进行解析)

  16进制的数据中是含有e的,可以构建使得两个数字比较的,这里有一个现成的例子:

md5('240610708') 
//0e462097431906509019562988736854.
md5('QNKCDZO')
//0e830400451993494058024219903391

  可以看到,这两个字符串一个只包含数字,一个只包含字母,虽然两个的哈希不一样,但是都是一个形式:0e 纯数字这种格式的字符串在判断相等的时候会被认为是科学计数法的数字,先做字符串到数字的转换。

  转换后都成为了0的好多好多次方,都是0,相等。(大家可以自己尝试一下)因此

md5('240610708')==md5('QNKCDZO'); //True
md5('240610708')===md5('QNKCDZO'); //False

用===可以避免这一漏洞。

3、示例代码2: 利用 类’a’==0的漏洞

<?php

if (isset($_POST['json'])) {

$json = json_decode($_POST['json']);
$key ="**********************";
if ($json->key == $key) {
//login success ,continue
} else {
//login failed ,return
}

?>

  这次这个例子是传入一个JSON的数据,JSON在RESTful的网站中是很常用的一种数据传输的格式。这个表单会把一个name为key的input的数据作为json传到服务端

{"key":"your input"}

  我们该如何破解?想”a”==0这个漏洞,之用我们使$json->key是一个数字类型的变量就可以,怎么做到呢?

  php的json_decode()函数会根据json数据中的数据类型来将其转换为php中的相应类型的数据,也就是说,如果我们在json中传一个string类型,那么该变量就是string,如果传入的是number,则该变量为number。因此,我们如果传入一个数字,就可以使之相等。网页中的表单可能限制了所有的输入都是string,即使输入数字,传入的东西也是

{"key":"0"}

  这是一个字符串0,我们需要让他为数字类型,用burp拦截,把两个双引号去掉,变成这样即可:

{"key":0}

  值得讨论的一点是,在这种方法的漏洞利用中,很难在直接表单类型的POST的数据中使用,这是为什么呢,这个和HTTP协议有关。首先,我们看一下,在POST给服务器的数据中,有几种类型,也就是HTTP header中的Content-Type:

application/x-www-form-urlencoded
multipart/form-data
application/json
application/xml

  第一个application/x-www-form-urlencoded,是一般表单形式提交的。content-type第二个,是包含文件的表单。第三,四个,分别是json和xml,一般是js当中上传的

  但是因为在直接的POST的payload当中是无法区分字符串和数字的,因为在其中并没有引号出现,举一个抓包的例子

POST /login HTTP/1.1
Host: xxx.com
Content-Length: 41
Accept: application/json, text/javascript,application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Connection: close

username=admin&password=admin

  可以看到,payload是放在http包的最后面的,而且都是以没有引号的形式传递的,并没有办法区分到底是字符串还是数字。因此,PHP将POST的数据全部保存为字符串形式,也就没有办法注入数字类型的数据了。而JSON则不一样,JSON本身是一个完整的字符串,经过解析之后可能有字符串,数字,布尔等多种类型。

二、md5()漏洞

1、数组绕过

  md5()函数无法处理数组,如果传入的为数组,会返回NULL,所以两个数组经过加密后得到的都是NULL,也就是相等的。

img

2、利用==漏洞绕过

  利用==比较漏洞:如果两个字符经MD5加密后的值为 0exxxxx形式,就会被认为是科学计数法,且表示的是0*10的xxxx次方,还是零,都是相等的

  下列的字符串的MD5值都是0e开头的:

QNKCDZO

240610708

s878926199a

s155964671a

s214587387a

s214587387a

0e开头的md5和原值:

s878926199a
0e545993274517709034328855841020

s155964671a
0e342768416822451524974117254469

s214587387a
0e848240448830537924465865611904

s214587387a
0e848240448830537924465865611904

s878926199a
0e545993274517709034328855841020

s1091221200a
0e940624217856561557816327384675

s1885207154a
0e509367213418206700842008763514

s1502113478a
0e861580163291561247404381396064

s1885207154a
0e509367213418206700842008763514

s1836677006a
0e481036490867661113260034900752

s155964671a
0e342768416822451524974117254469

s1184209335a
0e072485820392773389523109082030

s1665632922a
0e731198061491163073197128363787

s1502113478a
0e861580163291561247404381396064

s1836677006a
0e481036490867661113260034900752

s1091221200a
0e940624217856561557816327384675

s155964671a
0e342768416822451524974117254469

s1502113478a
0e861580163291561247404381396064

s155964671a
0e342768416822451524974117254469

s1665632922a
0e731198061491163073197128363787

s155964671a
0e342768416822451524974117254469

s1091221200a
0e940624217856561557816327384675

s1836677006a
0e481036490867661113260034900752

s1885207154a
0e509367213418206700842008763514

s532378020a
0e220463095855511507588041205815

s878926199a
0e545993274517709034328855841020

s1091221200a
0e940624217856561557816327384675

s214587387a
0e848240448830537924465865611904

s1502113478a
0e861580163291561247404381396064

s1091221200a
0e940624217856561557816327384675

s1665632922a
0e731198061491163073197128363787

s1885207154a
0e509367213418206700842008763514

s1836677006a
0e481036490867661113260034900752

s1665632922a
0e731198061491163073197128363787

s878926199a
0e545993274517709034328855841020

三、strcmp漏洞

注:这一个漏洞适用与5.3之前版本的php

  我们首先看一下这个函数,这个函数是用于比较字符串的函数

int strcmp ( string $str1 , string $str2 )

  参数 str1第一个字符串。str2第二个字符串。如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。

  可知,传入的期望类型是字符串类型的数据,但是如果我们传入非字符串类型的数据的时候,这个函数将会有怎么样的行为呢?实际上,当这个函数接受到了不符合的类型,这个函数将发生错误,但是在5.3之前的php中,显示了报错的警告信息后,将return 0 !!!! 也就是虽然报了错,但却判定其相等了。这对于使用这个函数来做选择语句中的判断的代码来说简直是一个致命的漏洞,当然,php官方在后面的版本中修复了这个漏洞,使得报错的时候函数不返回任何值。但是我们仍然可以使用这个漏洞对使用老版本php的网站进行渗透测试。看一段示例代码:

<?php
$password="***************"
if(isset($_POST['password'])){

if (strcmp($_POST['password'], $password) == 0) {
echo "Right!!!login success";n
exit();
} else {
echo "Wrong password..";
}
?>

  对于这段代码,我们能用什么办法绕过验证呢, 只要我们$_POST[‘password’]是一个数组或者一个object即可,但是上一个问题的时候说到过,只能上传字符串类型,那我们又该如何做呢。

  其实php为了可以上传一个数组,会把结尾带一对中括号的变量,例如 xxx[]的name(就是$_POST中的key),当作一个名字为xxx的数组构造类似如下的request

POST /login HTTP/1.1
Host: xxx.com
Content-Length: 41
Accept: application/json, text/javascript
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Connection: close

password[]=admin

  即可使得上述代码绕过验证成功。

四、总结

  这一类型的漏洞的特点主要就是利用PHP中 的类型特性来绕过验证。由于 == 和 === 有着明显的区分,因此,估计短期内PHP的作者并不会调整对于这两个符号的策略。而对于开发市场而言,随着培训机构的增多,后端程序员尤其是php后端程序员的门槛越来越低,其水平必定也是良莠不齐,这些二把刀程序员可能带来更多的此类对于特性的不当使用导致的漏洞,因此这类漏洞仍然是非常具有利用价值的。

  记住保证安全的几句箴言:任何用户输入都是不可信的!对于web应用来说,前端(浏览器端)的安全限制只能起到防止一般用户的误输入行为,完全不可能对于黑帽子的行为有任何的防御作用

  因此,在防御这个漏洞的过程中,保证几件事情:

  • 在所有可能的地方,都使用===来代替==

  • 对于用户输入做过滤和类型检查

  • 尽量使用新版本的php,apache

  • 基本上就可以完美的防御这一类的漏洞。

  而对于渗透测试人员,在代码审计的过程中,对于有==,strcmp的比较也应极为敏感 。在黑盒渗透的时候也可以对于代码进行猜测,结合信息搜集过程中的一些版本特性,利用这些漏洞来绕过验证。