基本使用
1、首先下载struts2漏洞版本源码:
https://codeload.github.com/apache/struts/zip/refs/tags/STRUTS_2_3_20
2、构建codeql数据库(构建失败文末有解决办法):
codeql database create ~/CodeQL/databases/struts2-2.3.20-database --language="java" --command="mvn clean install --file pom.xml" --source-root=~/CodeQL/struts-STRUTS_2_3_20/xwork-core
QL代码编写
source的建模
以S2-032 ( CVE-2016-3081 )、S2-033 ( CVE-2016-3687 ) 和S2-037 ( CVE-2016-4438 )为例,这三个漏洞都是与调用ognlUtil.getValue有关,比如下列代码:
String methodName = proxy.getMethod(); //<--- untrusted source, but where from?
LOG.debug("Executing action method = {}", methodName);
String timerKey = "invokeAction: " + proxy.getActionName();
try {
UtilTimerStack.push(timerKey);
Object methodResult;
try {
methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action); //<--- RCE
上面的代码中使用了proxy.getMethod()方法来获取不受信任的数据源,最终导致了调用ognlUtil.getValue造成了RCE,但除此之外还有各种方法,例如getActionName()和getNamespace(),所以可以编写如下QL代码,对这些不受信任的源进行建模:
class ActionProxyGetMethod extends Method {
ActionProxyGetMethod() {
getDeclaringType().getASupertype*().hasQualifiedName("com.opensymphony.xwork2", "ActionProxy") and
(
hasName("getMethod") or
hasName("getNamespace") or
hasName("getActionName")
)
}
}
predicate isActionProxySource(DataFlow::Node source) {
source.asExpr().(MethodAccess).getMethod() instanceof ActionProxyGetMethod
}
因为proxy是com.opensymphony.xwork2.ActionProxy类型,所以getDeclaringType().getASupertype*().hasQualifiedName(“com.opensymphony.xwork2”, “ActionProxy”),其中*号代表递归
sink的建模
S2-032、S2-033和S2-037使用了 OgnlUtil::getValue(),然而在漏洞 S2-045(CVE-2017-5638)中,使用了 TextParseUtil::translateVariables(),所以推测一下OgnlUtil::compileAndExecute() 和 OgnlUtil::compileAndExecuteMethod() 也可能存在漏洞,所以对传入compileAndExecute、和compileAndExecute的sink进行建模:
predicate isOgnlSink(DataFlow::Node sink) {
exists(MethodAccess ma | ma.getMethod().hasName("compileAndExecute") or ma.getMethod().hasName("compileAndExecuteMethod") |
ma.getMethod().getDeclaringType().getName().matches("OgnlUtil") and sink.asExpr() = ma.getArgument(0))
}
MethodAccess表示方法访问,即当调用xxx.compileAndExecute或者xxx.compileAndExecuteMethod
ma.getMethod().getDeclaringType().getName().matches(“OgnlUtil”)表示该方法所在的类为OgnlUtil。
进行污点追踪
之前已经定义好了sink和source,所以直接将这两个套进去,这里重写了一个isAdditionalFlowStep,用来衔接一些没匹配到的参数。
class OgnlTaintTrackingCfg extends DataFlow::Configuration {
OgnlTaintTrackingCfg() {
this = "mapping"
}
override predicate isSource(DataFlow::Node source) {
isActionProxySource(source)
}
override predicate isSink(DataFlow::Node sink) {
isOgnlSink(sink)
}
override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
TaintTracking::localTaintStep(node1, node2) or
exists(Field f, RefType t | node1.asExpr() = f.getAnAssignedValue() and node2.asExpr() = f.getAnAccess() and
node1.asExpr().getEnclosingCallable().getDeclaringType() = t and
node2.asExpr().getEnclosingCallable().getDeclaringType() = t
)
}
}
from OgnlTaintTrackingCfg cfg, DataFlow::Node source, DataFlow::Node sink
where cfg.hasFlow(source, sink)
select source, sink
isAdditionalFlowStep用于衔接一些没匹配上的数据流,简单来说可能有如下情况,在调用bar()时没有提前调用foo(),导致数据流并不认为foo到bar是连通的所以默认DataFlow::Configuration是不包括这个流步骤的,因为也不能确定bar()中对this.field的访问总是被污染的,但是在漏洞挖掘中,包含这种形式的调用是非常有意义的。
public void foo(String taint) {
this.field = taint;
}
public void bar() {
String x = this.field; //x is tainted because field is assigned to tainted value in `foo`
}
对isAdditionalFlowStep进行逐步分析:
TaintTracking::localTaintStep(node1, node2) 测试node1到node2的连通性。
Field 表示字段,RefType表示除了原始类型(int、float等)、null、数组的任何类型
f.getAnAssignedValue()表示获取被赋值给该字段的表达式。例如有一个字段int a,其中a = 10+20,那么表达式为10+20
f.getAnAccess()表示该字段的访问。例如int y = x;//访问字段x
node1.asExpr() = f.getAnAssignedValue() and node2.asExpr() = f.getAnAccess()这段连起来:表示有一个获取被赋值给字段的表达式node1,node2表达式对该字段进行了访问。
node1.asExpr().getEnclosingCallable().getDeclaringType() = t and node2.asExpr().getEnclosingCallable().getDeclaringType() = t表示node1和node2都是同一个类
初版代码
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.TaintTracking
class ActionProxyGetMethod extends Method{
ActionProxyGetMethod(){
getDeclaringType().getASupertype*().hasQualifiedName("com.opensymphony.xwork2", "ActionProxy") and
(
hasName("getMethod") or
hasName("getActionName") or
hasName("getNamespace")
)
}
}
predicate isActionProxySource(DataFlow::Node source) {
source.asExpr().(MethodAccess).getMethod() instanceof ActionProxyGetMethod
}
predicate isOgnlSink(DataFlow::Node sink) {
exists(MethodAccess ma | ma.getMethod().hasName("compileAndExecute") or ma.getMethod().hasName("compileAndExecuteMethod") |
ma.getMethod().getDeclaringType().getName().matches("OgnlUtil") and sink.asExpr() = ma.getArgument(0))
}
class OgnlTaintTrackingCfg extends DataFlow::Configuration {
OgnlTaintTrackingCfg() {
this = "mapping"
}
override predicate isSource(DataFlow::Node source) {
isActionProxySource(source)
}
override predicate isSink(DataFlow::Node sink) {
isOgnlSink(sink)
}
override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
TaintTracking::localTaintStep(node1, node2) or
exists(Field f, RefType t | node1.asExpr() = f.getAnAssignedValue() and node2.asExpr() = f.getAnAccess() and
node1.asExpr().getEnclosingCallable().getDeclaringType() = t and
node2.asExpr().getEnclosingCallable().getDeclaringType() = t
)
}
}
from OgnlTaintTrackingCfg cfg, DataFlow::Node source, DataFlow::Node sink
where cfg.hasFlow(source, sink)
select source, sink
改进isBarrier
查看数据流步骤:
在数据流经过几步之后,调用getActionName()返回的值流入到了对pkg.getActionConfigs()返回的对象上调用get()的一个参数中:
String chainedTo = actionName + nameSeparator + resultCode; //actionName comes from `getActionName` somewhere
ActionConfig chainedToConfig = pkg.getActionConfigs().get(chainedTo); //chainedTo contains `actionName` and ended up in the `get` method.
点进入ValueStackShadowMap::get()函数内部,代码类似如下:
public Object get(Object key) {
Object value = super.get(key); //<--- key gets tainted?
if ((value == null) && key instanceof String) {
value = valueStack.findValue((String) key); //<--- findValue ended up evaluating `key`
}
return value;
}
通过查看数据流步骤发现,因为pkg.getActionConfigs()返回一个Map,而ValueStackShadowMap实现了Map接口,理论上,pkg.getActionConfigs()返回的值可能是ValueStackShadowMap的一个实例。因此,CodeQL DataFlow库显示了从变量chainedTo到ValueStackShadowMap类中get()实现的这一潜在流动。
但实际上,ValueStackShadowMap类属于jasperreports插件,且该类的实例只在几个地方创建,其中没有任何一个是通过pkg.getActionConfigs()返回的。所以ValueStackShadowMap::get()可能是个误报,不太可能被触发。
定义一个isBarrier,如果污点数据流入ValueStackShadowMap的get()或containsKey()方法,则不继续跟踪它。
override predicate isBarrier(DataFlow::Node node) {
exists(Method m | (m.hasName("get") or m.hasName("containsKey")) and
m.getDeclaringType().hasName("ValueStackShadowMap") and
node.getEnclosingCallable() = m
)
}
最终代码:
https://github.com/Semmle/SecurityQueries/blob/master/semmle-security-java/queries/struts/cve_2018_11776/initial.ql
坑点
1、构建数据库失败
~/CodeQL/struts-STRUTS_2_3_20/xwork-core/target/surefire-reports
-------------------------------------------------------------------------------
Test set: TestSuite
-------------------------------------------------------------------------------
Tests run: 741, Failures: 5, Errors: 0, Skipped: 0, Time elapsed: 13.404 sec <<< FAILURE!
testSetPropertiesDate(com.opensymphony.xwork2.ognl.OgnlUtilTest) Time elapsed: 0.011 sec <<< FAILURE!
junit.framework.AssertionFailedError: expected:<Fri Feb 12 00:00:00 CST 1982> but was:<null>
at junit.framework.Assert.fail(Assert.java:47)
at junit.framework.Assert.failNotEquals(Assert.java:283)
at junit.framework.Assert.assertEquals(Assert.java:64)
at junit.framework.Assert.assertEquals(Assert.java:71)
at com.opensymphony.xwork2.ognl.OgnlUtilTest.testSetPropertiesDate(OgnlUtilTest.java:351)
testRangeValidation(com.opensymphony.xwork2.validator.DateRangeValidatorTest) Time elapsed: 0.028 sec <<< FAILURE!
junit.framework.AssertionFailedError: Expected date range validation error message.
at junit.framework.Assert.fail(Assert.java:47)
at junit.framework.Assert.assertTrue(Assert.java:20)
at junit.framework.Assert.assertNotNull(Assert.java:214)
at com.opensymphony.xwork2.validator.DateRangeValidatorTest.testRangeValidation(DateRangeValidatorTest.java:60)
testVisitorChildConversionValidation(com.opensymphony.xwork2.validator.VisitorFieldValidatorTest) Time elapsed: 0.015 sec <<< FAILURE!
junit.framework.AssertionFailedError: expected:<6> but was:<4>
at junit.framework.Assert.fail(Assert.java:47)
at junit.framework.Assert.failNotEquals(Assert.java:283)
at junit.framework.Assert.assertEquals(Assert.java:64)
at junit.framework.Assert.assertEquals(Assert.java:195)
at junit.framework.Assert.assertEquals(Assert.java:201)
at com.opensymphony.xwork2.validator.VisitorFieldValidatorTest.testVisitorChildConversionValidation(VisitorFieldValidatorTest.java:189)
testVisitorChildValidation(com.opensymphony.xwork2.validator.VisitorFieldValidatorTest) Time elapsed: 0.015 sec <<< FAILURE!
junit.framework.AssertionFailedError: expected:<5> but was:<3>
at junit.framework.Assert.fail(Assert.java:47)
at junit.framework.Assert.failNotEquals(Assert.java:283)
at junit.framework.Assert.assertEquals(Assert.java:64)
at junit.framework.Assert.assertEquals(Assert.java:195)
at junit.framework.Assert.assertEquals(Assert.java:201)
at com.opensymphony.xwork2.validator.VisitorFieldValidatorTest.testVisitorChildValidation(VisitorFieldValidatorTest.java:167)
testContextIsPropagated(com.opensymphony.xwork2.validator.VisitorFieldValidatorTest) Time elapsed: 0.008 sec <<< FAILURE!
junit.framework.AssertionFailedError: expected:<3> but was:<2>
at junit.framework.Assert.fail(Assert.java:47)
at junit.framework.Assert.failNotEquals(Assert.java:283)
at junit.framework.Assert.assertEquals(Assert.java:64)
at junit.framework.Assert.assertEquals(Assert.java:195)
at junit.framework.Assert.assertEquals(Assert.java:201)
at com.opensymphony.xwork2.validator.VisitorFieldValidatorTest.testContextIsPropagated(VisitorFieldValidatorTest.java:153)
报错的都是一些测试方法,解决方法:删除测试目录,即:
~/CodeQL/struts-STRUTS_2_3_20/xwork-core/src/test
参考链接
https://www.freebuf.com/articles/web/283795.html (入门推荐)
https://securitylab.github.com/research/apache-struts-CVE-2018-11776/