利用Druid实现应用,SQL监控和防SQL注入

本文介绍了Druid作为一个JDBC组件的功能,包括监控数据库访问性能、替换DBCP和C3P0、数据库密码加密以及SQL执行日志。详细讲述了如何配置DruidDataSource,以及在Web.xml和Spring中的配置,以实现SQL注入防御。同时,文章还概述了Druid的SQL解析和注入检测机制。

一、关于Druid

Druid是一个JDBC组件,它包括三部分: 

  • DruidDriver 代理Driver,能够提供基于Filter-Chain模式的插件体系。 

  • DruidDataSource 高效可管理的数据库连接池。 

  • SQLParser 

Druid可以做什么? 

1) 可以监控数据库访问性能,Druid内置提供了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,这对于线上分析数据库访问性能有帮助。 

2) 替换DBCPC3P0。Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。 

3) 数据库密码加密。直接把数据库密码写在配置文件中,这是不好的行为,容易导致安全问题。DruidDruiver和DruidDataSource都支持PasswordCallback。 

4) SQL执行日志,Druid提供了不同的LogFilter,能够支持Common-LoggingLog4j和JdkLog,你可以按需要选择相应的LogFilter,监控你应用的数据库访问情况。 

扩展JDBC,如果你要对JDBC层有编程的需求,可以通过Druid提供的Filter-Chain机制,很方便编写JDBC层的扩展插件。

Druid开源中国社区地址:http://www.oschina.NET/p/druid

 

二、工程配置

1、工程引入druid-1.0.15.jar

2、数据源配置

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" lazy-init="false">
<property name="driverClassName" value="#{db.driver}" />
<property name="url" value="#{db.url}" />
<property name="username" value="#{db.user}" />
<property name="password" value="#{db.password}" />
<property name="initialSize" value="1" />
<property name="maxActive" value="50" />
<property name="maxIdle" value="0" />
<property name="maxWait" value="30000" />

<property name="filters" value="stat,wall" />
<property name="timeBetweenEvictionRunsMillis" value="3000" />  
<property name="minEvictableIdleTimeMillis" value="300000" />  
<property name="validationQuery" value="SELECT 'x'" />  
<property name="testWhileIdle" value="true" />  
<property name="testOnBorrow" value="false" />  
<property name="testOnReturn" value="false" />  
<property name="poolPreparedStatements" value="true" />  
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
</bean>

详细参数说明

 

配置缺省值说明
name 配置这个属性的意义在于,如果存在多个数据源,监控的时候
可以通过名字来区分开来。如果没有配置,将会生成一个名字,
格式是:”DataSource-” + System.identityHashCode(this)
jdbcUrl 连接数据库的url,不同数据库不一样。例如:
mysql : jdbc:mysql://10.20.153.104:3306/druid2 
oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 连接数据库的用户名
password 连接数据库的密码。如果你不希望密码直接写在配置文件中,
可以使用ConfigFilter。详细看这里:
https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter
driverClassName根据url自动识别这一项可配可不配,如果不配置druid会根据url自动识别dbType,
然后选择相应的driverClassName
initialSize0初始化时建立物理连接的个数。初始化发生在显示调用init方法,
或者第一次getConnection时
maxActive8最大连接池数量
maxIdle8已经不再使用,配置了也没效果
minIdle 最小连接池数量
maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,
缺省启用公平锁,并发效率会有所下降,
如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatementsfalse是否缓存preparedStatement,也就是PSCache。
PSCache对支持游标的数据库性能提升巨大,比如说oracle。
在mysql5.5以下的版本中没有PSCache功能,建议关闭掉。
5.5及以上版本有PSCache,建议开启。
maxOpenPreparedStatements-1要启用PSCache,必须配置大于0,当大于0时,
poolPreparedStatements自动触发修改为true。
在Druid中,不会存在Oracle下PSCache占用内存过多的问题,
可以把这个数值配置大一些,比如说100
validationQuery 用来检测连接是否有效的sql,要求是一个查询语句。
如果validationQuery为null,testOnBorrow、testOnReturn、
testWhileIdle都不会其作用。
testOnBorrowtrue申请连接时执行validationQuery检测连接是否有效,
做了这个配置会降低性能。
testOnReturnfalse归还连接时执行validationQuery检测连接是否有效,
做了这个配置会降低性能
testWhileIdlefalse建议配置为true,不影响性能,并且保证安全性。
申请连接的时候检测,如果空闲时间大于
timeBetweenEvictionRunsMillis,
执行validationQuery检测连接是否有效。
timeBetweenEvictionRunsMillis 有两个含义:
1) Destroy线程会检测连接的间隔时间
 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun 不再使用,一个DruidDataSource只支持一个EvictionRun
minEvictableIdleTimeMillis  
connectionInitSqls 物理连接初始化的时候执行的sql
exceptionSorter根据dbType自动识别当数据库抛出一些不可恢复的异常时,抛弃连接
filters 属性类型是字符串,通过别名的方式配置扩展插件,
常用的插件有:
监控统计用的filter:stat 
日志用的filter:log4j
 防御sql注入的filter:wall
proxyFilters 类型是List<com.alibaba.druid.filter.Filter>,
如果同时配置了filters和proxyFilters,
是组合关系,并非替换关系

 


3、Web.xml配置

<servlet>
      <servlet-name>DruidStatView</servlet-name>
      <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
    </servlet>
    <servlet-mapping>
      <servlet-name>DruidStatView</servlet-name>
      <url-pattern>/druid/*</url-pattern>
    </servlet-mapping>

至此基本配置完成,可以查看内置监控页面了,路径是/druid/index.html,

完整路径是http://127.0.0.1:8080/druid/index.html

如果还需要使用Web应用、URI监控、Session监控、spring监控等则还需要继续增加配置。

4、Wen应用配置

WebStatFilter用于采集web-jdbc关联监控的数据

在Web.xml中增加配置,就可以使用Web应用、URI监控、Session监控等功能

 

<filter>
    <filter-name>DruidWebStatFilter</filter-name>
    <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
    <init-param>
        <param-name>exclusions</param-name>
        <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
    </init-param>
    <init-param>
    	<param-name>profileEnable</param-name>
    	<param-value>true</param-value>
	</init-param>
	<init-param>
        <param-name>principalCookieName</param-name>
        <param-value>USER_COOKIE</param-value>
    </init-param>
    <init-param>
        <param-name>principalSessionName</param-name>
        <param-value>USER_SESSION</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>DruidWebStatFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

 

5、Spring监控配置

在Spring配置文件中增加

<bean id="druid-stat-interceptor" class="com.alibaba.druid.support.spring.stat.DruidStatInterceptor">
    </bean>
<bean id="druid-stat-pointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut" scope="prototype">
   <property name="patterns">
       <list>
           <value>com.company.bussines.service.*</value>
           <value>com.company.bussines.dao.*</value>
       </list>
   </property>
</bean>

<aop:config proxy-target-class="true">
   <aop:advisor advice-ref="druid-stat-interceptor" pointcut-ref="druid-stat-pointcut" />
</aop:config>

所有配置完成。

 

三. 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.makaidong.com

击者渠道的角度去理解,攻击者最终的目的是要获取信息 http: // www.马开东/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就会自动地对各种形式的注释符进

 

 

 

此文来自: 马开东博客 转载请注明出处 网址: http://www.makaidong.com

行删除,删除了注释后,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);
    }
}
 

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

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


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语句中出现了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.马开东/littlehann/p/3505410.html
基于ast抽象语法树的sql注入检测 (1)
 

6. 目前存在的问题需要改进

1. 在review日志的时候发现用户的业务sql中也出现了子句中的永真导致的误报,这块需要细化一下规则,减少误报
2. 目前druid对盲注的拦截能力还有待提高:
  2.1<span javascript"="" style="margin: 0px; padding: 0px; list-style: none; border: 0px; outline: 0px; background: transparent; max-width: 650px; height: auto;">var url = window.location.href;document.write("此文链接:"+url+"
 
");document.write("转载请注明出处: "+document.title+"");

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值