前置知识

什么是Druid?

Druid是一个高效的数据查询系统,主要解决的是对于大量的基于时序的数据进行聚合查询。数据可以实时摄入,进入到Druid后立即可查,同时数据是几乎是不可变。通常是基于时序的事实事件,事实发生后进入Druid,外部系统就可以对该事实进行查询。

源码架构

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

java druid 在生成 SQL 的时候仍然保留驼峰命名 druid sql注入_sql

1.词法解析(lexer类)

druid\src\main\java\com\alibaba\druid\sql\parser\sqlparser.java

druid\src\main\java\com\alibaba\druid\sql\parser\lexer.java

druid\src\main\java\com\alibaba\druid\sql\parser\token.java

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"),
......

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

设置是否忽略注释,之后是通过大量的if判断对sql的token进行识别。

2.语法分析

语法解析器:在sql parser词法解析的基础上,对词法树(tokens)中的token节点进行语义识别(sql语义),将其解析成一个符合sql语法的规范化结构语法树。druid\src\main\java\com\alibaba\druid\sql\parser\sqlstatementparser.java

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;
}
... ...

因为SQL是结构化语言,所以通过递归遍历,以及又是一大段if判断,一层层解析出一个个子节点作为“特征单元”添加到语法树中,最后生成一个statementlist列表。
比如输入:

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

最后生成的statementlist,每个list元素被打上不同的标签做不同的检测:

select name, pwd
from admin
where id = 1
and 1 = 1
lasttoken: eof

最终字符串被解析为一个有不同的“特征单元”组成的多层次语法树。

3.注入检测

程序会自动根据当前sqlstatement节点的节点类型,判断数据库类型,再去调用相应的类。后文所说的sql注入检测的规则,就是在这些类中抽象出的对象里体现出来的。

如果我们的规则匹配成功,即在用户输入的sql语句中检测到了注入攻击的行为,则调用addviolation()添加检测结果信息,报错的同时写入日志。

private static void addviolation(wallvisitor visitor, int errorcode, string message, sqlobject x) 
{
        visitor.addviolation(new illegalsqlobjectviolation(errorcode, message, visitor.tosql(x)));
}

检测思路

1.检测规则

上文的语义分析部分已经解释了SQL语句的解析方法,下面就是Druid根据解析出来的“特征单元”和语法树,编写不同的检测逻辑,针对不同的数据库进行的不同注入方式进行的针对性防护。

例子如下:

druid\src\main\java\com\alibaba\druid\wall\spi\wallvisitorutils.java

  • 此处为针对char()+char()...+char()的绕过检测
  • 此条规则说明:只要sql内容中包含超过四条char或者chr,则告警
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();
            //判断调用了char()方法
            if ("chr".equals(methodName) || "char".equals(methodName)) {
                if (methodExpr.getArguments().get(0) instanceof SQLLiteralExpr) {
                    chrCount++;
                }
                /*
                * 此处为针对char()+char()...+char()的绕过检测
                * 此条规则说明:只要sql内容中包含超过四条char或者chr,则告警
                */
            }
                /*
                *对char函数内参数长度超过5的内容加白
                */
        } else if (item instanceof SQLCharExpr) {
            if (((SQLCharExpr) item).getText().length() > 5) {
                chrCount = 0;
                continue;
            }
        }
                
        if (chrCount >= 4) {
            addViolation(visitor, ErrorCode.EVIL_CONCAT, "evil concat", x);
            break;
        }
    }

检测逻辑

  1. 只允许基本的crud命令(增删改查)

druid\src\main\java\com\alibaba\druid\wall\WallConfig.java

noneBaseStatementAllow参数绝对了是否允许非基本语句的其他语句,缺省关闭,通过这个选项就能够屏蔽诸如CREATE、DROP、ALERT等可能存在严重危害的DDL语言。

java druid 在生成 SQL 的时候仍然保留驼峰命名 druid sql注入_java_02

默认值为false,是最严格的过滤格式,基本不可行,现在正常的企业业务几乎不存在完全屏蔽crud之外所有命令还能正常运行的,开启之后会严重损害SQL的灵活性。

2.禁止访问系统级表

出于权限控制的需要,Druid对于系统表的操作进行了详细的限制,给予用户充分的自定义空间。举例:

select * from information_schema.columns;

该操作不存在注入点,只是对系统表进行简单查询,所以是被允许的。
但是如果是:

select id  from admin  where id = 1  and 5 = 6  union  select concat(id, name, score) from (select column_name from information_schema.columns  where table_name = class1)

因为SQL在子语句中使用了union进行了concat拼接,拼接之后连接了系统表进行查询。Druid在sql parser解析后,判断information_schema在层次中的位置,如果它的父节点为SQL表达式(select等)、左节点为"from",就会满足子句拼接的条件,从而被认为具有攻击性。
判断拼接的Druid代码位于druid\src\main\java\com\alibaba\druid\wall\spi\wallvisitorutils.java

java druid 在生成 SQL 的时候仍然保留驼峰命名 druid sql注入_SQL_03

代码中的owner参数由配置文件确定,可以自行修改,以mysql为例,位于druid\src\main\resources\META-INF\druid\wall\mysql\deny-schema.txt

java druid 在生成 SQL 的时候仍然保留驼峰命名 druid sql注入_SQL_04

3.禁止访问系统变量

Druid同样也是通过配置策略的方式限制用户对于系统敏感变量的访问,代码与系统表的限制类似,正常的针对version、basedir的查询不会报错,但是:

select * from database where id='1' and len(@@version)>0 and '1'='1'

上文的语句中使用逻辑表达式,尝试探测版本信息。因为@@version的内容在where或having之后,所以会被禁止。判断代码Druid\src\main\java\com\alibaba\druid\wall\spi\wallvisitorutils.java
Druid使用黑名单限制了对敏感的系统变量的访问,具体内容直接被写在配置文件Druid\src\main\resources\META-INF\druid\wall\mysql\deny-variant.txt中:

java druid 在生成 SQL 的时候仍然保留驼峰命名 druid sql注入_java_05

4.禁止访问系统函数

和系统敏感的表、变量一样,Druid冶金用了诸如sleep等危险的系统函数的使用,最新的Druid在mysql中摒弃了黑名单的做法,采用白名单的方式限制函数的使用,其他数据库仍旧使用黑名单。

而且在判断使用危险系统函数的时候,和上文一样,Druid会判断敏感函数在sql语句中出现的位置:

select load_file('\\etc\\passwd');

不会被禁止,原因也是一样,不存在注入点。

select * from  ((select sleep(0))a);

会被禁止,因为显而易见的sleep函数出现在了可能存在注入点的位置(from的子节点)。
Druid\src\main\resources\META-INF\druid\wall\mysql\permit-function.txt

java druid 在生成 SQL 的时候仍然保留驼峰命名 druid sql注入_SQL_06

Druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java

5.禁止出现注释

通常的业务SQl语句不会带有注释,而在SQL注入中类似的行为却很常见,Druid默认模式下,会在SQL parser解析之前,先消除语句中的单行和多行注释内容。

诸如'//or//'1'='2等常见绕waf手段都是利用了SQL的快注释符。

删除注释,并重新拼接为“合规”的sql语句的代码,位于

Druid\src\main\java\com\alibaba\druid\sql\parser\lexer.java

public final void nexttoken()
{
  ... ...
/*
  解析'#'注释符
  判断'#'解析出的节点是'单行注释'、或'多行注释'
*/
case '#':
    scanSharp();
    if ((token == Token.LINE_COMMENT || token == Token.MULTI_LINE_COMMENT) && skipComment) {
        bufPos = 0;
        continue;
    }
    return;

以“#”为例,首先判断#号的注释符,然后判断如果是单行或者多行注释。
这是一种对业务低伤害的防护方式,因为业务人员如果是正常使用sql的注释功能,删除之后正常进入解析器,不会对语句正常执行造成任何影响,而如果是恶意的SQL注入行为,则会报错告警。

6.禁止同时执行多条SQL语句

Druid默认每次只允许执行一条SQL,一次执行多条会被认为疑似是恶意SQL注入语句。

7.禁止永真条件

利用永真条件判断是否存在注入点是sql注入攻击最常用的手段。Druid对where、order by和group by节点之后的两个及以上永真条件进行过滤。

因为单纯的永真语句普遍存在于业务代码中,比如

$sql = "select info from admin where ID = $id";

其中$id为可控输入,如果输入为1,在数据库层就会变成永真条件。因此Druid目前的规则允许语句子句之后最多只存在一个永真逻辑表达式。

where id =-1 or 1=1;

之类的都会被拦截。

private static Object getValue_and(WallVisitor visitor, List<SQLExpr> groupList) {
    int dalConst = 0;
    Boolean allTrue = Boolean.TRUE;
    for (int i = groupList.size() - 1; i >= 0; --i) {
        SQLExpr item = groupList.get(i);
        Object result = getValue(visitor, item);
        Boolean booleanVal = SQLEvalVisitorUtils.castToBoolean(result);
        if (Boolean.TRUE == booleanVal) {
            final WallConditionContext wallContext = WallVisitorUtils.getWallConditionContext();
            if (wallContext != null && !isFirst(item)) {
                wallContext.setPartAlwayTrue(true);
            }
            dalConst++;
        } else if (Boolean.FALSE == booleanVal) {
            final WallConditionContext wallContext = WallVisitorUtils.getWallConditionContext();
            if (wallContext != null && !isFirst(item)) {
                wallContext.setPartAlwayFalse(true);
            }
            allTrue = Boolean.FALSE;
            dalConst++;
        } else {
            if (allTrue != Boolean.FALSE) {
                allTrue = null;
            }
            dalConst = 0;
        }
        if (dalConst == 2 && visitor != null && !visitor.getConfig().isConditionDoubleConstAllow()) {
            addViolation(visitor, ErrorCode.DOUBLE_CONST_CONDITION, "double const condition", item);
        }
    }

8.禁止 getshell

into outfile是常用的利用注入点进行文件写入,从而getshell 的技术。


同样,druid的拦截是智能的,它只对真正的注入进行拦截,而正常的语句,例如:

记录每个用户的登录ip,写入文件中:

select "127.0.0.1" into outfile 'c:\index.php';   -- 允许

而攻击者常用的攻击语句(写入编码后的一句话)

select id from messages where id=?id=3 union select 1,0x3c3f706870206576616c28245f524551554553545b315d293b3f3e,3 into outfile 'C:\\Users\\Administrator.WIN2012\\Desktop\\phpStudy\\WWW\\outfile.php' --+

这个语句会被拦截下来

9.SQL盲注防御

盲注手法千千万万,也是防御模块最复杂的一部分,这里举几个例子来对防御方式进行说明:

0xa 盲注

  • order by
select * from cnp_news where id='23' order by if((len(@@version)>0),1,0);

利用盲注思想来进行注入,获取敏感信息

  • group by

select * from cnp_news where id='23' group by (select @@version);

利用数据库的错误信息报错来进行注入,获取敏感信息

  • 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);
    }
}