CodeQL挖掘MyBatis框架的SQL注入
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()
)
}
}
现在只是获取到了方法,目的是要获取到参数,所以编写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
为该语句对应的方法名,第二个框是出现漏洞的地方
第一步先找到所有的MapperXML文件
MapperXML只有一个名为mapper
的子标签
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的效果如下:
但是这里存在一个问题,父标签下获取值会把所有子标签下的值获取,而子标签获取值会再次获取其下面的值,这里出现大量重复,待改进
getId是获取标签中属性id
的值,为了得到该sql语句对应的是哪个方法
第三步获取对应类中的方法
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()
是一个接口
this.getDeclaringType().getASubtype*().getQualifiedName()
这个表示的是获取类型,然后递归查询父类并获取类名,如果类名和namespace
所指的一样,说明其实现了namespace
接口
查询效果如下:
找到方法后,将方法的参数设置为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(".")
完整代码
/**
* @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的库跑出来的结果
华夏ERPv2.3:
官方也是给有sql注入查询的ql,在codeql-main\java\ql\src\experimental\Security\CWE\CWE-089\MyBatisMapperXmlSqlInjection.ql
运行对比了上面的结果,官方给出的结果比较多,但是重复的也多
华夏ERPv2.3:跑出102条结果,存在重复
在华夏ERPv3.1跑出4条结果,但是两条是重复的