MyBatis的SQL注入

MyBatis支持两种参数符号,一种是#,另一种是$。
使用参数符号#的句子:

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

MyBatis会创建一个预编译语句,生成的代码类似于

// Similar JDBC code, NOT MyBatis…
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

参数会在SQL语句中用占位符”?”来标识,然后使用prepareStatement来预编译这个SQL语句\

另一种使用参数符号$时,MyBatis直接用字符串拼接把参数和SQL语句拼接在一起,然后执行。众所周知,这种情况非常危险,极容易产生SQL注入漏洞,例如:

<select id="getBlogById" resultType="Blog" parameterType=”int”>
         SELECT id,title,author,content
         FROM blog
WHERE id=${id}
</select>

CodeQL编写

了解完漏洞原理后就可以开始编写QL规则了

因为要进行全局污点分析,所以要先找到污染源Source,和污染汇聚点Sink

class MyTaintTrackingConfiguration extends TaintTracking2::Configuration {
    MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }
    override predicate isSource(DataFlow::Node source) {
        ...
    }
    override predicate isSink(DataFlow::Node sink) {
        ...
    }
}

获取Source

污染源Source一般来自请求中的数据,在Spring项目中主要表现在Controller方法中的参数,可以写一个谓词来获取这些方法

查找的思想是,找到类名包含Controller的类,然后获取类中的方法

class AllControllerMethod extends Method{  //获取所有Controller中的方法
    AllControllerMethod(){
        exists(
            RefType rt |
            rt.getName().indexOf("Controller")>0 and 
            this = rt.getACallable()
            )
    }
}

image-20240124144111587

现在只是获取到了方法,目的是要获取到参数,所以编写isSource如下

override predicate isSource(DataFlow::Node source) {
        source instanceof RemoteFlowSource or
        exists(Method m |
            m instanceof AllControllerMethod and
            source.asParameter() = m.getAParameter()  //将Controller的方法参数当作source
            )
    }

这里的Source还添加了RemoteFlowSource,防止遗漏 ,RemoteFlowSource类内置了主流的获取参数的方式,因此也可以使用这种方式获取source

获取Sink

该sql注入的特征是,在MyBatis MapperXML 文件,使用了${..}来处理数据,因此思路是找到xml文件中使用 ${..} 的字段对应的java

方法中的参数。如下图,第一个框中的id为该语句对应的方法名,第二个框是出现漏洞的地方

image-20240124150520929

第一步先找到所有的MapperXML文件

MapperXML只有一个名为mapper的子标签

image-20240124151516631

class MyBatisMapperXmlFile extends XmlFile { //获取MyBatis MapperXML 文件
    MyBatisMapperXmlFile(){
    count( XmlElement e|e = this.getAChild()) = 1 and  
    this.getAChild().getName()="mapper"
    }
}

第二步想办法获取到标签中的值

class MyBatisMapperXmlElement extends XmlElement {
    MyBatisMapperXmlElement(){
        this.getFile() instanceof MyBatisMapperXmlFile  //确保XmlElement是MapperXML文件下的
    }

    string getValue() { 
        result = this.getAChild*().allCharactersString().trim() //递归获取标签下的值(大量重复),父标签会包含子标签的值
    }

    string getId() { 
        result = this.getAttribute("id").getValue() //获取id属性的值,代表的是方法名
    }
}

getValue的效果如下:

image-20240124152159618

但是这里存在一个问题,父标签下获取值会把所有子标签下的值获取,而子标签获取值会再次获取其下面的值,这里出现大量重复,待改进

image-20240124152447438

getId是获取标签中属性id的值,为了得到该sql语句对应的是哪个方法

image-20240124152647857

第三步获取对应类中的方法

class MyBatisMapperVulMethod extends Method{  //在MapperXML文件中找到${}对应的方法
    MyBatisMapperVulMethod(){
        exists(MyBatisMapperXmlElement mmxe|
            mmxe.getValue().indexOf("${")>0 and //找到${}的标签
            this.hasName(mmxe.getId()) and //方法名为属性id
            this.getDeclaringType().getASubtype*().getQualifiedName() =  //递归获取父类名 是否和xml中namespace相等
            mmxe.getFile().getAChild().getAttribute("namespace").getValue() 
            )
    }
}

其中mmxe.getFile().getAChild().getAttribute("namespace").getValue() 是一个接口

image-20240124153717805

image-20240124153807580

this.getDeclaringType().getASubtype*().getQualifiedName()这个表示的是获取类型,然后递归查询父类并获取类名,如果类名和namespace所指的一样,说明其实现了namespace接口

查询效果如下:

image-20240124154420132

找到方法后,将方法的参数设置为Source,代码如下

override predicate isSink(DataFlow::Node sink) {   //sink点为使用了 ${...} 的XML文件对应的方法的方法调用中的任意一个参数
        exists(MethodCall call, MyBatisMapperXmlElement mmxe, string
            unsafeExpression |
            call.getMethod() instanceof MyBatisMapperVulMethod and
            //sink处方法调用的参数一定得是使用了${...}的
            unsafeExpression = mmxe.getValue().regexpFind("(\\$)\\{[^\\}]*\\}",
            _, _) and
            (sink.asExpr().toString() = unsafeExpression.substring(2,
            unsafeExpression.length()-1) or
            //形如criterion.condition这种参数,应以"."作为分隔符,分别判断
            sink.asExpr().toString() = unsafeExpression.substring(2,
            unsafeExpression.length()-1).splitAt(".")) and
            sink.asExpr() = call.getAnArgument()
            )
    }

这里需要注意的是形如criterion.condition的参数,这里的处理是将其分开分别为criterion,和condition

from MyBatisMapperXmlElement mmxe,string unsafeExpression,MethodCall call
where 
    call.getMethod() instanceof MyBatisMapperVulMethod and 
    unsafeExpression = mmxe.getValue().regexpFind("(\\$)\\{[^\\}]*\\}",_, _)
select 
    unsafeExpression,
    unsafeExpression.substring(2,  unsafeExpression.length()-1),
    unsafeExpression.substring(2,  unsafeExpression.length()-1).splitAt(".")

image-20240124161136529

完整代码

/**
 * @kind path-problem
 */
import java
import DataFlow2::PathGraph
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking2

class AllControllerMethod extends Method{  //获取所有Controller中的方法
    AllControllerMethod(){
        exists(
            RefType rt |
            rt.getName().indexOf("Controller")>0 and 
            this = rt.getACallable()
            )
    }
}

class MyBatisMapperXmlFile extends XmlFile { //获取MyBatis MapperXML 文件
    MyBatisMapperXmlFile(){
    count( XmlElement e|e = this.getAChild()) = 1 and
    this.getAChild().getName()="mapper"
    }
}

class MyBatisMapperXmlElement extends XmlElement {
    MyBatisMapperXmlElement(){
        this.getFile() instanceof MyBatisMapperXmlFile
    }

    string getValue() { 
        result = this.getAChild*().allCharactersString().trim() //递归获取元素中的值(大量重复),父标签会包含子标签的值
    }

    string getId() { 
        result = this.getAttribute("id").getValue() //获取id属性的值,代表的是方法名
    }
}

class MyBatisMapperVulMethod extends Method{  //在MapperXML文件中找到${}对应的方法
    MyBatisMapperVulMethod(){
        exists(MyBatisMapperXmlElement mmxe|
            mmxe.getValue().indexOf("${")>0 and
            this.hasName(mmxe.getId()) and 
            this.getDeclaringType().getASubtype*().getQualifiedName() =  //递归获取父类名 是否和xml中namespace相等
            mmxe.getFile().getAChild().getAttribute("namespace").getValue()
            )
    }
}

//数据流分析

class MyTaintTrackingConfiguration extends TaintTracking2::Configuration{
    MyTaintTrackingConfiguration(){
        this = "MyTaintTrackingConfiguration"
    }
    override predicate isSource(DataFlow::Node source) {
        source instanceof RemoteFlowSource or
        exists(Method m |
            m instanceof AllControllerMethod and
            source.asParameter() = m.getAParameter()  //将Controller的方法参数当作source
            )
    }

    override predicate isSink(DataFlow::Node sink) {   //sink点为使用了 ${...} 的XML文件对应的方法的方法调用中的任意一个参数
        exists(MethodCall call, MyBatisMapperXmlElement mmxe, string
            unsafeExpression |
            call.getMethod() instanceof MyBatisMapperVulMethod and
            //sink处方法调用的参数一定得是使用了${...}的
            unsafeExpression = mmxe.getValue().regexpFind("(\\$)\\{[^\\}]*\\}",
            _, _) and
            (sink.asExpr().toString() = unsafeExpression.substring(2,
            unsafeExpression.length()-1) or
            //形如criterion.condition这种参数,应以"."作为分隔符,分别判断
            sink.asExpr().toString() = unsafeExpression.substring(2,
            unsafeExpression.length()-1).splitAt(".")) and
            sink.asExpr() = call.getAnArgument()
            )
    }
}

from MyTaintTrackingConfiguration config, DataFlow2::PathNode source,DataFlow2::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(),source, sink, "sqlInject!"

运行结果

这个是华夏ERPv3.1的库跑出来的结果

image-20240124161643644

华夏ERPv2.3:

image-20240124161858665

官方也是给有sql注入查询的ql,在codeql-main\java\ql\src\experimental\Security\CWE\CWE-089\MyBatisMapperXmlSqlInjection.ql

运行对比了上面的结果,官方给出的结果比较多,但是重复的也多

华夏ERPv2.3:跑出102条结果,存在重复

image-20240124163520882

在华夏ERPv3.1跑出4条结果,但是两条是重复的

image-20240124163214688