CodeQL学习笔记

前言

​ 在挖了一段时间的漏洞后,逐渐感觉挖洞变成了一个体力活,虽然也使用正则匹配的方式减少了部分工作量,但这种方式还是有很大的缺陷,准确率比较低,因此希望找到一种新的方式来辅助挖洞,最近CodeQL比较火,很多师傅也写了相应的文章,相对来说学习成本已经算比较低了。尽管看了很多师傅的文章,但感觉上自己对原理或者语法的学习还是比较迟钝,因此打算去分析师傅们已经写好的一些query语法,帮助自己理解。

java-sec-code

​ 第一个Demo来自文章Codeql 入门,师傅以java-sec-code项目为例编写了多个query语句。

查询所有内容为空的方法

1
2
3
4
5
6
7
import java

from Method m, BlockStmt block
where
block = m.getBody() and
block.getNumStmt() = 0
select m

from语句为变量定义,where语句相当于数据库查询中的搜索条件的限制语句,select为查询语句。在QL中,方法称作谓词

Method类型是方法类,表示获取当前项目中所有的方法。getBody谓词返回body体,BlockStmt代表一个语句块。getNumStmt谓词获取块child statements的数量。关于BlockStmt这部分应该是和AST有一些关系。

Local Data Flow分析SPEL

1
2
3
4
5
6
7
8
9
10
11
import java
import semmle.code.java.frameworks.spring.SpringController
import semmle.code.java.dataflow.TaintTracking

from Call call,Callable parseExpression,SpringRequestMappingMethod route
where
call.getCallee() = parseExpression and
parseExpression.getDeclaringType().hasQualifiedName("org.springframework.expression", "ExpressionParser") and
parseExpression.hasName("parseExpression") and
TaintTracking::localTaint(DataFlow::parameterNode(route.getARequestParameter()),DataFlow::exprNode(call.getArgument(0)))
select route.getARequestParameter(),call

本地数据流

​ 本地数据流是单个方法可调用对象中的数据流。本地数据流通常比全局数据流更容易、更快、更精确。

​ 本地数据流库位于模块 DataFlow 中,该模块定义了表示数据可以流经的任何元素的类 Node。节点分为表达式节点(ExprNode)和参数节点(ParameterNode)。您可以使用成员谓词 asExpr 和 asParameter 在数据流节点和表达式/参数之间进行映射或使用谓词 exprNode 和 parameterNode。

​ 如果存在从节点nodeFrom到节点nodeTo的即时数据流边,则谓词localFlowStep(Node nodeFrom, Node nodeTo)成立。您可以通过使用+和运算符*,或者通过使用定义的递归谓词localFlow(相当于localFlowStep)来递归应用该谓词。

​ 例如,可以在零个或多个本地步骤中找到从参数源到表达式接收器的污点传 播:DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))

本地污点跟踪

​ 本地污点跟踪通过包括非保留值步骤来扩展本地数据流。例如:

1
2
String temp = x;
String y = temp + ", " + temp;

​ 如果x是一个污点字符串,那么y也是污点。

​ 本地污染跟踪库位于TaintTracking模块中。像本地数据流一样,如果从nodeFrom 节点到nodeTo节点之间存在直接的污点传播边线,则谓词localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo)成立。您可以使用+和``*运算符,也可以使用预定义的递归谓词localTaint(等效于localTaintStep*)来递归地应用谓词。

​ 所以我们再来看下面的代码,是不是就可以理解了。即使用本地污点跟踪的方式查询从参数节点route.getARequestParameter()到表达式节点call.getArgument(0)的数据流是否成立。

1
TaintTracking::localTaint(DataFlow::parameterNode(route.getARequestParameter()),DataFlow::exprNode(call.getArgument(0))) 

Call和Callable

Callable表示可调用的方法或构造器的集合。

Call表示调用Callable的这个过程(方法调用,构造器调用等等)

​ 那么call.getCallee() = parseExpression 就代表获取方法调用为parseExpression

​ 再通过下面的语句对parseExpression进行限制,也就是org.springframework.expression包下的ExpressionParser类的parseExpression方法,我们可以记住这个语句,用到的时候直接套也可以。

1
2
parseExpression.getDeclaringType().hasQualifiedName("org.springframework.expression", "ExpressionParser") and
parseExpression.hasName("parseExpression")

SpringRequestMappingMethod

SpringRequestMappingMethod可以获取所有的Spring Controller的方法。

getARequestParameter 获取请求的参数

getArgument 获取方法调用时的参数

​ 所以再来看下面的代码,意思就是获取所有RequestMapping方法的参数到调用parseExpression方法第一个参数的数据流。

1
TaintTracking::localTaint(DataFlow::parameterNode(route.getARequestParameter()),DataFlow::exprNode(call.getArgument(0)))

照猫画虎

​ 理解了上面代码的意思,我们完全可以照猫画虎,追踪所有Controller中的命令执行。

1
2
3
4
5
6
7
8
9
10
11
import java
import semmle.code.java.frameworks.spring.SpringController
import semmle.code.java.dataflow.TaintTracking

from Call call,Callable parseExpression,SpringRequestMappingMethod route
where
call.getCallee() = parseExpression and
parseExpression.getDeclaringType().hasQualifiedName("java.lang", "Runtime") and
parseExpression.hasName("exec") and
TaintTracking::localTaint(DataFlow::parameterNode(route.getARequestParameter()),DataFlow::exprNode(call.getArgument(0)))
select route.getARequestParameter(),call

全局数据流

​ 本地数据流虽然分析效率比较高,但是会存在一些遗漏,举个栗子。我想分析SSRF漏洞,假如我找到的Sink是new URL("xx"),但是在下面的Controller中并没有直接调用,而是调用了HttpUtils.URLConnection(url);。而在URLConnection创建了URL对象,那么我使用本地数据流分析是分析不到的,因为他只能在单个方法中分析,跨方法的调用就不行了,这个时候就需要全局数据流。

image-20211117160226654

可以通过继承类DataFlow::Configuration来使用全局数据流库。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import semmle.code.java.dataflow.DataFlow

class MyDataFlowConfiguration extends DataFlow::Configuration {
MyDataFlowConfiguration() { this = "MyDataFlowConfiguration" }

override predicate isSource(DataFlow::Node source) {
...
}

override predicate isSink(DataFlow::Node sink) {
...
}
}

​ 可能对QL中的Class比较陌生,Class简单介绍如下。

image-20211117160956245

​ 所以上例中的isSourceisSink都是父类DataFlow::Configuration的非私有谓词。predicate 代表当前的谓词没有返回值。下面是关于DataFlow::Configuration谓词的介绍。

isSource-定义数据可能来源

isSink-定义数据可能流向的位置

isBarrier—可选,限制数据流

isAdditionalFlowStep—可选,添加额外的数据流步骤

​ 这里的Source代表输入点,Sink代表执行点,isAdditionalFlowStep它的作用是将一个可控节点A强制传递给另外一个节点B,那么节点B也就成了可控节点。

全局污点追踪

​ 全局污点跟踪是针对全局数据流而言,就像本地污点跟踪是针对本地数据流一样。也就是说,全局污点跟踪通过额外的non-value-preserving步骤扩展了全局数据流。我们可以通过扩展类TaintTracking::Configuration来使用全局污点跟踪库:

1
2
3
4
5
6
7
8
9
10
11
12
13
import semmle.code.java.dataflow.TaintTracking

class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }

override predicate isSource(DataFlow::Node source) {
...
}

override predicate isSink(DataFlow::Node sink) {
...
}
}

isSource-定义污点的可能来源

isSink-定义污点可能流向的位置  

isSanitizer—可选,限制污点流

isAdditionalTaintStep—可选,添加额外污点步骤

​ 这里解释下isSanitizer也就是净化函数,代表污点传播到这里就会被阻断。

​ 下面我们用全局污点追踪分析SSRF漏洞,就可以分析到HttpUtils.URLConnection中的URL请求了。

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
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.frameworks.spring.SpringController
import semmle.code.java.dataflow.TaintTracking

class Configuration extends DataFlow::Configuration {
Configuration() {
this = "Configer"
}

override predicate isSource(DataFlow::Node source) {
exists( SpringRequestMappingMethod route| source.asParameter()=route.getARequestParameter() )
}

override predicate isSink(DataFlow::Node sink) {
exists(Call call ,Callable parseExpression|
sink.asExpr() = call.getArgument(0) and
call.getCallee()=parseExpression and
parseExpression.getDeclaringType().hasQualifiedName("java.net", "URL") and
parseExpression.hasName("URL")
)
}
}

from DataFlow::Node src, DataFlow::Node sink, Configuration config
where config.hasFlow(src, sink)
select src,sink

​ 这里有必要讲下exists语句

它根据内部的子查询返回true or false,来决定筛选出哪些数据。格式为exists(Obj obj| somthing)

Shiro反序列化漏洞

​ 之前有师傅也讲了如何使用CodeQL分析Shiro的反序列化漏洞,我们也学习一下思路,首先根据之前我们对Shiro反序列化漏洞的了解,这个洞还是稍微有点复杂的,所以肯定是要使用全局污点追踪的方法分析的。

数据库构建

​ 首先从github下载shiro的源码并且切换到1.2.4版本。

1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout 9549384

​ 构建数据库

1
CodeQL database create shiro1.2.4 --language=java --overwrite --command="mvn clean install -Dmaven.test.skip=true-Dmaven.test.skip=true"

​ 直接构建会有一个错误。

image-20211118100254113

​ 经过查找资料,主要是aspectj依赖包的问题。

aspectjweaver 1.8.9之前的版本不支持JDK1.8, aspectjweaver 1.8.9是在使用JDK1.8时的最低版本。

   所以对于此有两种方法进行解决:
        
   一: 降低JDK的版本,如果aspectjweaver的版本是1.8.9之前的,那么可以使用JDK1.7
        
   二:升级aspectjweaver的版本, 如果aspectjweaver的版本是1.8.9之前的,那么可以使用1.8.9来解决这个问题。

​ 虽然说是这么说,但是我改了pom.xml里的版本后并没有解决问题,切换JDK版本为1.7可以顺利构建成功。

​ 导入数据库后就可以通过下面的查询语句分析啦

代码分析

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
61
62
63
64
65
66
67
68
69
70
71
72
73
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph

predicate isCookiegetValue(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="Cookie" and
expDest=call and
call.getMethod() = method and
method.hasName("getValue") and
method.getDeclaringType().toString() = "Cookie"
)
}

predicate isReadObject(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="ObjectInputStream" and
expDest=call and
call.getMethod() = method and
method.hasName("readObject") and
method.getDeclaringType().toString() = "ObjectInputStream"
)
}

predicate isBase64(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="String" and
expDest=call and
call.getMethod() = method and
method.hasName("decode") and
method.getDeclaringType().toString() = "Base64"
)
}

predicate isdecrypt(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="byte" and
expDest=call and
call.getArgument(0)=expSrc and
call.getMethod() = method and
method.hasName("decrypt") and
method.getDeclaringType().toString() = "CipherService"
)
}

class VulConfig extends TaintTracking::Configuration {
VulConfig() { this = "shiroConfig" }

override predicate isSource(DataFlow::Node source) {
exists(MethodAccess call |
call.getMethod().getName()="getCookies" and
source.asExpr()=call
)
}

override predicate isSink(DataFlow::Node sink) {
exists(MethodAccess call |
call.getMethod().getName()="readObject" and
sink.asExpr()=call
)
}

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
isCookiegetValue(node1.asExpr(), node2.asExpr()) or
isReadObject(node1.asExpr(), node2.asExpr()) or
isBase64(node1.asExpr(), node2.asExpr()) or
isdecrypt(node1.asExpr(), node2.asExpr())
}
}

from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "source are"

​ 但是还有一个坑,直接使用上述代码是切换不到alert中查看污点传播路径的,需要在开始加上下面的内容。

1
2
3
/**
* @kind path-problem
*/

​ 上面的内容虽然可以出现结果,但是好像比较麻烦,我们可以直接将readValue的返回值当作Source,将readObject当作Sink。

image-20211118144929941

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

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



class VulConfig extends TaintTracking::Configuration {
VulConfig() { this = "shiroConfig" }

override predicate isSource(DataFlow::Node source) {
exists(MethodAccess call |
call.getMethod().getName()="readValue" and
source.asExpr()=call
)
}

override predicate isSink(DataFlow::Node sink) {
exists(MethodAccess call |
call.getMethod().getName()="readObject" and
sink.asExpr()=call
)
}
}

from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "source are"

image-20211118145119754

​ 如果将getCookie的返回值当作Source并不能获取结果,所以我们要将getCookiereadValue链接起来。这里用到了isAdditionalTaintStep谓词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
predicate isCookiegetValue(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="Cookie" and
expDest=call and
call.getMethod() = method and
method.hasName("readValue") and
method.getDeclaringType().toString() = "Cookie"
)
}

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
isCookiegetValue(node1.asExpr(), node2.asExpr())
}

​ 主要分析下exists中的语句,

  • expSrc.getType().toString()="Cookie"主要是获取第一个表达式的类型为Cookie的。
  • expDest=call第二个表达式为方法调用,后面是对调用的限制
  • call.getMethod() = method and method.hasName("readValue")限制调用的方法为readValue
  • method.getDeclaringType().toString() = "Cookie"限制方法类型为Cookie。也就是调用Cookie类里的readValue方法。

​ 上面代码实现的功能是将所有返回类型为Cookie的表达式和readValue表达式连接起来。但是假如我只想把getCookie表达式和readValue表达式链接起来呢?

​ 再次照猫画虎

1
2
3
4
5
6
7
8
9
predicate isCookiegetValue(Expr expSrc, Expr expDest) {
exists(Method method,Method methodsrc, MethodAccess call,MethodAccess callsrc|
expSrc=callsrc and callsrc.getMethod() = methodsrc and methodsrc.hasName("getCookie") and methodsrc.getDeclaringType().toString() = "CookieRememberMeManager" and
expDest=call and
call.getMethod() = method and
method.hasName("readValue") and
method.getDeclaringType().toString() = "Cookie"
)
}

image-20211118160505663

apache kylin命令执行漏洞

​ 这个项目比较大,我们就不自己构建数据库了,可以从https://lgtm.com/projects/g/apache/kylin/直接下载现成的数据库

image-20211118161726606

​ 甚至可以直接在lgtm.com中直接编写查询语句。

image-20211118161805113

​ 这个漏洞的Source是SpringMapping,Sink是ProcessBuilder,编写查询语句也比较简单

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
/**
* @kind path-problem
*/

import semmle.code.java.frameworks.spring.SpringController
import semmle.code.java.dataflow.TaintTracking
import DataFlow::PathGraph
import semmle.code.java.dataflow.FlowSources

class Configuration extends TaintTracking::Configuration {
Configuration() {
this = "Configer"
}

override predicate isSource(DataFlow::Node source) {
exists( SpringRequestMappingMethod route| source.asParameter()=route.getARequestParameter() )

}

override predicate isSink(DataFlow::Node sink) {
exists(Call call ,Callable parseExpression|
sink.asExpr() = call.getArgument(0) and
call.getCallee()=parseExpression and
parseExpression.getDeclaringType().hasQualifiedName("java.lang", "ProcessBuilder") and
parseExpression.hasName("ProcessBuilder")
)
}
}

from DataFlow::PathNode src, DataFlow::PathNode sink, Configuration config
where config.hasFlowPath(src, sink)
select sink.getNode(), src, sink, "source are"

image-20211118171537390

​ 除了上面的方式获取source还可以使用下面的方式。

1
2
3
override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}

RemoteFlowSource类内置了主流的获取参数的方式,因此也可以使用这种方式获取source。

总结

​ 通过上面的学习,已经可以编写一些简单的查询语句分析了,CodeQL上手虽然并不复杂,而且比较高效,而且官方也对很多漏洞提供了相应的查询语法,用来分析一些有源码的项目还是可以的,但是似乎目前不能分析jar包里的代码,目前也没有比较好的解决方法。