从零开始学Java Web安全之S2-001分析

youncyb 发布于 2019-10-27 2786 次阅读 Web安全


1. 环境搭建

1. 工具
IntelliJ IDEAtomcat 8struts2.0.1

2. IDEA配置tomcat服务器
https://blog.csdn.net/huo920/article/details/78307797

3. 创建struts2项目

  1. 选择Java Enterprise Web Application Struts 2,注意librariies选择 稍后设置,点击next
  2. 命名和选择路径后,点击next
  3. WEB-INF目录下新建lib目录
  4. 然后下载s2.0.1,解压后将struts-2.0.1/apps/struts2-blank-2.0.1/WEB-INF/lib目录下的jar包全部放入刚才新建的lib目录,然后在IDEA中将lib添加为library
  5. 修改index.jsp的内容,主要是form表单,因为漏洞就在password的输入点
<%@ 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://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</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. WEB-INF下添加welcome.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>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>
  1. web.xml,中添加welcome-file-list
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  1. 修改struts.xml,添加action
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
        "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
    <package name="s2-001" extends="struts-default">
        <action name="login" class="com.demo.action.LoginAction">
            <result name="success">welcome.jsp</result>
            <result name="error">index.jsp</result>
        </action>
    </package>
</struts>
  1. src目录下,创建用于处理登录的LoginAction.java,继承了ActionSupport,重写其execute()方法,以便struts找到对应的jsp显示页面
package com.demo.action;

import com.opensymphony.xwork2.ActionSupport;
public class LoginAction extends ActionSupport {
    private String username = null;
    private String password = null;

    public String getUsername()
    {
        return this.password;
    }

    public String getPassword()
    {
        return this.password;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }

    public void setPassword(String password)
    {
        this.password = password;
    }

    public String execute() throws Exception
    {
        if((this.username.isEmpty()) || (this.password.isEmpty()))
        {
            return "error";
        }
        if((this.username.equalsIgnoreCase("youncyb")) && (this.password.equals("1")))
        {
            return "success";
        }
        return "error";
    }
}

2. 前置知识

进行下一步分析前,你可能需要尽可能多的了解以下知识:

1. struts2是怎么运作的
2. Java的反射机制和Java的类加载机制和Java的动态代理
3. Ognl表达式(划重点,就是这玩意导致的漏洞)  
4. IDEA调试方法

3. 漏洞分析

apache通告来看

altSyntax属性开启后,允许OGNL表达式插入到文本字符串中,并且会被递归执行,并且强调了是因为递归执行字符串,而且由于表单的验证错误,usernamepassword处理后的结果会被回显给用户,那么这里就有两个问题:

  1. 为什么是递归执行会导致rce
  2. 这里的validation error,怎么理解?

换揣着这两个疑惑,我们开始看代码:
首先看看Ognl表达式是如何执行命令的:

再来看看,这里漏洞点的位置:

可以看到payload的写法就是Ognl表达式的语法,继续看代码,先给LoginAction.java下断点:

用debug的方式启动tomcat,在输入过用户名密码后,对代码进行step over直到com/opensymphony/xwork2/DefaultActionInvocation.class#171step into跟进this.executeResult()

然后step over直到com/opensymphony/xwork2/DefaultActionInvocation.class#248,step into跟进this.result.execute(this)

跟到org/apache/struts2/dispatcher/StrutsResultSupport.class#58step into跟进this.doExecute(this.lastFinalLocation, invocation)

继续step over/Users/youncyb/IdeaProjects/share/s2-001/web/WEB-INF/lib/struts2-core-2.0.1.jar!/org/apache/struts2/dispatcher/ServletDispatcherResult.class#48,可以看到通过dispatcher.forward(request, response)对request的请求内容进行处理,我们继续step into跟进这个函数,这里一直step into,要花费很久的时间,函数栈如下:

到达org/apache/struts2/views/jsp/ComponentTagSupport.class#23step into跟进this.component.end(this.pageContext.getOut(), this.getBody()),然后跟入web/WEB-INF/lib/struts2-core-2.0.1.jar!/org/apache/struts2/components/UIBean.class#72

一路step over来到/Users/youncyb/IdeaProjects/share/s2-001/web/WEB-INF/lib/struts2-core-2.0.1.jar!/org/apache/struts2/components/UIBean.class#272

可以看到开启了altSyntax后,会对传入的数据加上%{},而我们传入的password,就会变成%{password},而这时的expr还是%{password},我们继续跟进this.addParameter("nameValue", this.findValue(expr, valueClazz))

跟进TextParseUtil.translateVariables('%', expr, this.stack)

跟进translateVariables(open, expression, stack, String.class, (TextParseUtil.ParsedValueEvaluator)null).toString()

开始对expr进行处理,取出%{}中的数据,即password,然后传入stack.findValue(var, asType)进行处理,我们继续跟进:

在这里,就可以看到OgnlUtil.getValue(expr, this.context, this.root, asType),一个标准的OGNL取值表达式,而此时的expr='password',即取出password对应的数据%{1+1},然后继续返回translateVariables这个函数中的循环:

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

可以看到此时传入stack.findValue(var, asType)时,var变成了1+1,而执行后结果变成了2,完成了命令执行,再次思考提出的问题1,不难看出递归执行就在于com/opensymphony/xwork2/util/TextParseUtil.class的while循环中,其会循环解析%{}符号,直到字符中不存在此字符串。

思考第二个问题:为何validation error也是漏洞形成的原因之一?(此处分析,和chybeta师傅的分析相反)

查看官方文档,可以看到验证器的有一个特别的作用:

如果在default interceptor stack中,如果出现验证错误,则会将输入返回到页面,并且将将用户带回到表单页面

继续思考,程序中我们也没有设置相关的validators.xml,这个validation error和我们有什么关系?

如果你仔细阅读官方文档,则会看到如下一段:

default interceptor stack会自动开启validation,并且从代码的调用栈我们也可以看到:

会调用默认的ValidationInterceptor

所以我们的命令执行后会被回显到index.jsp

4. 与struts2.0.9的补丁对比

可以看到,在再次处理expression,会对循环进行判断,loopCount > maxLoopCount, 我们输入的%{1+1},就会被原原本本显示为%{1+1}

5. 参考

chybeta师傅的s2-001代码执行分析