前言
为什么学习CodeQL呢?在学习了一段代码审计,逐渐感觉代码审计是个体力活。而且越大的项目想要较全面的审计起来更是耗时间,还有可能漏掉一些很容易发现的漏洞。而CodeQL就是用来辅助漏洞挖掘,半自动化挖掘+人工辅助审计可大大减少人工成本,也提高了漏洞准确率。随着近几年网上公开的越来越多的严重级漏洞都是通过CodeQL挖掘出来的,所以目前对想学代码审计的人来说,学习CodeQL利大于弊,其目前也渐渐成为国内半自动化代码审计所使用的主流工具了。
安装及环境配置
CodeQL安装
CodeQL本身包含两部分解析引擎+SDK
。
解析引擎用来解析我们编写的规则,虽然不开源,但是我们可以直接在官网下载二进制文件直接使用。
SDK
完全开源,里面包含大部分现成的漏洞规则,我们也可以利用其编写自定义规则。
引擎安装
下载地址:https://github.com/github/codeql-cli-binaries/releases解压文件夹,复制codeql文件夹到CodeQL(新建的)文件夹下
配置环境变量指向codeql.exe目录即可
配置成功
SDK安装
移动到CodeQL目录下
git clone https://github.com/Semmle/ql
安装成功后CodeQL目录下就有两个文件夹codeql和ql
CodeQL插件安装
在官网下载并安装 Visual Studio Code,并安装CodeQL插件
配置引擎路径
到此就完全配置好了CodeQL开发环境了
CodeQL测试
靶场环境:https://github.com/l4yn3/micro_service_seclab/(其他也可)
先测试靶场环境是否可以正常调试,先测试下”Hello World”
由于CodeQL
的处理对象并不是源码本身,而是中间生成的AST结构数据库,所以我们先需要把我们的项目源码转换成CodeQL
能够识别的CodeDatabase
codeql database create testdemo --language="java" --command="mvn clean install --file pom.xml" --source-root=D:/codeql/testdemo/micro_service_seclab-main/
成功如下图所示
基础语法解释
database create testdemo 指我们要创建的database为testdemo(注:要先创建databases目录)
--language="java" 表示当前程序语言为Java
--command="mvn clean install --file pom.xml" 编译命令(因为Java是编译语言,所以需要使用–command命令先对项目进行编译,再进行转换,python和php这样的脚本语言不需要此命令)
--source-root=D:/codeql/testdemo/micro_service_seclab-main/ 指的是项目路径
导入database,选择testdemo文件夹
导入成功
编写查询打开刚才下载的SDK,在ql一一>java一一>ql一一>examples目录下创建demo.ql
编写好查询语句,右击执行Run Query
出现如下右侧结果说明调试成功
CodeQL语法
参考文档:https://codeql.github.com/docs
因为CodeQL是识别不了源码本身的,而是通过CodeQL引擎把源码转换成CodeQL可识别的AST结构数据库,所以想要真正理解CodeQL原理,要学会看懂分析AST抽象语法树。
选中某个源码文件
点击View AST
通过语法树来学习CodeQL语法,可以更好的理解接下来怎么编写规则,为啥这么编写规则。
当然了,ql自身也提供了许多每种语言的Demo在snippets目录下供我们学习参考
接下看开始真正的CodeQL学习之旅!!!
先看一个小Demo
from /* ... variable declarations ... */
where /* ... logical formula ... */
select /* ... expressions ... */
import java
from int i
where i = 1
select i
第一行表示我们要引入CodeQL的类库,因为我们分析的项目是java的,所以在ql语句里,必不可少。
from int i
, 定义一个变量i,它的类型是int,获取所有的int类型的数据
where i = 1
, 当i等于1的时候,符合条件
select i
, 输出i
总结:获取项目中所有整形变量,当变量的值等于1时,输出这个变量。
类库
ql中我们常用到的类库
名称 | 解释 |
Method | 方法类,Method method表示获取当前项目中所有的方法 |
MethodAccess | 方法调用类,MethodAccess call表示获取当前项目当中的所有被调用的方法 |
Parameter | 参数类,Parameter表示获取当前项目当中所有的参数 |
怎么理解呢?通过AST语法树来理解。
Method method表示获取当前项目中所有的方法
MethodAccess call表示获取当前项目当中的所有被调用的方法
Parameter表示获取当前项目当中所有的参数
通过demo更好的理解一下
谓词
其实就是常说的”函数”
predicate isSmall(int i) {
i in [1 .. 9]
}
int getSuccessor(int i) {
result = i + 1 and
i in [1 .. 9]
}
predicate 表示当前方法没有返回值。
result是CodeQL引入的特殊变量,代表返回的变量
重点
如何进行全局污点追踪呢?
通过继承类DataFlow::Configuration
使用全局数据流库
class SqlInjectionConfiguration extends DataFlow::Configuration {
MyDataFlowConfiguration() { this = "SqlInjectionConfiguration" }
override predicate isSource(DataFlow::Node source) {
...
}
override predicate isSink(DataFlow::Node sink) {
...
}
}
下面是关于DataFlow::Configuration
谓词的介绍
isSource
-定义数据可能来源(输入点)。比如获取http请求的参数部分,就是非常明显的source。
isSink
-定义数据可能流向的位置(执行点)。比如SQL注入漏洞,最终执行SQL语句的函数就是sink(这个函数可能叫query或者exeSql,或者其它)
isSanitizer
—可选,限制数据流(净化点),代表污点传播到这里就会被阻断。比如SQL注入漏洞,虽然source经过多个Node可到达sink,但是数据类型是Int类型又或者其他原因使得漏洞不存在,则可以通过重写净化函数进行阻断,降低漏洞误报率。
isAdditionalFlowStep
—可选,添加额外的污点步骤。比如SQL注入Node1到Node2过程,由于QL本身规则没识别出该漏洞,而我们人工审核出是存在漏洞的,则可重写规则强制把Node1与Node2拼接起来,降低漏洞漏报率。
exists公式讲解
这里有必要讲下exists
公式,引入一些临时的变量。如果变量可以采用至少一组值来使正文中的公式为真,则它成立。
exists子查询,是CodeQL谓词语法里非常常见的语法结构,它根据内部的子查询返回true or false,来决定筛选出哪些数据。
获取source
靶场环境使用的是Spring Boot
框架,可以根据spring控制器的特点,注解中都存在XXXMapping来进行路径映射。怎么获取到注解呢?通过AST语法树分析来获取。
CodeQL代码实现
import java
from Method method, string c
where method.getAnAnnotation().toString() = c + "Mapping"
select method.getName()
这些方法的参数都可作为source,通过method.getParameter(n)获取Parameter
所以我们设置Source的代码为:
override predicate isSource(DataFlow::Node src) {
exists(Method method, string c ,int n |
src.asParameter() = method.getParameter(n)
and method.getAnAnnotation().toString() = c + "Mapping"
)
}
当然还有一种很更方便的获取方式,一行代码解决。那作者为啥要反其道而行呢?
①可以更好的理解分析AST语法树
②每种框架获取http请求参数不一,以下方法可能涵盖不到。当遇到很小众、很新的框架等原因,利用以下方式获取不到我们想要的参数该怎么办?就是利用以上最原始的方式分析语法树,代码自己的风格来获取。
③以下的方式不适合新手入门,可能理解不来。
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }
设置sink
本案例中,SQL语句最终执行点为query方法调用(MethodAccess),且传入的参数是第一个。
所以我们设置Sink的代码为:
override predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
method.hasName("query")
and call.getMethod() = method
and sink.asExpr() = call.getArgument(0)
)
}
意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink。
数据流
我们写好了source入口点和sink执行点,怎么实现一个污染参数通过头毫无拦截流到尾,成为可能真实存在的漏洞?这个连通工作就是CodeQL引擎本身来完成的。
代码样例:
from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"
我们传递给config.hasFlowPath(source, sink)
我们定义好的source和sink,系统就会自动帮我们判断是否存在漏洞了。
第一代成果
/**
* @id java/examples/sqldemo
* @name Sql-Injection
* @description Sql-Injection
* @kind path-problem
* @problem.severity warning
*/
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph
class SqlInjectionConfig extends TaintTracking::Configuration {
SqlInjectionConfig() {
this = "SqlInjectionConfig"
}
override predicate isSource(DataFlow::Node src) {
exists(Method method, string c ,int n |
src.asParameter() = method.getParameter(n)
and method.getAnAnnotation().toString() = c + "Mapping"
)
}
override predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
method.hasName("query")
and call.getMethod() = method
and sink.asExpr() = call.getArgument(0)
)
}
}
from SqlInjectionConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"
成功挖掘出SQL注入漏洞
注:上面的注释不能够删除,它是程序的一部分,因为在我们生成测试报告的时候,上面注释当中的name,description等信息会写入到审计报告中Metadata for CodeQL queries — CodeQL (github.com)
漏报排除
人工审核很容易发现这是MyBatis注解式写法因为$造成的SQL注入,但是CodeQL如何发现它呢?误报往往可以通过人工进行排查,而漏报一但项目上线就有可能造成重大的经济损失等。所以我们要尽可能全方面涵盖进行挖掘。
老方法,依旧通过AST语法树来进行分析。
之前我们是通过定位query方法来发现漏洞,但是不够全面。现在大部分项目都是采用Mybatis注解方式,所以需要一起包含进去。
①通过定位接口名称以及接口注解来定位Mapper
②获得到相对应的Mapper后,通过注解字段Select,Update等来定位注解式语法
③通过参数名,获取参数别名
④将${
+参数别名+}
在注解式子中进行匹配,发现漏洞
CodeQL代码实现:
import java
from Interface i, string c, Method method,string name, int n
where i.getName() = c + "Mapper" and method.getAnAnnotation().toString() = "Select" and method.getAnAnnotation().getValue(name).toString().indexOf("${"+method.getParameter(n).getAnAnnotation().getValue(name).toString().replaceAll(""", "")+"}") > 0
select method.getParameter(n).getAnAnnotation().getValue(name).toString()
第二代成果
/**
* @id java/examples/sqldemo
* @name Sql-Injection
* @description Sql-Injection
* @kind path-problem
* @problem.severity warning
*/
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph
class SqlInjectionConfig extends TaintTracking::Configuration {
SqlInjectionConfig() {
this = "SqlInjectionConfig"
}
override predicate isSource(DataFlow::Node src) {
exists(Method method, string c ,int n |
src.asParameter() = method.getParameter(n)
and method.getAnAnnotation().toString() = c + "Mapping"
)
}
override predicate isSink(DataFlow::Node sink) {
exists(|myBatisSink(sink) or querySink(sink))
}
}
predicate myBatisSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call, Interface interface, string name, int n |
interface.getAnAnnotation().toString() = "Mapper"
and method.getAnAnnotation().toString() in ["Select", "Update", "Insert", "Delete"]
and call.getMethod() = method
and method.getAnAnnotation().getValue(name).toString().indexOf("${"+method.getParameter(n).getAnAnnotation().getValue(name).toString().replaceAll(""", "")+"}") > 0
and sink.asExpr() = call.getArgument(0)
)
}
predicate querySink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
method.hasName("query")
and call.getMethod() = method
and sink.asExpr() = call.getArgument(0)
)
}
from SqlInjectionConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"
成功挖掘出MyBatis注解类型SQL注入漏洞。当然靶场中还存在其他各种各样的漏报,比如Lombok问题。
Lombok编写的项目,CodeQL在对项目解析时,会对CodeQL分析器造成干扰,导致所构建的数据库中少了很多源码。导致CodeQL分析不到Lombok带来的SQL注入问题。
解决方法:
①使用maven-delombok,在pom.xml中添加以下代码,重新编译即可。(推荐)
<build>
<sourceDirectory>${project.basedir}/src/main/lombok</sourceDirectory>
<plugins>
<plugin>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>1.18.20.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>delombok</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<addOutputDirectory>false</addOutputDirectory>
<sourceDirectory>src/main/java</sourceDirectory>
<outputDirectory>${project.basedir}/src/main/lombok</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
成功还原,并挖掘出Lombok插件带来的SQL注入风险。
②官方也给出了解决方法https://github.com/github/codeql/issues/4984,这种还原方式有可能会出现未定义Object的场景。
# get a copy of lombok.jar
wget https://projectlombok.org/downloads/lombok.jar -O "lombok.jar"
# run "delombok" on the source files and write the generated files to a folder named "delombok"
java -jar "lombok.jar" delombok -n --onlyChanged . -d "delombok"
# remove "generated by" comments
find "delombok" -name '*.java' -exec sed '/Generated by delombok/d' -i '{}' ';'
# remove any left-over import statements
find "delombok" -name '*.java' -exec sed '/import lombok/d' -i '{}' ';'
# copy delombok'd files over the original ones
cp -r "delombok/." "./"
# remove the "delombok" folder
rm -rf "delombok"
误报排除
该方法的参数类型是List
检测思路:如果当前Node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流。
ParameterizedType:泛型类型的参数化
CodeQL代码实现:
override predicate isSanitizer(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or
node.getType() instanceof BoxedType or
node.getType() instanceof NumberType or
exists(ParameterizedType pt| node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType )
}
第三代成果
/**
* @id java/examples/sqldemo
* @name Sql-Injection
* @description Sql-Injection
* @kind path-problem
* @problem.severity warning
*/
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph
class SqlInjectionConfig extends TaintTracking::Configuration {
SqlInjectionConfig() {
this = "SqlInjectionConfig"
}
override predicate isSource(DataFlow::Node src) {
exists(Method method, string c ,int n |
src.asParameter() = method.getParameter(n)
and method.getAnAnnotation().toString() = c + "Mapping"
)
}
override predicate isSink(DataFlow::Node sink) {
exists(|myBatisSink(sink) or querySink(sink))
}
override predicate isSanitizer(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or
node.getType() instanceof BoxedType or
node.getType() instanceof NumberType or
exists(ParameterizedType pt| node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType )
}
}
predicate myBatisSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call, Interface interface, string name, int n |
interface.getAnAnnotation().toString() = "Mapper"
and method.getAnAnnotation().toString() in ["Select", "Update", "Insert", "Delete"]
and call.getMethod() = method
and method.getAnAnnotation().getValue(name).toString().indexOf("${"+method.getParameter(n).getAnAnnotation().getValue(name).toString().replaceAll(""", "")+"}") > 0
and sink.asExpr() = call.getArgument(n)
)
}
predicate querySink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
method.hasName("query")
and call.getMethod() = method
and sink.asExpr() = call.getArgument(0)
)
}
from SqlInjectionConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"
成功排除,当然有时候还有其他因素,比如开发写的过滤函数,白名单检测等排除。需要大家自己开动脑筋!
MyBatis-XML问题
这个问题CodeQL官网其实有提供相应特殊规则MyBatisMapperXmlSqlInjection.ql,该ql文件所做的事情是扫描Mapper配置Mybatis XML的${}的SQL注入。当然了包括上面所说的MyBatis注解式写法,官方也有提供相应的规则方便我们使用。
第四代成果
/**
* @id java/examples/SqlInject
* @name Sql-Injection
* @description Sql-Injection
* @kind path-problem
* @problem.severity error
*/
import java
import semmle.code.java.dataflow.FlowSources
import MyBatisMapperXmlSqlInjectionLib
import MyBatisCommonLib
import MyBatisAnnotationSqlInjectionLib
import DataFlow::PathGraph
class SqlInjectionConfiguration extends TaintTracking::Configuration {
SqlInjectionConfiguration() {
this = "SqlInjectionConfiguration"
}
override predicate isSource(DataFlow::Node src) {
src instanceof RemoteFlowSource
}
override predicate isSink(DataFlow::Node sink) {
exists(|
sink instanceof MyBatisAnnotatedMethodCallArgument or
sink instanceof MyBatisMapperMethodCallAnArgument or
querySink(sink)
)
}
override predicate isSanitizer(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or
node.getType() instanceof BoxedType or
node.getType() instanceof NumberType or
exists(ParameterizedType pt| node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType )
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(MethodAccess ma |
ma.getMethod().getDeclaringType() instanceof TypeObject and
ma.getMethod().getName() = "toString" and
ma.getQualifier() = node1.asExpr() and
ma = node2.asExpr()
)
}
}
predicate querySink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
method.hasName("query")
and call.getMethod() = method
and sink.asExpr() = call.getAnArgument()
)
}
from SqlInjectionConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"
成功扫出MyBatis注解式和MyBatis-XML存在的SQL注入问题
总结
相信通过上面的学习,大家也都发现了所有的分析都是离不开AST语法树的。因为CodeQL自身只能识别AST语法树,所以要进行任何的分析都需要围绕AST语法树来进行分析,也可以更好的理解QL语法原理,对自己的分析大有益处。当作者自己第一次看别人的分析文章看着CodeQL代码半天一筹莫展,丝毫没理解懂。当知道QL的核心就是AST语法树,再结合着语法树进行分析就很好理解了。也会同时有更多自己不一样的分析思路。
当然了,通过CodeQL不可能挖掘出所有的漏洞。而且不同的源码需要不同的规则进行匹配。所以需要大家自己探索不断完善自己的规则,它的好处就是可以大大减少人工成本,在较通用的漏洞上帮助大家更好的发现。当然网上也有很多非常隐蔽的漏洞也是通过CodeQL进行挖掘的,这就需要大家自己独特的挖掘规则思路。
参考文章
https://www.freebuf.com/articles/web/283795.html
原文始发于微信公众号(JDArmy):codeql-sql篇