搭建环境
由于 Struts 2.0.8 版本较为陈旧,IntelliJ IDEA 对其支持有限,导致在环境配置过程中会遇到较多报错和兼容性问题。希望在搭建环境这一步,大家能够保持耐心,严格按照我的步骤操作,并积极查阅相关资料来解决遇到的问题。
我的项目地址:https://gitee.com/xiao-tang-san/struts2
创建项目
按照下面的配置创建一个项目。
导入依赖
这一步一定要按我的操作做,网上都找不到相关的教程。
在pom.xml中导入依赖,正常情况下这一步会报错,因为Maven 仓库里面可能没有这个2.0.8版本,这个需要我们去下载。
注意这个导入的依赖要根据项目中的需求进行导入,因为下面我们已经安装了2.0.8版本的所有jar包,需要根据现实情况修改相应的配置。
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
<groupId>opensymphony</groupId>
<artifactId>xwork</artifactId>
<version>2.0.3</version>
</dependency>
下载地址:https://archive.apache.org/dist/struts/library/
从这个仓库里面找到2.0.8版本下载,然后使用下面的批量添加脚本。
批量添加脚本
#!/bin/bash
# 这个地址是下载下来的目录
LIB_DIR="/Downloads/struts-2.0.8/lib"
for jarfile in "$LIB_DIR"/*.jar
do
filename=$(basename "$jarfile")
# 简单从文件名解析artifactId(去掉版本号和.jar)
artifactId=$(echo "$filename" | sed -E 's/(-[0-9]+\.[0-9]+\.[0-9]+)?\.jar$//')
echo "Installing $filename as artifactId=$artifactId ..."
mvn install:install-file \
-DgroupId=org.apache.struts \
-DartifactId=$artifactId \
-Dversion=2.0.8 \
-Dpackaging=jar \
-Dfile="$jarfile"
done
此时在项目里面的导入就不会报错了。
配置过滤器
再修改web.xml
,在这里主要是配置struts2
的过滤器。
<web-app>
<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>
配置文件
main目录下添加java目录。
创建类
内容
package com.test.s2001.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";
}
}
然后,在 webapp
目录下创建&修改两个文件 —— index.jsp
&welcome.jsp
,内容如下。
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<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
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<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>
然后在 main
文件夹下创建一个 resources
文件夹,内部添加一个 struts.xml
,内容为:
<?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.test.s2001.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>
添加tomcat
按照下图的方法配置就行了。
要注意部署和tomcat的版本,这一步根据报错信息可以自行解决。
完整效果
项目目录
运行结果
流程分析
上面我们已经搭建好了环境,这一部分我们来调试代码。
完整调用链
HttpServlet → Dispatcher.serviceAction()
↓
createContextMap() → extraContext 包含用户参数
↓
ActionProxyFactory.createActionProxy()
↓
DefaultActionInvocation.invoke()
↓
ParametersInterceptor.intercept()
↓
ValueStack.setValue(参数名为表达式) ← 触发 OGNL 求值 → RCE
Filter过滤器
先回到我们上面配置的web.xml中的过滤器
在 web.xml
中配置的 org.apache.struts2.dispatcher.FilterDispatcher
过滤器,是 Struts2 早期版本的核心请求入口。每当有请求到达时,Tomcat 会调用过滤器的 doFilter
方法来处理请求。这个方法中,过滤器会把大部分不重要的细节跳过,直接调用内部的 dispatcher.serviceAction
方法来完成请求的处理。
dispatcher.serviceAction
这个方法是整个 Struts2 请求处理的核心,它会根据请求的 URL 和参数,加载对应的配置(比如通过 DefaultConfiguration#addPackageConfig
注入的配置),然后找到对应的 Action 进行执行,最后把结果渲染到页面。
所以,简单来说:
FilterDispatcher.doFilter
是入口,会被服务器自动调用。- 里面直接调用
dispatcher.serviceAction
处理请求。 serviceAction
通过加载和管理配置(如DefaultConfiguration.RuntimeConfigurationImpl#namespaceActionConfigs
)找到正确的业务逻辑执行。- 这是 Struts2 的请求处理核心流程。
doFilter方法
org.apache.struts2.dispatcher.FilterDispatcher#doFilter
在 doFilter()
方法中,它首先调用 prepareDispatcherAndWrapRequest()
初始化 Dispatcher
并包装 HttpServletRequest
(处理多部分请求等),然后通过 actionMapper.getMapping()
获取与当前请求匹配的 ActionMapping
。若匹配成功,则调用 dispatcher.serviceAction()
方法正式交由 Struts2 执行对应的 Action
,这其中会触发 ActionProxy
的创建、DefaultActionInvocation
初始化、拦截器链执行(例如 ParametersInterceptor
),最后调用目标 Action
方法并返回结果视图。
与漏洞相关的是dispatcher.serviceAction()
,我们跟入this.dispatcher.serviceAction
serviceAction
org.apache.struts2.dispatcher.Dispatcher#serviceAction
通过 createContextMap()
方法将 HttpServletRequest
中的请求参数封装为 extraContext
,这些参数中可能包含用户精心构造的恶意 OGNL 表达式。随后,调用 createActionProxy()
创建 ActionProxy
实例时会将 extraContext
传入,该过程会进一步调用 DefaultActionInvocation
并加载拦截器链。接下来执行 proxy.execute()
时,会触发拦截器链中如 ParametersInterceptor
的执行,该拦截器会从 extraContext
中取出参数并通过 OGNL 表达式求值机制将其设置到目标对象的属性中。
下图是我们需要关注的重点代码。
ActionProxy proxy = ((ActionProxyFactory)config.getContainer().getInstance(ActionProxyFactory.class)).createActionProxy(namespace, name, extraContext, true, false);
通过 ActionProxyFactory 的 createActionProxy()
类初始化一个 ActionProxy,在这过程中也会创建 DefaultActionInvocation
的实例
execute
之后执行org.apache.struts2.impl.StrutsActionProxy#execute
,里面会执行com.opensymphony.xwork2.DefaultActionInvocation#invoke
org.apache.struts2.impl.StrutsActionProxy#execute
invoke
com.opensymphony.xwork2.DefaultActionInvocation#invoke
该方法首先检查动作是否已执行,防止重复调用;然后依次执行拦截器链中的拦截器,若无剩余拦截器则调用动作的核心逻辑。关键在于拦截器链中的 ParametersInterceptor
,它会从请求参数中解析并绑定带有 OGNL 表达式的参数到动作对象上,若参数中含恶意 OGNL 表达式就会被执行,导致远程代码执行漏洞。
ParametersInterceptor
会在本次请求的上下文中取出访问参数,将参数键值对通过 OgnlValueStack 的 setValue 通过调用 OgnlUtil.setValue()
方法。
ParametersInterceptor
com.opensymphony.xwork2.interceptor.ParametersInterceptor
它在 doIntercept
方法中获取请求参数,通过 setParameters
方法利用 OGNL 表达式将参数设置到 Action 对象上。该过程直接处理来自客户端的参数,且对参数名的过滤较为宽松,容易被构造恶意 OGNL 表达式利用。
setValue
com.opensymphony.xwork2.util.OgnlValueStack#setValue(java.lang.String, java.lang.Object, boolean)
它先获取 OGNL 上下文,设置当前操作的属性名和是否抛异常的标志,然后调用 OgnlUtil.setValue
执行赋值操作。如果出现异常,根据 throwExceptionOnFailure
决定是抛出异常还是记录日志,最后清理上下文状态。
漏洞流程
在所有拦截器执行完成后,DefaultActionInvocation
会调用 invokeActionOnly()
方法。该方法通过反射机制,执行了 Action 实现类中的 execute
方法,开始处理用户的业务逻辑。
随后,流程回到 DefaultActionInvocation
,调用了 executeResult()
方法,执行配置好的 Result 实现类中的 execute()
方法,开始处理此次请求的响应结果。比如,我们配置的返回结果是一个 JSP 文件。
JSP 文件请求最终交由 JspServlet
处理,在解析 JSP 标签时,标签的开始和结束位置分别调用了对应标签实现类(如 org.apache.struts2.views.jsp.ComponentTagSupport
)中的 doStartTag()
(用于初始化)和 doEndTag()
(标签解析结束后调用的收尾方法)。
流程进入了漏洞触发的关键位置,即调用了组件 org.apache.struts2.components.UIBean
的 end()
方法。
这里跟入evaluateParams
在 end()
方法中,会调用 evaluateParams()
来评估标签参数。由于 altSyntax
默认为开启状态,接下来会调用 findValue()
方法去寻找参数的实际值。
跟入TextParseUtil.translateVariables
进一步跟进 findValue()
,它会调用 TextParseUtil.translateVariables()
,该方法会解析字符串中 %{}
包裹的内容,提取并传递给 findValue()
进行求值。
实际的解析过程是通过 OGNL 表达式引擎实现的。此处漏洞的本质是发生了二次解析:对参数的 key 值直接拼接形成表达式 expression
,然后通过 while
循环不断调用 OGNL 解析表达式。
为什么最终会造成漏洞实际上是触发了二次解析,我们接着往下看,对key的值直接拼接去赋值给expression
然后回到上面,由于此处使用的是 while 循环来解析 Ognl ,所以获得的 %{1+1} 又会被再次执行,最终也就造成了任意代码执行