Java安全攻防之Spring Cloud Gateway攻击Redis
2024-4-1 18:39:6 Author: govuln.com(查看原文) 阅读量:27 收藏

1

概述

案例来源于去年二月的一次攻防项目,在进入到目标公司外网nacos后,通过翻阅nacos中的配置文件发现存在gateway路由配置文件,以及内网redis地址、密码。
尝试在nacos中通过编辑路由配置文件,通过新增路由以及配置AddResponseHeader过滤器执行SPEL表达式实现RCE。
测试后发现由于目标是高版本的Spring Cloud修复了该漏洞只能执行简单SPEL表达式无法实现RCE,在最后我们通过Gateway攻击了内网的redis实现了RCE。
本文以spring-cloud-starter-gateway3.1.5测试。

2

漏洞利用

以actuator/gateway来介绍该利用方法(和nacos原理差不多,通过heapdump也能获取到redis地址密码)。

spring-cloud-starter-gateway#3.0.7、3.1.1修复了CVE-2022-22947,使用GatewayEvaluationContext替换了StandardEvaluationContext。

public static class GatewayEvaluationContext implements EvaluationContext {    private final BeanFactoryResolver beanFactoryResolver;    private final SimpleEvaluationContext delegate;
public GatewayEvaluationContext(BeanFactory beanFactory) { this.beanFactoryResolver = new BeanFactoryResolver(beanFactory); Environment env = (Environment)beanFactory.getBean(Environment.class); boolean restrictive = (Boolean)env.getProperty("spring.cloud.gateway.restrictive-property-accessor.enabled", Boolean.class, true); if (restrictive) { this.delegate = SimpleEvaluationContext.forPropertyAccessors(new PropertyAccessor[]{new RestrictivePropertyAccessor()}).withMethodResolvers(new MethodResolver[]{(context, targetObject, name, argumentTypes) -> { return null; }}).build(); } else { this.delegate = SimpleEvaluationContext.forReadOnlyDataBinding().build(); }
}

在GatewayEvaluationContext中,生成了SimpleEvaluationContext以及限制了属性的访问和方法的调用。导致从3.0.7开始只能执行一些简单的的SPEL表达式,无法再实现RCE。

题外话,在实战中,经常会遇到通过/actuator/gateway/routes能够发现一些其他攻击者新增的路由,但是自己新增后refresh一直不存在。这种通常是因为被其他攻击者插入了错误路由,导致在refresh的时候一直异常没法新增。

比如其他攻击者在之前已经新增了一个命令执行的路由(或者语法错误的),

然后此时我们去查看路由,是不存在spel这个恶意路由的。

因为版本比较高,漏洞已经修复的原因,导致refresh的时候直接出了异常,所以没法新增路由。

这个时候我们需要先将存在错误的路由给删除掉。

/actuator/gateway/routes 没法看到恶意的路由,通过/actuator/gateway/routedefinitions 接口可以查看到所有的。

然后将这个错误路由删除掉,即可正常新增路由了。

再回到漏洞利用,没法通过SPEL来实现RCE,并且已知了内网redis地址以及密码,很容易想到能否通过新增一个路由来指向redis地址,然后来攻击redis。

虽然gateway新增的路由仅支持http/https协议,但是因为我们能够完全的控制请求包,意味着可以随意的注入新行,按理也能正常攻击redis。

首先创建一个指向redis的路由,然后刷新,访问路由。

{      "id": "redis",      "predicates": [        "Path=/xxxxxxxx/**"    ],      "filters": [],      "uri": "http://localhost:6379/",      "order": 0    }

在请求gateway的路由后,gateway确实将完整的请求转发到了redis端口上。

然后正常来说,只需要在一个新行里面注入slaveof xx xx即可实现RCE,此时又有了新的问题。

io.netty.handler.codec.DefaultHeaders

public T addObject(K name, Object value) {    return this.add(name, this.valueConverter.convertObject(ObjectUtil.checkNotNull(value, "value")));}
public T add(K name, V value) {    this.nameValidator.validateName(name);    ObjectUtil.checkNotNull(value, "value");    int h = this.hashingStrategy.hashCode(name);    int i = this.index(h);    this.add0(h, i, name, value);    return this.thisT();}

在Spring Cloud Gateway解析重组request的header时,首先通过:分割得到header的name和value,然后调用addObject方法来添加请求头,在该方法中通过validateName方法来验证header name是否合法,this.valueConverter.convertObject方法来转换header value。

验证了header name中是否存在空白符,如果存在空白符就直接抛出了异常。所以没法在header中插入slaveof xxx来实现RCE。

虽然没法在header name中插入redis语句,但是又很容易想到request body里面肯定不会存在限制,可以随意的插入redis语句。

成功在请求包中插入了redis语句。

int processCommand(client *c) {    if (!scriptIsTimedout()) {        /* Both EXEC and scripts call call() directly so there should be         * no way in_exec or scriptIsRunning() is 1.         * That is unless lua_timedout, in which case client may run         * some commands. */        serverAssert(!server.in_exec);        serverAssert(!scriptIsRunning());    }
/* in case we are starting to ProcessCommand and we already have a command we assume * this is a reprocessing of this command, so we do not want to perform some of the actions again. */ int client_reprocessing_command = c->cmd ? 1 : 0;
/* only run command filter if not reprocessing command */ if (!client_reprocessing_command) { moduleCallCommandFilters(c); reqresAppendRequest(c); }
/* Handle possible security attacks. */ if (!strcasecmp(c->argv[0]->ptr,"host:") || !strcasecmp(c->argv[0]->ptr,"post")) { securityWarningCommand(c); return C_ERR; }

但是很容易想到一个问题,redis在很久以前逐行处理命令的时候,就会判断该行中是否含有host: 或者 post关键字,如果含有则会直接返回异常不再继续处理后续的命令。

POST这个关键字没影响,因为我可以随意修改请求包,GET+request body,但是host:这个关键字经过测试,就算我在请求包中删除了host头,经过了gateway的解析重组后它会自动的添加上host头导致没法解决。

在这里卡了几十分钟,一直没法解决。后面想到,既然之前的spel漏洞利用是通过新增filter AddResponseHeader来实现的,那么有没有什么其他的filter能帮助我删除掉host头。

https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories/removerequestheader-factory.html

翻阅文档发现还真有一个removerequestheader的filter,最后经过测试发现并不能实现利用,在gateway解析重组的流程中,是先把filter链作用完后再添加了host header,导致无法实现利用。

然后又只能继续翻阅文档看看还有没有什么好玩的filter,

https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-mvc/filters/addrequestheader.html

发现存在addrequestheader filter,能够想到如果在filter链中重组header时,如果gateway没有处理好crlf也可能利用。

addrequestheader组装header最后调用和之前提到的是一致的。

io.netty.handler.codec.http.DefaultHttpHeaders#addObject,

public T addObject(K name, Object value) {    return this.add(name, this.valueConverter.convertObject(ObjectUtil.checkNotNull(value, "value")));}

在this.valueConverter.convertObject中,HeaderValueConverterAndValidator对header value进行校验。

private static final class HeaderValueConverterAndValidator extends HeaderValueConverter {    static final HeaderValueConverterAndValidator INSTANCE = new HeaderValueConverterAndValidator();
private HeaderValueConverterAndValidator() { super(null); }
public CharSequence convertObject(Object value) { CharSequence seq = super.convertObject(value); int state = 0;
for(int index = 0; index < seq.length(); ++index) { state = validateValueChar(seq, state, seq.charAt(index)); }
if (state != 0) { throw new IllegalArgumentException("a header value must not end with '\\r' or '\\n':" + seq); } else { return seq; } }
private static int validateValueChar(CharSequence seq, int state, char character) { if ((character & -16) == 0) { switch (character) { case '\u0000': throw new IllegalArgumentException("a header value contains a prohibited character '\u0000': " + seq); case '\u000b': throw new IllegalArgumentException("a header value contains a prohibited character '\\v': " + seq); case '\f': throw new IllegalArgumentException("a header value contains a prohibited character '\\f': " + seq); } }
switch (state) { case 0: switch (character) { case '\n': return 2; case '\r': return 1; } default: return state; case 1: if (character == '\n') { return 2; }
throw new IllegalArgumentException("only '\\n' is allowed after '\\r': " + seq); case 2: switch (character) { case '\t': case ' ': return 0; default: throw new IllegalArgumentException("only ' ' and '\\t' are allowed after '\\n': " + seq); } } }

从该方法中可以看出,如果header value中只\n是不行的,但是只要\t在\n后面就可以,\t不会影响redis命令的解析。

\n抛出了异常,修改为 "value": "\n\taaaa" 

成功注入了新行,并且在host之前。

成功执行了redis命令,最终RCE了目标系统。

3

总结

Spring Cloud Gateway 3.1.6修复了CRLF注入,在添加header的方法中新增了validateValue方法对header value进行校验,不再允许存在换行等空白符。

public T add(K name, V value) {    this.validateName(this.nameValidator, true, name);    this.validateValue(this.valueValidator, name, value);    ObjectUtil.checkNotNull(value, "value");    int h = this.hashingStrategy.hashCode(name);    int i = this.index(h);    this.add0(h, i, name, value);    return this.thisT();}

在攻击redis时,除了通过slaveof等方式rce(需要出网,以及版本不能太高),还可以尝试通过set token来利用。目前很多系统的鉴权都通过判断redis中是否含有对应的token实现,通过set token可以通过鉴权进入到目标系统中RCE或者配合fastjson等其他反序列化利用。

spring gateway处理{{}}此类数据时,会尝试解析,所以需要通过append实现。

当然除了攻击redis这种方式,也可以通过新增内网地址gateway尝试攻击内网http应用漏洞,但是由于不知道内网情况难度比较大。

(yulegeyu@边界无限烛龙实验室供稿)


往期推荐


文章来源: https://govuln.com/news/url/p7bX
如有侵权请联系:admin#unsafe.sh