前言 为什么学习CodeQL呢?在学习了一段代码审计,逐渐感觉代码审计是个体力活。而且越大的项目想要较全面的审计起来更是耗时间,还有可能漏掉一些很容易发现的漏洞。而CodeQL就是用来辅助漏洞挖掘,半自动化挖掘+人工辅助审计可大大减少人工成本,也提高了漏洞准确率。随着近几年网上公开的越来越多的严重级漏洞都是通过CodeQL挖掘出来的,所以目前对想学代码审计的人来说,学习CodeQL利大于弊,其目前也渐渐成为国内半自动化代码审计所使用的主流工具了。
安装及环境配置 CodeQL安装 CodeQL本身包含两部分解析引擎+SDK
。
解析引擎用来解析我们编写的规则,虽然不开源,但是我们可以直接在官网下载二进制文件直接使用。
SDK
完全开源,里面包含大部分现成的漏洞规则,我们也可以利用其编写自定义规则。
引擎安装 下载地址:https://github.com/github/codeql-cli-binaries/releases解压文件夹,复制codeql文件夹到CodeQL(新建的)文件夹下
配置环境变量指向codeql.exe目录即可
配置成功
SDK安装 移动到CodeQL目录下
1 git clone https://gi thub.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
1 codeql database create testdemo --language="java" --command="mvn clean install --file pom.xml" --source -root=D:/codeql/ testdemo/micro_service_seclab-main/
成功如下图所示
基础语法解释
1 2 3 4 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
1 2 3 from /* ... variable declarations ... */ where /* ... logical formula ... */ select /* ... expressions ... */
1 2 3 4 5 import java from int iwhere 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更好的理解一下
谓词 其实就是常说的”函数”
1 2 3 4 5 6 7 8 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
使用全局数据流库
1 2 3 4 5 6 7 8 9 10 11 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代码实现
1 2 3 4 5 import java from Method method , string c where method .getAnAnnotation () .toString () = c + "Mapping "select method .getName ()
这些方法的参数都可作为source,通过method.getParameter(n)获取Parameter
所以我们设置Source的代码为:
1 2 3 4 5 6 override predicate isSource(DataFlow::Node src ) { exists(Method method , string c ,int n | src.as Parameter() = method .getParameter(n ) and method .getAnAnnotation() .to String() = c + "Mapping " ) }
当然还有一种很更方便的获取方式,一行代码解决。那作者为啥要反其道而行呢?
①可以更好的理解分析AST语法树
②每种框架获取http请求参数不一,以下方法可能涵盖不到。当遇到很小众、很新的框架等原因,利用以下方式获取不到我们想要的参数该怎么办?就是利用以上最原始的方式分析语法树,代码自己的风格来获取。
③以下的方式不适合新手入门,可能理解不来。
1 override predicate isSource (DataFlow::Node src ) { src instanceof RemoteFlowSource }
设置sink 本案例中,SQL语句最终执行点为query方法调用(MethodAccess),且传入的参数是第一个。
所以我们设置Sink的代码为:
1 2 3 4 5 6 7 override predicate isSink(DataFlow::Node sink ) { exists(Method method , MethodAccess call | method .hasName("query" ) and call.getMethod() = method and sink.as Expr() = call.getArgument(0) ) }
意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink。
数据流 我们写好了source入口点和sink执行点,怎么实现一个污染参数通过头毫无拦截流到尾,成为可能真实存在的漏洞?这个连通工作就是CodeQL引擎本身来完成的。
代码样例:
1 2 3 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,系统就会自动帮我们判断是否存在漏洞了。
第一代成果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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.as Parameter() = method .getParameter(n ) and method .getAnAnnotation() .to String() = c + "Mapping " ) } override predicate isSink(DataFlow::Node sink ) { exists(Method method , MethodAccess call | method .hasName("query" ) and call.getMethod() = method and sink.as Expr() = 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注解式写法因为<img src=”https://latex.codecogs.com/gif.latex?造成的SQL注入,但是CodeQL如何发现它呢?误报往往可以通过人工进行排查,而漏报一但项目上线就有可能造成重大的经济损失等。所以我们要尽可能全方面涵盖进行挖掘。
老方法,依旧通过AST语法树来进行分析。
之前我们是通过定位query方法来发现漏洞,但是不够全面。现在大部分项目都是采用Mybatis注解方式,所以需要一起包含进去。
①通过定位接口名称以及接口注解来定位Mapper
②获得到相对应的Mapper后,通过注解字段Select,Update等来定位注解式语法
③通过参数名,获取参数别名
④将"/>{
+参数别名+}
在注解式子中进行匹配,发现漏洞
CodeQL代码实现:
1 2 3 4 5 6 7 import java from Interface i, string c, Method method ,string name, int n where i.getName() = c + "Mapper" and method .getAnAnnotation() .to String() = "Select" and method .getAnAnnotation() .getValue(name ) .to String() .indexOf("${" +method .getParameter (n ) .getAnAnnotation() .getValue(name ) .to String() .replaceAll("\"" , "" ) +"}" ) > 0 select method .getParameter(n ) .getAnAnnotation() .getValue(name ) .to String()
第二代成果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 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.as Parameter() = method .getParameter(n ) and method .getAnAnnotation() .to String() = 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() .to String() = "Mapper " and method .getAnAnnotation() .to String() in ["Select ", "Update ", "Insert ", "Delete "] and call.getMethod() = method and method .getAnAnnotation() .getValue(name ) .to String() .indexOf("${" +method .getParameter (n ) .getAnAnnotation() .getValue(name ) .to String() .replaceAll("\"" , "" ) +"}") > 0 and sink.as Expr() = call.getArgument(0) ) } predicate querySink(DataFlow::Node sink ) { exists(Method method , MethodAccess call | method .hasName("query" ) and call.getMethod() = method and sink.as Expr() = 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中添加以下代码,重新编译即可。(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <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的场景。
1 2 3 4 5 6 7 8 9 10 11 12 wget https://projectlombok.org/downloads/lombok.jar -O "lombok.jar" java -jar "lombok.jar" delombok -n --onlyChanged . -d "delombok" find "delombok" -name '*.java' -exec sed '/Generated by delombok/d' -i '{}' ';' find "delombok" -name '*.java' -exec sed '/import lombok/d' -i '{}' ';' cp -r "delombok/." "./" rm -rf "delombok"
误报排除
该方法的参数类型是List,不存在注入漏洞。所以我们需要用到上面所说的净化函数来进行阻断排除。
检测思路:如果当前Node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流。
ParameterizedType:泛型类型的参数化
CodeQL代码实现:
1 2 3 4 5 6 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 ) }
第三代成果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 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.as Parameter() = method .getParameter(n ) and method .getAnAnnotation() .to String() = 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() .to String() = "Mapper " and method .getAnAnnotation() .to String() in ["Select ", "Update ", "Insert ", "Delete "] and call.getMethod() = method and method .getAnAnnotation() .getValue(name ) .to String() .indexOf("${" +method .getParameter (n ) .getAnAnnotation() .getValue(name ) .to String() .replaceAll("\"" , "" ) +"}") > 0 and sink.as Expr() = call.getArgument(n ) ) } predicate querySink(DataFlow::Node sink ) { exists(Method method , MethodAccess call | method .hasName("query" ) and call.getMethod() = method and sink.as Expr() = 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注解式写法,官方也有提供相应的规则方便我们使用。
第四代成果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 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() = "to String " and ma.getQualifier() = node1.as Expr() and ma = node2.as Expr() ) } } predicate querySink(DataFlow::Node sink ) { exists(Method method , MethodAccess call | method .hasName("query" ) and call.getMethod() = method and sink.as Expr() = 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