一、基本概念
1、序列化与反序列化
(1)序列化:将对象写入IO流中,ObjectOutputStream类的writeobject()方法可以实现序列化
(2)反序列化:从IO流中恢复对象,ObjectinputStream类的readObject()方法用于反序列化
(3)意义:序列化机制允许将实现序列化的Java对象转换为字节序列,这些字节序列可以保存到磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在
(4)序列化与反序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化储存。主要应用在以下场景:
HTTP:多平台之间的通信,管理等,也可以用于流量带外
RMI:是Java的一组拥护开发分布式应用程序的API,实现了不同操作系统之间程序的方法调用。值得注意的是,RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口
JMX:JMX是一套标准的代理和服务,用户可以在任何Java应用程序中使用这些代理和服务实现管理,中间件weblogic的管理页面就是基于JMX开发的,而JBoss则整个系统都基于JMX框架
(5)Java代码审计思路
如果是Java原生类,则需要入口类readObject方法,同时实现了序列化接口,使其可以进行有效的反序列化,此时如果存在DNS解析,或者实现反序列化(利用Runtime对象进行类反射操作)
需要有最终的执行函数(可以执行代码或者命令),比如Runtime.getRuntime().exec,ProcessBuilder().start,getHostAddress,文件读写...等等,这些函数需要自己平常去收集,这样审计起来会更得心应手
2、Java类反射机制
(1) 反射机制的作用:通过Java语言中的反射机制可以操作字节码文件(可以读和修改字节码文件),可以通过另外的方式调用到类的属性和方法,甚至私有属性和方法
(2)反射机制的相关类在java.lang.reflect.*包下面
(3)反射机制的相关类有哪些:Constructor、Field、Method、Class等类
(4)java.lang.Class代表字节码文件,代表整个类
(5)java.lang.reflect.Method代表字节码中的方法字节码,代表类中的方法java.lang.reflect.Constructor代表字节码中的构造方法字节码,代表类中的构造方法java.lang.reflect.Field代表字节码中的属性字节码,代表类中的属性
(6)Java中为什么要使用反射机制,直接创建对象不是更方便?
如果有多个类,每个用户所需求的对象不同,直接创建对象,就要不断的去new一个对象,非常不灵活。而Java反射机制,在运行时确定类型,绑定对象,动态编译最大限度发挥了java的灵活性
(7)获取成员变量
(8)获取并调用方法
(9)获取构造方法
(10)访问私有属性
二、类反射机制实践
package com.woniu.vul;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
class Test{
public String name = "蜗牛学苑";
public int age = 8;
private String addr = "西安";
private int price = 10000;
public Test() {
}
public Test(int price){
this.price = price;
}
public int setPrice(int price){
System.out.println("新价格为:" + price);
return price;
}
public int getPrice(){
return this.price;
}
private void getAddr(){
System.out.println("私有方法:" + addr);
}
}
public class Reflect {
public static void main(String[] args) throws Exception {
/*Test t = new Test();
System.out.println(t.getPrice());
Test t1 = new Test(15000);
System.out.println(t1.getPrice());
System.out.println(t.name);
*/
//使用反射机制实现属性和方法的调用(包括构造方法)
//使用Class.forName可以获取到类本身,在JVM中动态加载Test类
//Class clazz = Class.forName("com.woniu.vul.Test");
//Class clazz = Test.class;
//使用new Instance进行实例化
//Test t = (Test) clazz.newInstance();
//System.out.println(t.getPrice());
//Object o = clazz.newInstance(); //实例化动态加载的类,类型必须是Object
// Method m1 = clazz.getMethod("getPrice");
// int price1 = (int)m1.invoke(o,null);
// System.out.println(price1);
//
// Method m2 = clazz.getMethod("setPrice",int.class);
// int price2 = (int)m2.invoke(o,15000);
// System.out.println(price2);
//调用price私有属性和getAddr私有方法,getFiled只能调用公有属性,getDeclareField才能调私有属性
//Field f1 = clazz.getDeclaredField("price");
//f1.setAccessible(true); //设置私有属性可访问
//System.out.println(f1.get(o));
//getMethod只能调用公有方法,而getDeclareMethod才能嗲用私有方法
//Method m1 = clazz.getDeclaredMethod("getAddr");
//m1.setAccessible(true);
//m1.invoke(o,null);
//构造方法如果有参数,怎么办?
Class clazz = Class.forName("com.woniu.vul.Test");
Constructor c = clazz.getConstructor(int.class); //获取到一个带参数的构造器
Object o = c.newInstance(10); //用构造器去构造一个动态加载的类
Method m1 = clazz.getDeclaredMethod("getPrice");
int price = (int) m1.invoke(o,null);
System.out.println(price);
//遍历所有方法或操作
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method + " " + method.getName() + " " + method.getModifiers());
}
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field + " " + field.getName() + " " + field.getModifiers());
}
}
}
三、序列化与反序列化
序列化的实现代码
package com.woniu.vul;
import java.io.*;
class Student implements Serializable {
public String name = "";
public int id = 0;
public String phone = "";
public Student(){
System.out.println("构造方法运行");
}
public void study(){
System.out.println("学生正在学习");
}
public void sleep(){
System.out.println("学生正在休息");
}
}
public class Unserial {
public void serial() throws Exception {
Student s = new Student();
s.name = "张三";
s.id = 12345;
s.phone = "188123456786";
FileOutputStream fos = new FileOutputStream("./data/student.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s);
}
public void unserial() throws Exception {
FileInputStream fis = new FileInputStream("./data/student.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Student obj =(Student) ois.readObject();
System.out.println(obj.name);
obj.study();
}
public static void main(String[] args) throws Exception {
Unserial us = new Unserial();
//us.serial();
us.unserial();
}
}
序列化的内容如下:
然后进行反序列化,切记不要去改我们的序列化内容,不然无法反序列化回去
我们再补充两个小问题
正常情况下,高亮部分是可以被序列化的,但是如果在这之前加上transient来修饰的话就无法序列化
高亮部分的意思就是定义一个序列化版本的编号,也就是唯一标识
这个标识是用来干嘛的
接下来我们看看
我们重新反序列化看看效果
报错信息告诉我们是一个不可用的类
为什么不可用,让我们继续看报错信息
序列化的时候类的ID是前面的那一个,但是反序列的时候类的ID是后面那一个,序列化和反序列化的时候标识号是不一样的,意思就是这个类并不是我们需要反序列化的类
我们将代码中的ID改为其序列化时候原本的ID,那我们就可以完成反序列化的操作了
也就是说这个类在序列化的时候会记录下其类的标识UID
接下来我们看看反序列化产生的机制
首先这个序列化对象一旦有重写的方法,那我们在反序列化的时候会优先调用重写的readObject
因为我们重写了readObject,所以就会先调用readObject,这就是Java反序列化的起点,也是唯一的起点
也就是说Java反序列化漏洞能够被利用,我们得有一个最基本的前提,就是目标类必须重写readObject方法,只有这样,代码才会被自动调用,否则就没有起点
如果不重写readObject方法的话,就不会发生Java反序列化漏洞
而我们的代码已经重写了readObject方法,所以我们可以对其利用
我们可以直接在重写方法的下面加上终点,也就是攻击者想要达到的效果,有始有终,整个攻击链才算完整
当然我们也可以使用类反射机制的手段去执行命令
因为getRuntime的实例不是纯粹的new出来的,而是通过调用getRuntime这个方法来获取其实例的,然后再通过这个实例去调用exec
加载java.lang.Runtime类
获取Runtime类中的getRuntime方法
调用Runtime方法,获取Runtime类的实例
获取Runtime类中的exec(string)方法
调用exec(String)方法,运行外部命令ifconfig
运行代码,发现没有报错,说明应该是利用成功了,我不知道为啥不会显示执行ifconfig命令的内容,如果是Windows的话,可以将ifconfig改为calc.exe,大概率会显示出计算器
当然为了执行命令,不仅仅只有Runtime,还有ProcessBuilder
接下来我们看看其反射的调用
根据正常的调用来构造反射
先使用Class.forName这个方法来加载java.lang.ProcessBuilder这个类
然后使用Class对象的getConstructor来获取Processbuilder类的构造函数
接着使用Constructor对象的newInstance方法来创建ProcessBuilder的实例
然后使用Class对象的getMethod方法去获取ProcessBuilder中的start方法
最后使用Method对象的invoke方法去调用start方法
然后运行,发现报错,是类型出现了错误
我们先去看看ProcessBuilder的构造方法,它不是严格意义上的String,是String...(可变长的字符串)如果是whoami /user这条命令的话,我们得写到两个字符串里面,在Java中,对于可变长的字符串是将其放入到数组当中去
然后我们将其修改为String[].class
然后继续运行,然后还是报错说类型不匹配
就是因为我们上面定义的是数组,下面是字符串,所以会报错
所以我们要将下面的类型转换为数组类型就可以了,也就是将其放到数组中就可以了,如下
继续运行,发现还是报错,报错信息还是类型不匹配
我们去看看newInstace的构造方法,发现还是一个数组,它的类型是数组,数组里面的参数还是一个数组
所以这个cmd的类型要定义成二维数组,二维数组只需加两个{}即可,如下
然后去运行,发现没有报错,但是也没有回显命令的内容,应该是电脑的问题,如果是Windows的话在命令那一块改为calc.exe就可以打开计算器了