本文主要讲我对漏洞的探索和探索过程中发现的一个可以进行缓存攻击的坑。
问题
struts2 的漏洞在网上已经够热闹了,各个技术站点都是头条显示,微博上也有大佬转发。
这个漏洞的危害行在于允许执行远程命令,直接攻击服务器,危害无穷;根源在于struts2框架把用户输入的数据当作命令执行了,这也是一切注入估计的根源。
今天旁边的同事soul在调试官方给出的可攻击的demo,想看看到底是怎么攻击;我好奇的是既然攻击可以远程启动一个子进程的话,那么那些输入的java代码应该会被编译,然后再执行,如果是这样的话,这框架不会是对每个输入都进行动态编译吧,这会很影响性能的,所以我想看看struts2到底是怎么处理的,就看我同事调试。
听过debug跟踪,发现struts2也不是直接调用Java 编译器的(毕竟性能问题在那里),而是直接根据输入命令解析为对应的命令对象(struts2称为Expression),在执行这个命令对象,这些命令对象会缓存在 com.opensymphony.xwork2.ognl.OgnlUtil
对象的一个 ConcurrentMap<String, Object>
属性里,键是输入的action 名称,值是命令对象,这样,对于每个输入参数,都先在缓存查找,如果没有则进行解析,这个逻辑如下:
public Object compile(String expression, Map context) throws OgnlException {
Object tree;
if (enableExpressionCache) {
tree = expressions.get(expression);
if (tree == null) {
tree = Ognl.parseExpression(expression);
expressions.putIfAbsent(expression, tree);
}
} else {
tree = Ognl.parseExpression(expression);
}
if (!enableEvalExpression && isEvalExpression(tree, context)) {
throw new OgnlException("Eval expressions has been disabled");
}
return tree;
}
具体的根据输入action name解析出相应命令对象的代码在:
这个远程漏洞的解决方法
仍然用公开的攻击方法进行攻击,根据抛出的异常信息,发现,对输入参数名进行了检查。
在 com.opensymphony.xwork2.interceptor.ParametersInterceptor
类里有两个常量:
public static final String ACCEPTED_PARAM_NAMES = "\\w+((\\.\\w+)|(\\[\\d+\\])|(\\(\\d+\\))|(\\['\\w+'\\])|(\\('\\w+'\\)))*";
protected static final int PARAM_NAME_MAX_LENGTH = 100;
在此类的其他代码检查了参数名是否匹配ACCEPTED_PARAM_NAMES
,且长度不超过PARAM_NAME_MAX_LENGTH
。
这样之前的那些攻击因为特殊字符都不能通过了,也就避免了这个漏洞。
还有一个改动是在其他地方,是解决redirect
和redirectAction
的,具体可对比代码。
总之,这个远程漏洞攻击确实是解决了。
新的问题
在调试过程发现的一个现象是:每个请求参数都有一个对应的缓存在 com.opensymphony.xwork2.ognl.OgnlUtil
的缓存里,以参数名为键,也就是说,这是一个单例对象,且是全局共享。
最大的问题是,哪怕我输入的参数名不是合法的Action的属性,它也会缓存在那里,这就是说:这个缓存的内容是用户可以决定的。
所以问题就简单了,我发送的每个请求的参数名都是不一样的,拿soul本地部署的struts-blank实例来测试,每个请求就一个参数,1万多个参数就让他OOME了。
这个问题的另一个副作用在于,即使还没有OOME,只要缓存的数量足够多时,也会导致 ConcurrentMap
退化成一个链表,因为每个位桶的链都会很长,这个影响就像之前爆出的Java Hash 漏洞攻击的效果一样。
我觉得这是一个bug,就在strtus2官网注册,提了个issue https://issues.apache.org/jira/browse/WW-4146,struts2的leader也很快回复了,说这个缓存是可以禁用的(从前面的代码也可以看出,只是我一激动就提单了):
<constant name="struts.ognl.enableExpressionCache" value="false"/>
,所以这不算是个bug吧,但我觉得这绝对一个坑了,因为默认是开启缓存的,也就是说默认情况下,我是可以让用了struts2的应用OOME的。
这个问题的为难之处在于,虽然可以关闭缓存来避免OOME,但是没有缓存肯定会导致性能劣化。如果用了缓存,不管用什么缓存机制,都可能被攻击。
目前也没有好的解决方法,所以还是先把缓存禁用了,慢总比无法访问好。
这个问题在最新版本 2.3.15.1 仍然存在。
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。
可以重新实现一个ognlutil,注入到应用中
if (tree == null && expressions.size < N) {
tree = Ognl.parseExpression(expression);
expressions.putIfAbsent(expression, tree);
}
N 放在应用中进行配置,并且一般应用中参数总量是固定的
你可以看看官网上的讨论,官方给出的方案可以修复这个问题了,做法大致是:对每个参数名如果不在缓存里,仍然解析出expression,然后执行expression,如果不出错,说明是个有效的参数名,则放入缓存,否则就不放入。