1
概述
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@边界无限烛龙实验室供稿)
往期推荐