这几天稍微有点休息时间,所以学习了下红日安全小组的代码审计项目,Day1的审计就是探究in_array
函数在使用时可能存在的安全缺陷,然后我就想到了对php的弱类型比较做一个比较系统的学习,顺便把学到的知识记录下来。
这一部分的知识比较多,一篇文章写不完,所以打算两部分或者三部分来完成。
1. in_array()
在in_array()
函数中,只有在第三个参数被设置为true
时才会对给定的值在数组中进行强类型比较搜索。而有些时候开发者可能会直接忘记将第三个参数值设置为true
而导致漏洞产生。
上面是一个简单的例子,在第三个参数没有被设置为true
时,进行就是弱类型比较,字符串1aaa
会被数字1匹配到。
1.1 题目
下面是一道ctf题
index.php :
1 |
|
config.php :
1 |
|
sql :
1 | # 搭建CTF环境使用的sql语句 |
1.2 漏洞分析
这道题的漏洞成因很简单,就是检查用户可控的参数id
是不是在$whitelist
内,$whitelist
就是数据库中的记录对应的条数,比如数据库中有5条记录,那$whitelist
就是[1,2,3,4,5]
。
然后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 | mysql> select make_set(1, 'aaa', 'bbb', 'ccc'); |
这个例子返回的结果是aaa
,将bits转化为二进制,1
转换为二进制的结果是0001
,第0位是1
,所以第0位对应的str1,也就是aaa
,会被输出。
1 | mysql> select make_set(3, 'aaa', 'bbb', 'ccc'); |
将bits位上的数字3
转为二进制是0011
,所以取第0位和第1位的字符串输出,这个返回的结果是aaa,bbb
,两个字符串之间用,
连接。
用make_set
替代concat
爆数据库看下:
1 | // 爆数据库 |
1.3.2 第二层:过滤了or
但是在爆数据表名的时候却发现了问题:or detected
……
1 | // 爆数据表 |
在stop_hack()
函数中过滤了or
,而information_schema
中包含or
,所以也被waf掉了。
一般过滤掉or
时,常见的绕过方法有:
- 双写绕过,但这种一般适用于用
replace()
函数把or
去掉的题目,这种题目一般都不会循环检查,所以双写or
就可以绕过; - 大写
OR
绕过,有的题目只会匹配小写的or
,但是不适用于本题,本题的正则是/$hack/i
,/i
表示大小写不敏感,所以大写的OR
还是会被匹配到; ||
绕过,但是这种适用于or
关键字,对于information_schema
中的or
没用。
现在看起来所有的方法都没用,一番苦搜之后,发现了两种替代方法,其中一种方法仅仅在Mysql版本5.7上适用。
information_schame数据库
我们首先要知道information_schema
是什么,为什么爆数据表、爆字段的时候都需要用到这个库。
information_schema
这个库是用来存储数据库元数据的,例如数据库名、数据表明、列名的数据类型、访问权限等。information_schema
中的表实际上是视图,而不是基本表,所以文件系统上没有与之相关的文件。
TABLES表
information_schema
数据库中的TABLES
数据表,就存储了mysql数据库中的表信息(包括视图),包括表属于哪个数据库,表的类型,存储引擎、创建时间等信息。
所以我们都用information_schema.TABLES
来获取数据表信息。
COLUMNS表
存储数据表中的列信息,包括表有多少个列,每个列的类型。
在sql注入时用information_schema.COLUMNS
来获取字段信息。
innodb
要求:MySQL > 5.6.x
在MySQL中使用最广泛的两种引擎莫过于InnoDB
和MyISAM
。在MySQL5.5版本之前,MyISAM
是MySQL关系数据库管理系统的默认存储引擎。在5.6版本的MySQL中,因为InnoDB引擎对支持事务,具有更高的并发性等优点开始被广泛的使用并取代MyISAM
,在5.6版本中正式成为MySQL的默认引擎。
从5.6.x版本开始,InnoDB
新增了两个数据表:
1 | innodb_index_stats |
从官方的解释可以看到这两个表的效果一样,这两张表中都存储了我们需要的database_name
和table_name
。
用mysql.innodb_table_stats
或是mysql.innodb_index.stats
替代information.schema
,可以得到payload:
1 | // 爆数据表 |
成功获得数据表信息:
但是问题马上来了,我们无法获得字段名,因为这两张表中不包含字段信息。
对于字段名未知的字段可以用union select
进行无列名注入,但是本题偏偏又过滤掉了union
关键字(过滤真不少…),把union
从黑名单中去除后来尝试一下。
在介绍具体的方法之前先看一下具体的例子,有一个users表,其中有id
,name
,email
和salary
字段。
1 | mysql> select * from users; |
使用union进行联合查询
1 | mysql> select 1,2,3,4 union select * from users; |
接着就可以使用数字来获取对应的列名,如第3列就对应了users表中的email
字段。
1 | mysql> select `3` from (select 1,2,3,4 union select * from users)f; |
然后加上limit
我们就可以读取任意一行的信息:
1 | mysql> select `3` from (select 1,2,3,4 union select * from users)f limit 2,1; |
在 ` 符号无法使用的时候,还可以用alias来替代:
而对于本题的flag
数据表在前面一步已经通过mysql.innodb_index_stats
获得了,再通过无列名注入的方式就可以获得数据信息:
最后生成的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 |
在没有过滤掉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
列(自增列),并提供有关这些列的信息。
从上图可以看到,从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(); |
在本地测试的结果:
对应本题有payload:
1 | // 爆数据表 |
获得users
表,但是当将limit 0,1
换成limit 1,1
来查询下一张表的时候却发现没有成功:
这是因为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
在视图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 |
但是这里也还是无法获取到字段信息,找不到一个视图能代替information_schema.COLUMNS
视图,绕来绕去还是要进行无列名注入。
本来本题的重点是in_array()
函数中存在的漏洞,没想到跑这么远,学到更多的是sql注入方面的技巧……
2. md5碰撞
2.1 漏洞原理
这个漏洞是世界上最好的语言php的特性导致的。
php在处理哈希字符串时,会利用!=
或==
来对哈希值进行比较,此时php会把每一个以0e
开头的哈希值都解释为0
:
1 | $ php -r "var_dump('0e768261251903820937390661668547' == '0')"; |
现在的网站一般都是将密码进行加密,并将加密后的结果存到数据库中,然后在登陆的时候取出来进行验证,所以如果数据库中有用户的密码的哈希值是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 | username: admin |
然后用新的密码进行登录:
1 | username: admin |
成功登录管理员账号:
2.2.3 漏洞成因
漏洞关键代码在sources/panel_login.php
文件中
第66行和第67行是进行sql查询,根据用户名$_POST['user']
从数据库中取出对应的用户信息。
$userdata['passwd']
中就是md5加密后的用户密码,然后在第73行将用户登录时input的$_POST['passwd']
密码进行md5加密后与从数据库里取出的密码进行比较,用的是!=
,属于弱比较。
可以看到我们存储在数据库中的密码是0e545993274517709034328855841020
(明文是s878926199a
),登录时用的密码是s155964671a
,对应的md5值为0e342768416822451524974117254469
。
前面已经说过,在进行弱类型比较时,php会认为这两个值相等
1 | $ php -r "var_dump(md5('s155964671a') == md5('s878926199a'));" |
所以接着程序会进入第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行:
这个漏洞和上一个漏洞的成因一样,都是根据用户名从数据库中取出hash加密后的密码,然后将用户登录时的密码也进行hash加密,然后将这两个密码进行比较,问题就是这里第49行采取的是弱类型比较,所以当用户加密后的密码是0e
开头的话,就会导致hash值碰撞。
2.3.3 官方修复
这个漏洞官方已经修复了。
官方的修复就是将==
号改成了强类型比较符号===
。