PHP 是现在网站中最为常用的后端语言之一,是一种类型系统 动态、弱类型的面向对象式编程语言。可以嵌入 html 文本中,是目前最流行的 web 后端语言之一,并且可以和 Web Server 如 apache 和 nginx 方便的融合。目前,已经占据了服务端市场的极大占有量。
但是,弱类型,一些方便的特性由于新手程序员的不当使用,造成了一些漏洞,这篇文章就来介绍一下一些渗透中可以用的特性。
上面都是废话,下面我们进入正题
注:这些漏洞适用于所有版本的 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)
- ?>
我们大概可以总结出如下的规则:当一个字符串被转换为数值时
- <?php
- 'a'==0 // true
- '12a'==12 //true
- '1'==1 //true
- '1aaaa55sss66'==1 //true
当然,上面的那些等式对于 === 都是 false 的,原本一些应该用 === 的地方误用了 ==,导致了可以注入的地方。
- <?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 {
- 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
用 === 可以避免这一漏洞。
- <?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 数据中的数据类型来将其转换为 php 中的相应类型的数据,也就是说,如果我们在 json 中传一个 string 类型,那么该变量就是 string,如果传入的是 number,则该变量为 number。因此,我们如果传入一个数字,就可以使之相等。网页中的表单可能限制了所有的输入都是 string,即使输入数字,传入的东西也是
- json_decode()
- {"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 本身是一个完整的字符串,经过解析之后可能有字符串,数字,布尔等多种类型。
注:这一个漏洞适用与 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..";
- }
- ?>
对于这段代码,我们能用什么办法绕过验证呢, 只要我们
是一个数组或者一个 object 即可,但是上一个问题的时候说到过,只能上传字符串类型,那我们又该如何做呢。
- $_POST['password']
其实 php 为了可以上传一个数组,会把结尾带一对中括号的变量,例如
的 name(就是 $_POST 中的 key),当作一个名字为
- xxx[]
的数组构造类似如下的 request
- xxx
- 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 应用来说, 前端(浏览器端)的安全限制只能起到防止一般用户的误输入行为,完全不可能对于黑帽子的行为有任何的防御作用
因此,在防御这个漏洞的过程中,保证几件事情:
来代替
- ===
- ==
基本上就可以完美的防御这一类的漏洞。
而对于渗透测试人员,在代码审计的过程中,对于有
,
- ==
的比较也应极为敏感 。在黑盒渗透的时候也可以对于代码进行猜测,结合信息搜集过程中的一些版本特性,利用这些漏洞来绕过验证。
- strcmp
来源: http://www.tuicool.com/articles/qQn6nmB