PHP渗透中的奇淫技巧--检查相等时的漏洞
PHP是现在⽹站中最为常⽤的后端语⾔之⼀,是⼀种类型系统动态、弱类型的⾯向对象式编程语⾔。可以嵌⼊HTML⽂本中,是⽬前最流⾏的web后端语⾔之⼀,并且可以和Web Server 如apache和nginx⽅便的融合。⽬前,已经占据了服务端市场的极⼤占有量。
但是,弱类型,⼀些⽅便的特性由于新⼿程序员的不当使⽤,造成了⼀些漏洞,这篇⽂章就来介绍⼀下⼀些渗透中可以⽤的特性。
上⾯都是废话,下⾯我们进⼊正题
1.弱类型的⽐较==导致的漏洞
注:这些漏洞适⽤于所有版本的php
先来复习⼀下基本的语法:php中有如下两种⽐较符号:两个等号和三个等号(这⼀点和Javascript)有些类似
$a==$b
$a===$b
我们来⼀下php官⽅⼿册的说法
$a == $b 等于TRUE,如果类型转换后 $a 等于 $b。$a === $b 全等 TRUE,如果 $a 等于 $b,并且它们的类型也相同。
明确的看到,两个等于号的等于会在⽐较的时候进⾏类型转换的⽐较。
如果⽐较⼀个数字和字符串或者⽐较涉及到数字内容的字符串,则字符串会被转换为数值并且⽐较按照数值来进⾏。此规则也适⽤于 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的,原本⼀些应该⽤===的地⽅误⽤了==,导致了可以注⼊的地⽅。
⽰例代码 1:利⽤转为数字后相等的漏洞
<?php
if (isset($_GET['v1']) && isset($_GET['v2'])) {
$logined = true;
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if (!ctype_alpha($v1)) {$logined = false;}
if (!is_numeric($v2) ) {$logined = false;}
if (md5($v1) != md5($v2)) {$logined = false;}
if ($logined){
// continuue to do other things
} else {php中header是什么意思
echo "login failed"
}
}
这是⼀个ctf的题⽬,⾮常有趣,可以看到,要求给出两字符串,⼀个是纯数字型,⼀个只能出现字符,使两个的md5哈希值相等,然⽽这种强碰撞在密码学上都是⽆法做到的。
但是我们看到,最终⽐较两者的哈希的时候,使⽤的是等于⽽不是全等于,因此可以利⽤⼀下这个漏洞
再回头看⼀md5()函数
string md5 ( string $str [, bool $raw_output = false ] )
str原始字符串。raw_output如果可选的 raw_output 被设置为 TRUE,那么 MD5 报⽂摘要将以16字节长度的原始⼆进制格式返回。
可以知道,第⼆个参数为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
⽤===可以避免这⼀漏洞。
⽰例代码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
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本⾝是⼀个完整的字符串,经过解析之后可能有字符串,数字,布尔等多种类型。
2. 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 "Rightlogin success";n
exit();
} else {
echo "Wrong password..";
}
>
对于这段代码,我们能⽤什么办法绕过验证呢,只要我们$_POST['password']是⼀个数组或者⼀个object即可,但是上⼀个问题的时候说到过,只能上传字符串类型,那我们⼜该如何做呢。
其实php为了可以上传⼀个数组,会把结尾带⼀对中括号的变量,例如xxx[]的name(就是$_POST中的key),当作⼀个名字为xxx的数组构造类似如下的request
POST /login HTTP/1.1
Host: xxx
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
即可使得上述代码绕过验证成功。
3 总结
这⼀类型的漏洞的特点主要就是利⽤PHP中的类型特性来绕过验证。由于 == 和 === 有着明显的区分,因此,估计短期内PHP的作者并不会调整对于这两个符号的策略。⽽对于开发市场⽽⾔,随着培训机构的增多,后端程序员尤其是php后端程序员的门槛越来越低,其⽔平必定也是良莠不齐,这些⼆把⼑程序员可能带来更多的此类对于特性的不当使⽤导致的漏洞,因此这类漏洞仍然是⾮常具有利⽤价值的。
总结⼀下,对于开发⼈员,需要坚持⼏个习惯:
认真阅读PHP manual,不能以其他语⾔的经验来完全带⼊php进⾏编码
在使⽤⼀个运算符或者函数之前,详细的查看⽂档,搞清楚函数在什么样的条件下,会有怎样的⾏为。
记住保证安全的⼏句箴⾔:任何⽤户输⼊都是不可信的!对于web应⽤来说,前端(浏览器端)的安全限制只能起到防⽌⼀般⽤户的误输⼊⾏为,完全不可能对于⿊帽⼦的⾏为有任何的防御作⽤
因此,在防御这个漏洞的过程中,保证⼏件事情:
在所有可能的地⽅,都使⽤===来代替==
对于⽤户输⼊做过滤和类型检查
尽量使⽤新版本的php,apache
基本上就可以完美的防御这⼀类的漏洞。
⽽对于渗透测试⼈员,在代码审计的过程中,对于有==,strcmp的⽐较也应极为敏感。在⿊盒渗透的时候也可以对于代码进⾏猜测,结合信息搜集过程中的⼀些版本特性,利⽤这些漏洞来绕过验证。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论