0x01 梳理应用攻击面

应用的攻击面其实就是攻击者可以与应用交互的地方,也就是攻击者可以控制的输入的地方,可以想象一个应用就是一个房子,而攻击面就是应用的窗户,门之类可以进入到应用内部的入口。
在静态分析领域,这种攻击面一般被称为source,所以拿到一个应用的第一件事就是梳理整个应用可能的source点。
我们使用CodeQL中的RemoteFlowSource来寻找代码中可能的source

import java
import semmle.code.java.dataflow.FlowSources

from RemoteFlowSource source
select source

image.png

定义额外的source

经过查询后,发现RemoteFlowSource只建模了普通的HTTP请求,而对于Spring的路由等参数的source并没有进行建模,所以我们需要自己建模新的source

这里要注意的是:CodeQL实际上已经对Spring框架的MVC进行了建模,但是由于GateWay的配置是Spring Actuator的配置,所以用内置的Spring库无法查出准确的source

在Gateway中实际使用@GetMapping@PostMapping来进行路由注册,所以我们可以定义如下QL来找到所有路由的source

import java
import semmle.code.java.dataflow.FlowSources


class RouteAnnotation extends AnnotationType{
    RouteAnnotation(){
        this.hasQualifiedName("org.springframework.web.bind.annotation", "GetMapping") or
        this.hasQualifiedName("org.springframework.web.bind.annotation", "PostMapping") 
    }
}

class EndpointMethod extends Callable{
    EndpointMethod(){
        this.getAnAnnotation().getType() instanceof RouteAnnotation
    }
}

class GateWaySource extends RemoteFlowSource{
    GateWaySource(){
        any(EndpointMethod m).getAParameter() = this.asParameter()
    }

    override string getSourceType(){
        result = "gateway route source"
    }
}

之后可以通过如下的QL来寻找所有可能的source:

import java
import gateway_route

from RemoteFlowSource source
select source, source.getSourceType()

在对应用的熟悉过程中,对source的定义可能会增加,目前我们可以梳理出这些端点是攻击者感兴趣的地方:
image.png

0x02 寻找应用的危险函数

在找到source之后,我们需要看看应用内有没有比较危险的方法调用,如果一个应用只有一个phpinfo的话那肯定也没有什么挖的价值。
在CodeQL中建模了不少Java的常见漏洞,我们以SpEL表达式注入作为sink,来看下整个应用中有没有SpEL表达式使用的地方

import java
import semmle.code.java.security.SpelInjectionQuery
import semmle.code.java.dataflow.DataFlow


from SpelInjectionConfig conf, DataFlow::Node sink
where conf.isSink(sink)
select sink, sink.getEnclosingCallable(), sink.getEnclosingCallable().getDeclaringType()

可以看到整个应用中有4个使用了SpEL表达式注入的地方:
image.png
第4个其实就是这个漏洞中SpEL表达式的地方,这个时候我们已经知道了我们的输入从哪里来,也知道了我们需要到什么地方,所以接下来的工作就都是寻找一条从source->sink的路径。

0x03 数据流分析

我们直接连接source和sink,看看能不能让CodeQL帮助我们查找出来一条路径

import java
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.security.SpelInjectionQuery
import gateway_route



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

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

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



from DataFlow::Node source, DataFlow::Node sink
where any(UnsafeUseConfiguration conf).hasFlow(source, sink)
select source, sink

最后的结果很遗憾,没有任何结果
image.png
这个时候不要觉得不存在问题,这种查找不出来实际上是常态,因为CodeQL并不分析Dependency的内容,所以会导致数据流中间会断掉,这个时候我们可以一步步从sink向上推,辅助人工的审计来找到这条path

ParitialPath & 反向数据流追踪

来到了数据流分析的重头戏:ParitialPath和反向数据流追踪
对于一个应用来说,source有可能是非常多的,而且source可能会被传递到各种各样的地方,但是sink一般来说比较少且数据的流向比较固定,所以我们可以反向从sink来查找,看看有没有一条路径可以到source,这个时候就需要使用到反向数据流追踪
在DataFlow的Configuration中有下面的方法:
image.png
通过hasPartialFlowRev我们可以查询哪些数据到了sink点,我们可以用explorationLimit来限制查询的深度
image.png
所以我们编写如下的QL来寻找到sink点的数据是从哪里来的,在编写QL的时候要始终记得:你想问CodeQL关于代码的什么问题,例如我们现在想问

  • 什么数据被传递给了sink点?
/**
 * @kind path-problem
 */

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.SpelInjectionQuery
import semmle.code.java.security.UnsafeDeserializationQuery
import semmle.code.java.security.JndiInjectionQuery
import semmle.code.java.dataflow.TaintTracking
import DataFlow::PartialPathGraph

// from RemoteFlowSource source
// select source

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

    override int explorationLimit() {result = 100}

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

    override predicate isSink(DataFlow::Node sink) {
        any(SpelInjectionConfig conf).isSink(sink)
    }
    
    override predicate isAdditionalTaintStep(DataFlow::Node n1, DataFlow::Node n2){
        exists(MethodAccess ma | ma.getMethod().getName() = "parseExpression" and n2.asExpr() = ma.getArgument(0) | n1.asExpr() = ma)
    }
}

from DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink, int dist
where any(UnsafeUseConfiguration conf).hasPartialFlowRev(source, sink, dist)
select sink.getNode(), source, sink, "has flow to sink"

最终查出来如下的内容
image.png
这里要注意的是,QL只会查出来传入sink的变量的定义的地方,所以需要我们不断去写查询来往上跟踪:
image.png

image.png
这个时候就需要我们一个个去看对应的结果了,用CodeQL来做辅助来看这个变量是不是用户可控的

以ShortcutConfigurable为例

可以看到expression是来自于parser.parseExpression这个函数,而这个函数的第一个参数如果是污染的,那么expression就是被污染的,所以我们编写如下QL来看看entryValue是从哪里来的?

// Q2:寻找parseExpression的参数从哪里来
from DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink, int dist
where any(ParseExpressionConfiguration conf).hasPartialFlowRev(source, sink, dist) and dist = 1
select sink.getNode(), source, sink, dist + " distance flow from source $@", source.getNode(), source.getNode().getEnclosingCallable().getName()

我们不断调整dist的大小,可以看到所有的污点都来自于normalize这个函数,里面的apply其实也是normalize里面的一个lamba表达式函数
image.png
可以看到如果normalize的第一个参数是污染的,那么就会导致漏洞
image.png
接下来我们继续要寻找normalize的调用的地方,可以看到只有一个地方调用了normalize
image.png
那么我们需要知道this.properties是在哪里被赋值了,因为如果我们能控制this.properties我们就可以走到SpEL注入的地方
所以用如下的QL找到对this.properties写入的地方:

class PropertiesWrite extends FieldWrite{
    PropertiesWrite(){
        this.getField().getName() = "properties" and this.getField().getDeclaringType().getName() = "AbstractBuilder"
    }
}

最后的结果也只有一个方法内部对properties进行了写入操作
image.png
所以我们接下来想知道,哪些数据是可以写入properties的,所以构建如下的QL:

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

    override int explorationLimit() {result = 5}

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

    override predicate isSink(DataFlow::Node sink) {
        exists(PropertiesWrite write| write.getRHS() = sink.asExpr())
    }
}

最终的结果可以看到,所有的properties的定义都是从routeDefinitionLocator中的RouteDefinition的Filter中获取的
image.png

image.png
所以需要看一下routeDefinitionLocator中是如何定义RouteDefinitions的
routeDefinitionLocator实际上是一个接口类型:
image.png
想要触发漏洞实际上就是要让getRouteDefinitions返回的是攻击者可以控制的对象
image.png
由于这个类的实现很多,每个实现都不一样,所以我们得分别进行建模,或者手工去进行审计
最终发现其中有一个类的getRouteDefinitions的值可以通过端点控制:
image.png
根据搜索,该类的save方法正好在save接口进行了调用
image.png
至此从source到sink的一条路径就连接了起来
image.png

0x04 总结

CodeQL不同于以往的静态分析工具,他更像是一个问答机器人,安全工程师对代码有疑问,比如:用户可控的source点在哪?危险的函数在哪?危险函数的参数是从哪里被传进来的?等等这些问题,可以通过自己编写QL来询问CodeQL。CodeQL是一种代码审计的辅助工具,由于数据流分析一直没有什么突破,想完全依靠静态分析挖掘漏洞不太现实,CodeQL将人工和自动结合了起来,帮助安全工程师可以挖掘更深的调用链。

0x05 篇外

通过CodeQL的查询,实际上如果Spring-Cloud-Gateway的注册中心被hack,那么使用这个注册中心的Spring-Cloud-Gateway同样会导致SpEL表达式注入
image.png