本周继续学习AST的SQL语法检测原理的学习,文章的接下来部分准备分为2部分进行学习:
1. SQL注入语法防御规则 2. druid中SQL注入防御模块sql-wall
1. 相关学习资料
http://code.alibabatech.com/wiki/display/Druid/WallFilter
http://code.alibabatech.com/wiki/display/Druid/WallFilterConfig
http://code.alibabatech.com/wiki/display/Druid/Get+Druid
2. 我对数据库防火墙的理解
数据库防火墙位于前端应用层之后,前端的应用层可以是PHP、ASP、Java等,这些语言通过一些统一的访问接口(ODBC、JDBC)对数据库系统发起访问请求
所以到了数据库这一层的都是纯的SQL请求,所以在数据库这一层面要考虑的不是一些应用系统的ODAY、本地变量覆盖的漏洞,而应该明确我们所处的防御层面,我们要防御的是黑客针对数据库发起的攻击。
1. 针对数据库的缓冲区溢出攻击: 这个是实战中很少见 http://www.yesky.com/94/1828594.shtml 2. 针对数据库底层代码的极限领域的攻击,例如,这是在一个CTF中出现过的Mysql Attack Topic: <?php # GOAL: dump the info for the secret id require ‘db.inc.php‘; $id = @(float)$_GET[‘id‘]; die(var_dump($id)); $secretId = 1; if($id == $secretId) { echo ‘Invalid id (‘.$id.‘).‘; } else { $query = ‘SELECT * FROM users WHERE id = \‘‘.$id.‘\‘;‘; $result = mysql_query($query); $row = mysql_fetch_assoc($result); echo "id: ".$row[‘id‘]."</br>"; echo "name:".$row[‘name‘]."</br>"; } ?> http://localhost/php4fun/index.php?id=1.0000000000001 攻击者的目标的是要查出id为1的admin的数据,这里的绕过思路是利用了Mysql的精度范围和PHP的精度范围不同,精度小的会忽略不能支持的位数。也就是说,浮点型的精度有上限和下限 3. 纯粹的拼接SQL语法对数据进行注入攻击: 这是最常见的,我们接下来重点分析这方面内容
3. SQL注入语法防御规则
目 前,druid的防御重点主要放在拼接型的SQL注入攻击,即利用注入点在原始的SQL语句的中间或后面"插入"、"拼接"上攻击性的SQL Payload,从而达到提取非法数据等目的,缓冲区溢出和特殊情况的攻击druid暂时没有实现,将放到未来的版本中逐渐完善,下面根据温少的文档、并 配合druid的源代码进行学习进行具体规则的学习:
0x1 只允许执行增删改查基本语句 \druid\src\main\java\com\alibaba\druid\wall\WallConfig.java(druid的源码和整体架构放在文章的后半部分) .... //是否允许非以上基本语句的其他语句,缺省关闭,通过这个选项就能够屏蔽DDL。 private boolean noneBaseStatementAllow = false; .... 这是最严格模式,但是也最缺乏灵活性,基本上是不能开启的,在正常的用户业务需求中,必不可少会用到除了CRUD(增删改查)之外的需求,开启这条规则会导致大量的误报,
故druid默认关闭这个开关
0x2 不允许一次执行多条语句 每次只允许执行一条SQL,一次执行多条SQL,是被认为可能正被SQL注入攻击。 1. sql server 6.0在其架构中引入了服务端游标,从而允许在同一连接句柄上执行包含多条语句的字符串。所有6.0之后的sql server版本均支持该功能且允许执行下列语句: select id from users;select name from users; 客户端连接到sql服务器并依次执行每条语句,数据库服务器向客户端返回每条语句发送的结果集。 http://database.51cto.com/art/201007/213806.htm 2. mysql在4.1及之后的版本中也引入了该功能,但是PHP自身限制了这种用法。 <?php $con = mysql_connect("127.0.0.1", "root" , "111"); mysql_select_db("php4fun_", $con); $sql = "update users set level=2;update users set pass=3;"; $result = mysql_query($sql, $con); echo mysql_error(); if($result) { $result_array = mysql_fetch_array($result); var_dump($result_array); } ?> result: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to
use near ‘select 1,2,3,4 from dual‘ at line 1 而如果使用的PDO方式操作数据库 <?php $db = new PDO("mysql:host=localhost:3306;dbname=php4fun_", ‘root‘, ‘111‘); $sql = "update users set level=2;update users set pass=3;"; try { $db->query($sql); } catch(PDOException $e) { echo $e->getMessage(); die(); } ?> result: ok 3. oracle不支持多条语句,除非使用PL/SQL \druid\src\main\java\com\alibaba\druid\wall\WallConfig.java .... private boolean multiStatementAllow = false; .... druid默认是禁止这种格式的SQL语句的,也即如果在传入的SQL语句中解析出了2条及以上的SQLStatement(一个SQLStatement抽象了一条SQL语句)就判断为注入攻击
0x3 不允许访问系统表 在之前的学习笔记中,有总结过,从攻击者渠道的角度去理解,攻击者最终的目的是要获取信息 http://www.cnblogs.com/LittleHann/p/3495602.html 而"访问系统表"就是获取信息的渠道之一,故需要拦截之 但是druid对这种规则的判断更加细化,druid只拦截在子句中出现的连接系统表查询,举例说明: 1. select * from information_schema.COLUMNS; 这条语句druid认为是合法的,因为这条语句没有注入点的存在,SQL语句本身的唯一目的就是查询系统表,说明用户在进行正常的业务操作 2. SELECT id
FROM admin
WHERE id = 1
AND 5 = 6
UNION
SELECT concat(0x5E252421, COUNT(8), 0x2A5B7D2F)
FROM (SELECT `column_name`, `data_type`, `character_set_name`
FROM `information_schema`.`COLUMNS`
WHERE TABLE_NAME = 0x73696E6765725F616C62756D
AND TABLE_SCHEMA = 0x796971696C61695F757466
) t
这条语句druid认为是非法的注入攻击,因为SQL在子句(可能是注入点的地方)采取了union拼接,进行了连接系统表的查询的操作 druid通过判断information_schema在AST层次结构中的位置,具体来说就是判断它的父节点是否为"SQL表达式"(例如union select)、以及它的左节点是否为"From节点"。
即满足子句拼接的模式。以此来判断这条SQL语句是否有攻击性,在代码中的体现就是 druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java ..... boolean sameToTopSelectSchema = false; if (parent instanceof SQLSelectStatement) { SQLSelectStatement selectStmt = (SQLSelectStatement) parent; SQLSelectQuery query = selectStmt.getSelect().getQuery(); if (query instanceof SQLSelectQueryBlock) { SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) query; SQLTableSource from = queryBlock.getFrom(); while (from instanceof SQLJoinTableSource) { from = ((SQLJoinTableSource) from).getLeft(); } if (from instanceof SQLExprTableSource) { SQLExpr expr = ((SQLExprTableSource) from).getExpr(); if (expr instanceof SQLPropertyExpr) { SQLExpr schemaExpr = ((SQLPropertyExpr) expr).getOwner(); if (schemaExpr instanceof SQLIdentifierExpr) { String schema = ((SQLIdentifierExpr) schemaExpr).getName(); schema = form(schema); if (schema.equalsIgnoreCase(owner)) { sameToTopSelectSchema = true; } } } } } } if (!sameToTopSelectSchema) { addViolation(visitor, ErrorCode.SCHEMA_DENY, "deny schema : " + owner, x); } 而代码中的owner是从配置文件中读取的: String owner = ((SQLName) x).getSimleName(); owner = WallVisitorUtils.form(owner); if (isInTableSource(x) && !visitor.getProvider().checkDenySchema(owner)) { ... 配置文件被统一放在了: \druid\src\main\resources\META-INF\druid\wall\mysql\deny-schema.txt information_schema mysql performance_schema 这样,druid就完成了对SQL中的对系统敏感表的注入的智能检测
0x4 不允许访问系统对象 在sqlserver中有系统对象的概念。对敏感系统对象"sysobject"的检测也是同样的原理,即只检测子句的非法连接,并从配置文件中读取拦截列表,代码和对系统表的检测是类似的
0x5 不允许访问系统变量 系统敏感变量同样也是攻击者获取非法数据的一种渠道,druid采取智能判断的做法,举例说明: 1. select @@basedir; 这条语句druid不做拦截,因为这里没有注入点的存在,也就不可能是黑客的注入攻击,应该归类于业务的正常需要 2. SELECT * FROM cnp_news where id=‘23‘ and len(@@version)>0 and ‘1‘=‘1‘ 这条语句druid会做拦截,攻击者在子句中利用逻辑表达式进行非法的探测注入,目前druid的检测机制是"黑名单机制",把需要禁止的系统变量写在了配置文件中: druid\src\main\resources\META-INF\druid\wall\mysql\deny-variant.txt basedir version_compile_os version datadir druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java ... if (!checkVar(x.getParent(), x.getName())) { boolean isTop = WallVisitorUtils.isTopNoneFromSelect(this, x); if (!isTop) { boolean allow = true; if (WallVisitorUtils.isWhereOrHaving(x) && isDeny(varName)) { allow = false; } if (!allow) { violations.add(new IllegalSQLObjectViolation(ErrorCode.VARIANT_DENY, "variable not allow : " + x.getName(), toSQL(x))); } } } ...
0x6 不允许访问系统函数 和"系统敏感表"、"系统敏感对象"、"系统敏感变量"一样,系统敏感函数也是攻击者用来获取非法信息的一种手段之一 druid中和禁用系统函数的配置文件: druid\src\main\resources\META-INF\druid\wall\mysql\deny-function.txt version load_file database schema user system_user session_user benchmark current_user sleep xmltype receive_message 对于系统敏感函数的禁用,这里要注意一下,和系统表的防御思想类型,druid会智能地判断敏感函数在SQL语句中出现的位置,例如: 1. select load_file(‘\\etc\\passwd‘); druid不会拦截这条语句,还是同样的道理,SQL注入的关键在于注入点,这条语句没有注入点的存在,所以只能是用户正常的业务需求 2. select * from admin where id =(SELECT 1 FROM (SELECT SLEEP(0))A); druid会智能地检测出这个敏感函数出现在"where子句节点"中,而"where子句节点"经常被黑客用来当作一个SQL注入点,故druid拦截之 代码如下: druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java public static void checkFunction(WallVisitor visitor, SQLMethodInvokeExpr x) { final WallTopStatementContext topStatementContext = wallTopStatementContextLocal.get(); if (topStatementContext != null && (topStatementContext.fromSysSchema || topStatementContext.fromSysTable)) { return; } checkSchema(visitor, x.getOwner()); if (!visitor.getConfig().isFunctionCheck()) { return; } String methodName = x.getMethodName().toLowerCase(); WallContext context = WallContext.current(); if (context != null) { context.incrementFunctionInvoke(methodName); } if (!visitor.getProvider().checkDenyFunction(methodName)) { boolean isTopNoneFrom = isTopNoneFromSelect(visitor, x); if (isTopNoneFrom) { return; } boolean isShow = x.getParent() instanceof MySqlShowGrantsStatement; if (isShow) { return; } if (isWhereOrHaving(x)) { addViolation(visitor, ErrorCode.FUNCTION_DENY, "deny function : " + methodName, x); } } }
0x7 不允许出现注释 正常执行的SQL是不应该附带注释的,有注释的SQL都会被认为是危险操作。druid是默认"禁止"单行注释和多行注释。这里所谓的"禁止"是值druid会在解析前自动地去除原始SQL语句中的注释。 例如攻击者常用的绕过方式: 1) sel/**/ect us/**/er() from dual; (黑客常用来绕过基于正则前端WAF) 2) select * from admin where no=4 and 1=2 /!40001+union/ select 1,concat(database(),0x5c,user(),0x5c,version()),3,4,5,6,7
(Mysql的comment dynamic execution bypass)
http://www.freebuf.com/articles/web/22041.html 这里druid采取的防御思路是"规范化",代码自动会将注释的部分删除,重新拼接SQL语句后,对"规范化"后的语句再进行注入检测,删除注释的代码逻辑在词法解析器中: druid\src\main\java\com\alibaba\druid\sql\parser\Lexer.java .. protected boolean skipComment = true; .. public final void nextToken() { .... /* 解析‘#‘注释符 判断‘#‘解析出的节点是‘单行注释‘、或‘多行注释‘ */ case ‘#‘: scanSharp(); if ((token() == Token.LINE_COMMENT || token() == Token.MULTI_LINE_COMMENT) && skipComment) { bufPos = 0; continue; } return; .... /* 检测是否是‘--‘这种单行注释符 */ if (subNextChar == ‘-‘) { scanComment(); if ((token() == Token.LINE_COMMENT || token() == Token.MULTI_LINE_COMMENT) && skipComment) { bufPos = 0; continue; } } ... /* 判断当前节点是否是 /* */ 这种类型的多行注释 */ if (nextChar == ‘/‘ || nextChar == ‘*‘) { scanComment(); if ((token() == Token.LINE_COMMENT || token() == Token.MULTI_LINE_COMMENT) && skipComment) { bufPos = 0; continue; } } ... 在对SQL的词法解析的过程中,druid就会自动地对各种形式的注释符进行删除,删除了注释后,druid再去解析SQL语句,这个时候会出现两个情况: 1) 解析失败抛异常,说明原本的SQL语句很有可能是攻击型的SQL语句,黑客使用了注释绕过或者注释执行技术 2) 解析正常,说明这是正常的SQL语句,不排除有的程序猿会把一些简短的注释写在SQL语句中,但是这个注释的删除对原本的执行没有影响,所以也就判定为合理SQL语句 Oracle Hints的语法是/* + */,druid能够区分注释和Hints
0x8 禁止永真条件 永真的注入是黑客在攻击中最常见的攻击手段,黑客通过注入"永真表达式"来探测当前"用户可控的输入点"是否可以转化为"可以导致注入的注入点", 但是druid的永真检测并不是简单的"等式匹配",而是对真正黑客可能采用的攻击模式进行结果化的匹配。 例如: 1) 正常的业务语句 SELECT F1, F2 FROM ADMIN WHERE 1 = 1; -- 允许 SELECT F1, F2 FROM ADMIN WHERE 0 = 0; -- 允许 SELECT F1, F2 FROM ADMIN WHERE 1 != 0; -- 允许 SELECT F1, F2 FROM ADMIN WHERE 1 != 2; -- 允许 这里允许的理由是,在正常的业务中有可能有这样的语句: <?php ... $sql = "SELECT F1, F2 FROM ADMIN WHERE 1 = $id"; .. //这是很常见的业务语句,当外部系统传入的$id=1的时候,到了数据库驱动层这里看到的语句就是: SELECT F1, F2 FROM ADMIN WHERE 1 = 1 了。但这并不能算是一条永真注入探测语句。 所以,druid目前的规则允许的判断方式是,在where子句(where节点的子节点)中只有一个"等于"或"不等于"的二元操作表达式(上面给出的例子),druid会判断为合法。 druid对永真注入探测的防御重点是针对where子句(where节点的子节点)后面的永真逻辑的判断,对where子句中超过2个及以上的永真逻辑表达式进行拦截,例如: select * from admin where id =-1 OR 17-7=10; -- 拦截 select * from admin where id =-1 and 1=2 -- 拦截 select * from admin where id =-1 and 2>1 -- 拦截 select * from admin where id =-1 and ‘a‘!=‘b‘ -- 拦截 select * from admin where id =-1 and char(32)>char(31) -- 拦截 select * from admin where id =-1 and ‘1‘ like ‘1‘ -- 拦截 select * from admin where id =-1 and 17-1=10 -- 拦截 select * from admin where id =-1 and NOT (1 != 2 AND 2 != 2) --拦截 select * from admin where id =-1 and id like ‘%%‘ -- 拦截 select * from admin where id =-1 and length(‘abcde‘) >= 5 -- 拦截 druid的实现核心代码如下: druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java public static void checkSelelct(WallVisitor visitor, SQLSelectQueryBlock x) { ... /* 目前druid只针对where节点进行判断,下一版本会提供对order by和group by类型节点的判断 */ if (Boolean.TRUE == whereValue) { if (queryBlockFromIsNull(visitor, x, false)) { addViolation(visitor, ErrorCode.EMPTY_QUERY_HAS_CONDITION, "empty select has condition", x); } if (!isSimpleConstExpr(where)) { // 简单表达式 addViolation(visitor, ErrorCode.ALWAY_TRUE, "select alway true condition not allow", x); } } ..
0x9 Getshell 1) into outfile 黑客常常使用这个技术利用注入点进行磁盘写入。进而getshell,获得目标服务器的控制权 同样,druid的拦截是智能的,它只对真正的注入进行拦截,而正常的语句,例如: 1.1) 有的业务情况会要求记录每个用户的登录IP select "127.0.0.1" into outfile ‘c:\index.php‘; -- 允许 1.2) 而攻击者常用的攻击语句 select id from messages where id=1 and 1=2 union select 0x3C3F706870206576616C28245F504F53545B2763275D293F3E into outfile ‘c:\shell.php‘; 这个语句会被拦截下来
0xA 盲注 1) order by select * from cnp_news where id=‘23‘ order by if((len(@@version)>0),1,0); 利用盲注思想来进行注入,获取敏感信息 2) group by select * from cnp_news where id=‘23‘ group by (select @@version); 利用数据库的错误信息报错来进行注入,获取敏感信息 3) having select * from users where id=1 having 1=(nullif(ascii((SUBSTRING(user,1,1))),0)); 利用数据库的错误信息进行列名的盲注、 druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java /* Having 如果Having条件出现了永真,则认为正处于被攻击状态。例如: SELECT F1, COUNT(*) FROM T GROUP BY F1 HAVING 1 = 1 */ if (Boolean.TRUE == getConditionValue(visitor, x, visitor.getConfig().isSelectHavingAlwayTrueCheck())) { if (!isSimpleConstExpr(x)) { addViolation(visitor, ErrorCode.ALWAY_TRUE, "having alway true condition not allow", x); } }
4. druid测试环境的搭建
http://code.alibabatech.com/wiki/display/Druid/Get+Druid
下载这个jar包之后,在eclipse中创建新的工程,并引入jar包
在工程中新建一个类: CheckInvaild.java
http://files.cnblogs.com/LittleHann/CheckInvaild.rar
点击运行即可测试SQL代码
5. druid的源代码架构
druid的源代码有很多,我并没有全部看懂,只是把SQL注入检测相关的wall部分给看了一遍
druid\src\main\java\com\alibaba\druid\sql\parser\Lexer.java
这个类负责把整个SQL进行"词法解析(注意和语法解析区分)",即把一个完整的SQL语句进行切分,拆分成一个个单独的SQL Token:
public enum Token { SELECT("SELECT"), DELETE("DELETE"), INSERT("INSERT"), UPDATE("UPDATE"), FROM("FROM"), HAVING("HAVING"), WHERE("WHERE"), ORDER("ORDER"), ...
这个SQL Token序列然后作为"语法解析器"的输入
druid\src\main\java\com\alibaba\druid\sql\parser\SQLParser.java
最终SQL字符串被解析成一个AST结构对象SQLStatement
一个SQLStatement就是对一条SQL语句的抽象,之前说过,SQL语言是一个结构化很严格的语言,所以在SQLStatement根节点下有很多子节点:
SQLSelectStatement、SQLUpdateStatement、SQLFromStatement...
最终形成一个由不同层次的"节点"组成的AST语法树
AST语法树生成后,druid采用了"访问者设计模式",因为在druid的项目中,对象列表是相对不容易变动的,而访问方式(也就是SQL注入的检测规则)是相对容易不断变化的(因为我们防注入的规则是在不断变化的)
http://www.knowsky.com/370713.html
而我们之前说的规则就是从实现访问者的这些访问者对象中提取出来的,和mysql相关的主要有两个文件:
MysqlWalVisitor.java、WallVisitorUtils.java
我们要实现对druid的SQL注入检测规则优化,也就是从这些访问者中进行修改
6. 目前存在的问题需要改进
1. 在review日志的时候发现用户的业务SQL中也出现了子句中的永真导致的误报,这块需要细化一下规则,减少误报 2. 目前druid对盲注的拦截能力还有待提高: 2.1 order by 2.2 group by 2.3 having 目前对永真的判断基本是在where这个点上,对其他点存在的注入点和盲注的解析和检测代码还有待完善 3. 对Mysql的词法中出现的注释(/*!432...*/)的解析还有待完善,接下来的工作希望能正确解析这块注释代码,而不是简单的删除,这样可以防止注释型的bypass