0%

用CodeQL分析漏洞_CVE-2022-42889

Apache Commons Text 是专门用来处理文本的一个库,据文档介绍其支持变量插值。在1.5 - 1.9版本默认支持”script”类型的插值,于是可以造成任意代码执行,也就是CVE-2022-42889。

漏洞原理

该工具库使用示例如下:

1
2
3
4
5
6
7
StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
String text = interpolator.replace(
"Base64 Decoder:${base64Decoder:SGVsbG9Xb3JsZCE=}\n"
+ "Date: ${date:yyyy-MM-dd}\n"
+ "Environment Variable: ${env:USER}\n"
+ "Script: ${script:javascript:3 + 4}\n");
System.out.println(text);

执行结果为:

Base64 Decoder:HelloWorld!
Date: 2022-12-17
Environment Variable: leixiao
Script: 7

很容易发现安全问题,所以漏洞原理其实也很简单。

命令执行POC为:

${script:js:new java.lang.ProcessBuilder("open", "-a", "calculator").start()}

CodeQL分析

测试环境

  • CodeQL 2.11.6
  • Commons Text 1.9

Sink

调试跟踪,最后的触发点在

org.apache.commons.text.lookup.ScriptStringLookup#lookup

1
Objects.toString(scriptEngine.eval(script), (String)null);

可以写出如下Sink点

1
2
3
4
5
6
7
8
9
class ScriptEngineEval extends DataFlow::Node {
ScriptEngineEval() {
exists(MethodAccess ma |
ma.getCallee().hasName("eval") and
ma.getCallee().getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngine") and
this.asExpr() = ma.getArgument(0)
)
}
}

Source

可以将该库所有公有类的字符串类型公有方法参数作为Source点

1
2
3
4
5
6
7
8
9
10
11
class PublicMethodParameter extends DataFlow::Node {
PublicMethodParameter() {
exists(Method m, Parameter p |
m.getDeclaringType().isPublic() and
m.isPublic() and
p = m.getAParameter() and
p.getType().hasName("String") and
this.asParameter() = p
)
}
}

额外的污点步骤

发现直接使用上面的Source和Sink会没有查询结果,说明从Source到Sink的路径上缺少了一步,可以使用PartialPathGraph来Debug

先将Source的范围缩小,否则查询会耗非常长时间,然后逐步将Source定义替换为沿路径出现的合适的Node

最后我的Source定义为函数org.apache.commons.text.StringSubstitutor#resolveVariable的参数,并且发现了污点传播中断的地方,完整查询如下:

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 java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.TaintTracking
import DataFlow::PartialPathGraph

class ResolveVariable extends DataFlow::Node {
ResolveVariable() {
exists(Method resolveVariable |
resolveVariable
.getDeclaringType()
.hasQualifiedName("org.apache.commons.text", "StringSubstitutor") and
resolveVariable.hasName("resolveVariable") and
this.asParameter() = resolveVariable.getParameter(0)
)
}
}

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

override predicate isSource(DataFlow::Node source) { source instanceof ResolveVariable }

override int explorationLimit() { result = 10 }
}

from TaintTrackingConfig cfg, DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink
where cfg.hasPartialFlow(source, sink, _)
select sink, source, sink, "-"

image-20221217154807090

污点传播止于resolver.lookup(variableName)的参数variableName,这里的resolverStringLookup接口。我期望的是污点继续流向StringLookup的具体实现类中,其实CodeQL一般是支持Java这种多态特性的,但是就该项目来看,这一部分的写法比较特殊,导致CodeQL不支持。(后面搜到了一条相关的issue:https://github.com/github/codeql/issues/11385 ,然后我又去做了对照实验,确实当接口名为org.apache.commons.text.lookup.StringLookup就会有这个bug)

最后解决办法比较粗暴,如果调用抽象方法,那么直接将污点传播到对应的所有具体方法上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TaintStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(MethodAccess ma, Method m, RefType baseClass |
//pred为调用抽象方法时传入的参数,succ为传入到具体方法的参数
(
ma.getCallee().isAbstract() and
pred.asExpr() = ma.getAnArgument() and
not m.isAbstract() and
succ.asParameter() = m.getAParameter()
) and
//调用的抽象方法需要和具体方法名字一样
ma.getCallee().getName() = m.getName() and
//调用的抽象方法所属的类和具体方法所属的类应该继承自同一抽象类
(
baseClass.isAbstract() and
ma.getCallee().getDeclaringType().hasSupertype*(baseClass) and
m.getDeclaringType().hasSupertype*(baseClass)
)
)
}
}

完整查询

最后完整查询如下

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

import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.TaintTracking
import DataFlow::PathGraph

class PublicMethodParameter extends DataFlow::Node {
PublicMethodParameter() {
exists(Method m, Parameter p |
m.getDeclaringType().isPublic() and
m.isPublic() and
p = m.getAParameter() and
p.getType().hasName("String") and
this.asParameter() = p
)
}
}

class ScriptEngineEval extends DataFlow::Node {
ScriptEngineEval() {
exists(MethodAccess ma |
ma.getCallee().hasName("eval") and
ma.getCallee().getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngine") and
this.asExpr() = ma.getArgument(0)
)
}
}

class TaintStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(MethodAccess ma, Method m, RefType baseClass |
(
ma.getCallee().isAbstract() and
pred.asExpr() = ma.getAnArgument() and
not m.isAbstract() and
succ.asParameter() = m.getAParameter()
) and
ma.getCallee().getName() = m.getName() and
(
baseClass.isAbstract() and
ma.getCallee().getDeclaringType().hasSupertype*(baseClass) and
m.getDeclaringType().hasSupertype*(baseClass)
)
)
}
}

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

override predicate isSource(DataFlow::Node source) { source instanceof PublicMethodParameter }

override predicate isSink(DataFlow::Node sink) { sink instanceof ScriptEngineEval }
}

from TaintTrackingConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink,
"Source: " + source.getNode().asParameter().getCallable().getDeclaringType() + "." +
source.getNode().asParameter().getCallable()