2016年4月26日,Apache Struts2官方又发布了一份安全公告:Apache Struts2 服务在开启动态方法调用的情况下可以远程执行任意命令,官方编号 S2-032,CVE编号 CVE-2016-3081。这是自2012年Struts2命令执行漏洞大规模爆发之后,该服务时隔四年再次爆发大规模漏洞。该漏洞也是今年目前爆出的最严重安全漏洞。黑客利用该漏洞,可对企业服务器实施远程操作,从而导致数据泄露、远程主机被控、内网渗透等重大安全威胁。
漏洞发生后,又是一次安全和相关公司的一次集体盛会,漏洞利用者在尽可能的利用此次漏洞来显示水平的高超;各大众测平台纷纷发布中招公司,来提升平台的作用;各大安全公司也充分利用此次漏洞来提高公司的影响力,借势营销,什么免费检测,第一时间升级等。还剩一大堆郁闷的厂家,我没招谁没惹谁啊;然后就是大量的苦闷的开发运维人员要连夜升级漏洞补丁。
但是对漏洞的原理危害影响防护等少有提及。本文就是针对以上几点提出自己的见解。
原理
这个漏洞是利用struts2的动态执行OGNL来访问任意java代码的,利用该漏洞,可以扫描远程网页,判断是否存在该类漏洞,进而发送恶意指令,实现文件上传,执行本机命令等后续攻击。
OGNL是Object-Graph Navigation Language的缩写,全称为对象图导航语言,是一种功能强大的表达式语言,它通过简单一致的语法,可以任意存取对象的属性或者调用对象的方法,能够遍历整个对象的结构图,实现对象属性类型的转换等功能。
#、%和$符号在OGNL表达式中经常出现
1.#符号的用途一般有三种。
访问非根对象属性,例如#session.msg表达式,由于Struts 2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀;用于过滤和投影(projecting)集合,如persons.{?#this.age>25},persons.{?#this.name=='pla1'}.{age}[0];用来构造Map,例如示例中的#{'foo1':'bar1', 'foo2':'bar2'}。
2.%符号
%符号的用途是在标志的属性为字符串类型时,计算OGNL表达式的值,这个类似js中的eval,很暴力。
3.$符号主要有两个方面的用途。
在国际化资源文件中,引用OGNL表达式,例如国际化资源文件中的代码:reg.agerange=国际化资源信息:年龄必须在${min}同${max}之间; 在Struts 2框架的配置文件中引用OGNL表达式。
代码利用流程
1、客户端请求
http://{webSiteIP.webApp}:{portNum}/{vul.action}?method={malCmdStr}
2、DefaultActionProxy的DefaultActionProxy函数处理请求。
protected DefaultActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
this.invocation = inv;
this.cleanupContext = cleanupContext;
LOG.debug("Creating an DefaultActionProxy for namespace [{}] and action name [{}]", namespace, actionName);
this.actionName = StringEscapeUtils.escapeHtml4(actionName);
this.namespace = namespace;
this.executeResult = executeResult;
//攻击者可以通过变量传递、语法补齐、字符转义等方法进行绕过。
this.method = StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(methodName));
}
3、DefaultActionMapper的DefaultActionMapper方法method方法名
String name = key.substring(ACTION_PREFIX.length());
if (allowDynamicMethodCalls) {
int bang = name.indexOf('!');
if (bang != -1) {
//获取方法名
String method = cleanupActionName(name.substring(bang + 1));
mapping.setMethod(method);
name = name.substring(0, bang);
}
}
4、调用DefaultActionInvocation 的invokeAction方法执行传入的方法。
protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
String methodName = proxy.getMethod();
LOG.debug("Executing action method = {}", methodName);
String timerKey = "invokeAction: " + proxy.getActionName();
try {
UtilTimerStack.push(timerKey);
Object methodResult;
try {
//执行方法
methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action);
} catch (MethodFailedException e) {
解决办法
官方的解决办法是在第三步中的函数cleanupActionName增加了校验。
protected Pattern allowedActionNames = Pattern.compile("[a-zA-Z0-9._!/\\-]*");
protected String cleanupActionName(final String rawActionName) {
//校验,输入过滤正则匹配("[a-zA-Z0-9._!/\\-]*"),这是采取白名单方式,只允许大小军VA
g3rr4(zsr^g&7*+j:'?"[vs{j6ǖ4( |