S2-001 调试分析

admin 2024年1月5日00:11:14评论17 views字数 4780阅读15分56秒阅读模式
- Title: S2-001 调试分析
- Link: http://sh3ll.me/archives/201703152213.txt
- Published: 2017-03-15 22:13
- Updated: 2017-06-08 16:15

应用中的认证函数添加错误:ActionSupport.addFieldError:

    public void validate() {
        if (username == null || username.trim().equals("")) {
            addFieldError("username", "UserName is required");
        }
        if (phoneNumber == null || phoneNumber.trim().equals("")) {
            addFieldError("phoneNumber", "PhoneNumber is required");
        }
    }

判断是否验证失败:

    // xwork-2.0.1.jar!/com/opensymphony/xwork2/interceptor/DefaultWorkflowInterceptor.class
    ... snip ...
    // 是否验证失败
    if(validationAwareAction1.hasErrors()) {
        if(_log.isDebugEnabled()) {
            _log.debug("Errors on action " + validationAwareAction1 + ", returning result name \'input\'");
        }

        return this.inputResultName;
    }

经过过滤器后进入到页面渲染,由外至内解析标签(form=>textfield=>submit):

    // struts2-core-2.0.6.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class
    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());
        return evalBody?(this.component.usesBody()?2:1):0;
    }

首先执行 doStartTag,标签闭合时执行 doEndTag,调试发现经过 doEndTag 后 Payload 得以执行,跟进之,随后跟进至 component.end:

    // struts2-core-2.0.6.jar!/org/apache/struts2/components/UIBean.class
    public boolean end(Writer writer, String body) {
        this.evaluateParams();// 遍历标签属性

        try {
            super.end(writer, body, false);
            this.mergeTemplate(writer, this.buildTemplateName(this.template, this.getDefaultTemplate()));
        } catch (Exception var7) {
            LOG.error("error when rendering", var7);
        } finally {
            this.popComponentStack();
        }

        return false;
    }

跟进 evaluateParams:

    public void evaluateParams() {
        ... snip ...
        if(this.name != null) {
            name = this.findString(this.name);  // 查询标签 name
            this.addParameter("name", name);
        }

        // 添加标签值至参数列表,供错误页面显示
        if(this.parameters.containsKey("value")) {
            this.parameters.put("nameValue", this.parameters.get("value"));
        } else if(this.evaluateNameValue()) {   // 是否取出标签的值,函数返回 true
            Class form = this.getValueClassType();
            if(form != null) {
                if(this.value != null) {
                    this.addParameter("nameValue", this.findValue(this.value, form));
                } else if(name != null) {
                    String tooltipConfigMap = name;
                    if(this.altSyntax()) {  // 若开启 altSyntax,拼接查询语句
                                            // altSyntax 属性指定是否允许在 Struts2 标签中使用表达式语法,默认值是true
                        tooltipConfigMap = "%{" + name + "}";
                    }

                    this.addParameter("nameValue", this.findValue(tooltipConfigMap, form)); // 查询并添加结果至参数列表
                                                                                            // 查询语句为 %{name}
                }
            } else if(this.value != null) {
                this.addParameter("nameValue", this.findValue(this.value));
            } else if(name != null) {
                this.addParameter("nameValue", this.findValue(name));
            }
        }
    ... snip ...
    }

跟进 findValue:

    // struts2-core-2.0.6.jar!/org/apache/struts2/components/Component.class
    protected Object findValue(String expr, Class toType) {
        if(this.altSyntax() && toType == String.class) {    // 若开启 altSyntax 进入 TextParseUtil.translateVariables,
                                                            // 对表达式进行解析
            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);
        }
    }

继续跟进 TextParseUtil.translateVariables:

    // xwork-2.0.1.jar!/com/opensymphony/xwork2/util/TextParseUtil.class
    public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
        Object result = expression; // 表达式,%{name}

        while(true) {
            int start = expression.indexOf(open + "{");
            int length = expression.length();
            int x = start + 2;
            int count = 1;
            
            // 匹配 %{key}
            while(start != -1 && x < length && count != 0) {
                char c = expression.charAt(x++);
                if(c == 123) {
                    ++count;
                } else if(c == 125) {
                    --count;
                }
            }

            int end = x - 1;
            if(start == -1 || end == -1 || count != 0) {
                return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
            }

            // 提取表达式 %{name} => name
            String var = expression.substring(start + 2, end);
            Object o = stack.findValue(var, asType);    // 取得标签值 o
                                                        // 进入 OGNL!
            if(evaluator != null) {
                o = evaluator.evaluate(o);
            }

            String left = expression.substring(0, start);
            String right = expression.substring(end + 1);
            // o 存在,这里为 Payload %{1+1}
            // 递归解析表达式,也就是说最终 Payload 将变为 1+1,进入 OGNL 最终得以执行!
            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;
            }
        }
    }
    
至此,S2-001 的漏洞原理已经很清晰,Struts2 的 OGNL 解析函数递归解析,导致提交的恶意代码最终会作为 OGNL 语句得以执行:

// Payload: username=%{1+1}
// Payload for real world:%{@java.lang.Runtime@getRuntime().exec("/usr/bin/gnome-calculator")}
%{name} => %{username} => %{1+1} => 2

官方修复方案也是取消了递归解析:
As of XWork 2.0.4, the OGNL parsing is changed so that it is not recursive. Therefore, in the example above, the result 
will be the expected %{1+1}. You can either obtain the WebWork 2.0.4 or Struts 2.0.9, which contains the corrected XWork 
library. Alternatively, you can obtain the patch and apply it to the XWork source code yourself.

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月5日00:11:14
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   S2-001 调试分析http://cn-sec.com/archives/2365854.html

发表评论

匿名网友 填写信息