文章总结: 文档分析了SpringCloudGateway在高版本修复SPEL表达式RCE漏洞后的攻击路径。作者通过nacos配置发现内网Redis地址,尝试利用AddResponseHeader过滤器失败后,转而通过AddRequestHeader过滤器注入CRLF实现Redis未授权访问。文章详细描述了请求头验证机制、Gateway路由配置流程及绕过host头限制的方法,最终实现内网Redis命令执行。 综合评分: 82 文章分类: 漏洞分析,实战经验,WEB安全,红队,内网渗透
Java安全攻防之Spring Cloud Gateway攻击Redis
原创
yulegeyu yulegeyu
实战攻防
2024年4月1日 18:20 北京
在小说阅读器读本章
去阅读
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@边界无限烛龙实验室供稿)
往期推荐
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:实战攻防 yulegeyu yulegeyu《Java安全攻防之Spring Cloud Gateway攻击Redis》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论