SQL注入检测模块、开源项目DRUID-SQL-WALL学习小结 (3)

作为SQL注入原理、SQL注入检测、防御系列学习的第三篇。本文主要关注了抽象语法树AST在SQL注入检测上的应用、以及开源项目DRUID-SQL-WALL的学习,希望能给研究这一领域的朋友带来一点帮助,同时也希望能引发大家的共同讨论,共同学习、成长

上一篇文章中,我们学习了数据库防火墙的一些基本知识

http://www.cnblogs.com/LittleHann/p/3505410.html

文章的接下来部分准备分为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

https://files.cnblogs.com/LittleHann/CheckInvaild.rar

点击运行即可测试SQL代码

 

5. druid的源代码架构

这里解释一下druid从解析到判断SQL语句的注入攻击性的代码检测流程

1) 词法解析

这一步负责把整个SQL字符串进行"词法解析(注意和语法解析区分)",即把一个完整的SQL语句进行切分,拆分成一个个单独的SQL Token,即解析成"此法数":

druid\src\main\java\com\alibaba\druid\sql\parser\Token.java
public
enum Token { SELECT("SELECT"), DELETE("DELETE"), INSERT("INSERT"), UPDATE("UPDATE"), FROM("FROM"), HAVING("HAVING"), WHERE("WHERE"), ORDER("ORDER"), ...
druid\src\main\java\com\alibaba\druid\wall\WallProvider.java
public
WallCheckResult check(String sql) { .. return checkInternal(sql);   .. private WallCheckResult checkInternal(String sql) { .. SQLStatementParser parser = createParser(sql);   ..

 创建词法解析器: Lexer,并准备开始处理第一个token

druid\src\main\java\com\alibaba\druid\sql\parser\SQLParser.java
public
SQLParser(String sql) { this(new Lexer(sql)); this.lexer.nextToken(); }

 设置是否忽略注释,当前扫描偏移index等信息

druid\src\main\java\com\alibaba\druid\sql\parser\Lexer.java
public
Lexer(String input, boolean skipComment) { this.skipComment = skipComment; this.text = input; this.pos = -1; scanChar();
}

 对SQL的词法token进行识别

druid\src\main\java\com\alibaba\druid\sql\parser\Lexer.java 
public final void nextToken() { bufPos = 0; for (;;) { if (isWhitespace(ch)) { scanChar(); continue; } if (ch == '$' && charAt(pos + 1) == '{') { scanVariable(); return; } if (isFirstIdentifierChar(ch)) { if (ch == 'N') { if (charAt(pos + 1) == '\'') { ++pos; ch = '\''; scanString(); token = Token.LITERAL_NCHARS; return; } .............

这里使用了访问者模式对SQL字符串进行词法解析

druid\src\main\java\com\alibaba\druid\sql\parser\SQLParser.java
public
void accept(Token token) { if (lexer.token() == token)   { lexer.nextToken(); }   else   { setErrorEndPos(lexer.pos()); throw new ParserException("syntax error, expect " + token + ", actual " + lexer.token() + " " + lexer.stringVal() + ", pos " + this.lexer.pos());   }
}

2) 语法解析

注意和第一步的词法解析区分开来, 这一步是在词法树(Tokens)的基础上,对Tokens中的Token节点进行语义识别(SQL语义),将其解析成一个语法树(符合SQL语法的规范化结构)

druid\src\main\java\com\alibaba\druid\wall\WallProvider.java
private
WallCheckResult checkInternal(String sql) { ... parser.parseStatementList(statementList); final Token lastToken = parser.getLexer().token(); ..

 遍历token词法树,构建语法树结构列表

druid\src\main\java\com\alibaba\druid\sql\parser\SQLStatementParser.java
public
void parseStatementList(List<SQLStatement> statementList, int max) { for (;;)   { if (max != -1)   { if (statementList.size() >= max)      { return; } } if (lexer.token() == Token.EOF)   { return; } if (lexer.token() == (Token.SEMI))   { lexer.nextToken(); continue; } if (lexer.token() == Token.SELECT)   { statementList.add(parseSelect()); continue; } ..

 针对SELECT节点创建特定的解析器,因为SQL是一个结构化的语言,SELECT节点下面还有别的子节点,需要继续递归的解析。创建SELECT解析器的同时将当前SELECT节点添加到当前的SQLStatement语法树中

druid\src\main\java\com\alibaba\druid\sql\parser\SQLStatementParser.java
public
SQLSelectStatement parseSelect() { SQLSelectParser selectParser = createSQLSelectParser(); SQLSelect select = selectParser.select(); return new SQLSelectStatement(select); }

 

druid\src\main\java\com\alibaba\druid\sql\parser\SQLStatementParser.java
public SQLSelectParser createSQLSelectParser() { return new SQLSelectParser(this.exprParser); }

 设置子查询、order by、HINT节点的解析信息

druid\src\main\java\com\alibaba\druid\sql\parser\SQLSelectParser.java
public
SQLSelect select() { SQLSelect select = new SQLSelect(); withSubquery(select); select.setQuery(query()); select.setOrderBy(parseOrderBy()); if (select.getOrderBy() == null)   { select.setOrderBy(parseOrderBy()); } if (lexer.token() == Token.HINT)   { this.exprParser.parseHints(select.getHints()); } return select; }

 设置当前SELECT的父节点信息

druid\src\main\java\com\alibaba\druid\sql\ast\statement\SQLSelectStatement.java

public SQLSelectStatement(SQLSelect select)
{
        this.setSelect(select);
}

public void setSelect(SQLSelect select) { if (select != null) { select.setParent(this); } this.select = select; }

通过不断地对Tokens中的Token节点进行解析,并添加进statementList列表中。

一个SQLStatement就是对一条SQL语句的抽象,之前说过,SQL语言是一个结构化很严格的语言,所以在SQLStatement根节点下有很多子节点:

SQLSelectStatement、SQLUpdateStatement、SQLFromStatement...

 对不同的类别的SQL节点进行识别、解析、添加

druid\src\main\java\com\alibaba\druid\sql\parser\SQLStatementParser.java
public void parseStatementList(List<SQLStatement> statementList, int max) 
{
        for (;;) 
	{
            ..
            if (lexer.token() == Token.EOF) 
	    {
                return;
            }

            if (lexer.token() == (Token.SEMI)) 
	    {
                lexer.nextToken();
                continue;
            }

            if (lexer.token() == Token.SELECT) 
	    {
                statementList.add(parseSelect());
                continue;
            }

	    if (lexer.token() == (Token.UPDATE)) 
	    {
                statementList.add(parseUpdateStatement());
                continue;
            }

            if (lexer.token() == (Token.CREATE)) 
	    {
                statementList.add(parseCreate());
                continue;
            }

            if (lexer.token() == (Token.INSERT)) 
	    {
                SQLStatement insertStatement = parseInsert();
                statementList.add(insertStatement);

                continue;
            }

            if (lexer.token() == (Token.DELETE)) 
	    {
                statementList.add(parseDeleteStatement());
                continue;
            }
	    ..

假设我们的输入为:

select name,pwd from admin where id=1 and 1=1;

最后的解析结果:

statementList:
[SELECT name, pwd
FROM admin
WHERE id = 1
    AND 1 = 1]
lastToken: EOF

 

druid\src\main\java\com\alibaba\druid\wall\WallProvider.java
private
WallCheckResult checkInternal(String sql) { ... parser.parseStatementList(statementList); final Token lastToken = parser.getLexer().token(); ..

最终形成一个由不同层次的"节点"组成的AST语法树,即SQL字符串被解析成一个AST结构对象SQLStatement

3) 注入检测

AST语法树生成后,druid采用了"访问者设计模式",因为在druid的项目中,对象列表是相对不容易变动的,而访问方式(也就是SQL注入的检测规则)是相对容易不断变化的(因为我们防注入的规则是在不断变化的)

http://www.knowsky.com/370713.html

而我们之前说的规则就是在实现访问者的这些访问者对象中体现出来的,和mysql相关的主要有两个文件:

MysqlWalVisitor.java、WallVisitorUtils.java

我们要实现对druid的SQL注入检测规则优化,也就是从这些访问者中进行修改

创建访问者(用于遍历这个AST语法树之用),获取用户输入的第一条SQL语句,并调用"访问者"进行递归的遍历

druid\src\main\java\com\alibaba\druid\wall\WallProvider.java
private
WallCheckResult checkInternal(String sql) { ...
  WallVisitor visitor = createWallVisitor();

  if (statementList.size() > 0)
  {
            SQLStatement stmt = statementList.get(0);
            try
       {
                stmt.accept(visitor);
            }
       catch (ParserException e)
       {
                violations.add(new SyntaxErrorViolation(e, sql));
            }
        }

 接下涉及到的是"访问者"设计模式,根据面向对象的多态性,程序会自动根据当前SQLSTATEMENT节点的节点类型去调用相应的"访问者类"。

 之前说过,SQL注入检测的规则就是在实现访问者的这些访问者对象中体现出来的

1. 和mysql相关的主要有两个文件:

MysqlWalVisitor.java、WallVisitorUtils.java

2. 和sqlserver相关的有两个文件

SQLServerWallVisitor.java、WallVisitorUtils.java

所有的和Mysql有关的规则逻辑都写在这两个文件当中(MysqlWalVisitor.java、WallVisitorUtils.java),在遍历AST节点的同时应用检测规则。以对char()+..char()拼接绕过的检测为例

  druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java
  /* 这条规则表明如果SQL语句中出现了4次及4次以上的char、chr字符拼接,就判定出错。例如: select id, name from admin where 1=1 and '233'=char(32)+char(33)+char(33); //正常 select id, name from admin where 1=1 and 2332=char(32)+char(33)+char(33)+char(32); //攻击 */ public static boolean check(WallVisitor visitor, SQLBinaryOpExpr x) { if (x.getOperator() == SQLBinaryOperator.BooleanOr || x.getOperator() == SQLBinaryOperator.BooleanAnd) { List<SQLExpr> groupList = SQLUtils.split(x); for (SQLExpr item : groupList) { item.accept(visitor); } return false; } if (x.getOperator() == SQLBinaryOperator.Add || x.getOperator() == SQLBinaryOperator.Concat) { List<SQLExpr> groupList = SQLUtils.split(x); if (groupList.size() >= 4) { int chrCount = 0; for (int i = 0; i < groupList.size(); ++i) { SQLExpr item = groupList.get(i); if (item instanceof SQLMethodInvokeExpr) { SQLMethodInvokeExpr methodExpr = (SQLMethodInvokeExpr) item; String methodName = methodExpr.getMethodName().toLowerCase(); if ("chr".equals(methodName) || "char".equals(methodName)) { if (methodExpr.getParameters().get(0) instanceof SQLLiteralExpr) { chrCount++; } } } } if (chrCount >= 4) { addViolation(visitor, ErrorCode.EVIL_CONCAT, "evil concat", x); } } } return true; }

如果我们的规则匹配成功,即在用户输入的SQL语句中检测到了注入攻击的模式,则调用addViolation()添加检测结果信息。

druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java
private static void addViolation(WallVisitor visitor, int errorCode, String message, SQLObject x) 
{
        visitor.addViolation(new IllegalSQLObjectViolation(errorCode, message, visitor.toSQL(x)));
}

以上基本就是druid对一条SQL的解析过程,更多的规则细节和代码细节可以到开源项目的主页上去查看,我在第一篇学习笔记中有写到关于这个开源项目的相关知识
http://www.cnblogs.com/LittleHann/p/3505410.html
基于AST抽象语法树的SQL注入检测 (1) 

 

posted @ 2014-01-10 21:57  郑瀚Andrew  阅读(1182)  评论(2编辑  收藏  举报