JAVA反序列化
java序列化会将数据转化为二进制流进行储存,反序列化时会将二进制流还原为原始数据结构或对象。java反序列化漏洞通常是由于应用程序在反序列化数据时没有进行充分的验证,导致攻击者可以构造恶意的序列化数据,从而执行任意代码执行
java编程基础
java本身就是面向对象的编程语言,此语言有以下主要特点:
- 大小写敏感:Java 是大小写敏感的,这就意味着标识符 Hello 与 hello 是不同的
- 类名:对于所有的类来说,类名的首字母应该大写。如果类名由若干单词组成,那么每个单词的首字母应该大写(驼峰命名法),例如 MyFirstJavaClass
- 方法名:所有的方法名都应该以小写字母开头。如果方法名含有若干单词,则后面的每个单词首字母大写
- 源文件名:源文件名必须和类名相同。当保存文件的时候,你应该使用类名作为文件名保存(切记 Java 是大小写敏感的),文件名的后缀为 .java。(如果文件名和类名不相同则会导致编译错误)
- 主方法入口:所有的 Java 程序由 public static void main(String[] args) 方法开始执行
入口方法解释图解,类的创建也差不多是这种形式:
访问控制修饰符有四种: default, public, protected, private
- default: 没有修饰符,表示默认访问权限,只能在同一个包(文件夹)内访问
- public: 公有,可以在任何地方访问
- protected: 受保护,可以在同一个包内访问,也可以在不同包的子类中访问
- private: 私有,只能在类的内部访问,不能被继承
直接写一个java文件
public class test2{
private String cartoon;
protected String character; //预定义变量
public test2(String cartoon,String character){
this.cartoon = cartoon;
this.character = character;
} // 构造方法(Constructor),构造方法的名字必须和类名相同,且没有返回值类型
public void myFavoriteCharacter(){
System.out.println("我最喜欢的角色是"+ cartoon + "里的" + character);
} // 普通方法(Method),方法名可以随意命名,必须有返回值类型,如果没有返回值则写void
public static void main(String[] args) {
test2 Cartoon = new test2("探险活宝","Jack");
test3 Cartoon_2 = new test3("Finn");
Cartoon.myFavoriteCharacter();
Cartoon_2.showExtraCharacter();
} // 程序入口,写在类里但不属于该类成员
}
class test3 extends test2{
protected String ExtraCharacter;
public test3(String cartoon, String character) {
super(cartoon, character);
} // 继承必须构造,与 this 类似,super 相当于是指向当前对象的父类,这样就可以用super.xxx来引用父类的成员
public test3(String ExtraCharacter){
super("探险活宝", "主角色"); // 接口应赋值继承的变量
this.ExtraCharacter = ExtraCharacter;
}
public void showExtraCharacter(){
System.out.println("还有" + ExtraCharacter);
}
}
可以看到java和php还是有很大不同的:java一个文件只能定义一个public类;入口对象都必须写在类里而不是外部;继承时需要调用父类的构造函数;方法调用时要指定对象和方法名之间的点号"."而不是箭头"->"
数组在java这样表示
String[] arr = {"apple", "banana", "cherry"};
int[] numbers = {1, 2, 3, 4, 5};
int[] numbers = new int[5]; // 创建一个长度为5的整数数组,默认值为0
String[] arr = new String[3]; // 创建一个长度为3的字符串数组,默认值为null
for (int n : numbers) {
System.out.println(n);
} // 增强型 for 循环,遍历数组
静态方法
静态方法是属于类而不是实例的方法,可以直接通过类名调用,无需创建对象
User.say();
泛型
定义List(盒子)可以装什么元素
List<String> list = new ArrayList<>();
list.add("Hello");
List<?> list2 = new ArrayList<>(); // <?>叫做叫做泛型通配符,表示未知类型,可以接受任何类型的元素
我们可以利用Arrays.asList()将元素封装成一个List对象
List<String> list = Arrays.asList("apple", "banana", "cherry");
前置知识
1. java平台三部分:
JDK:面向开发者,用于编写、编译和调试Java程序
JRE:面向普通用户,仅用于运行已编译好的Java程序
JVM:是执行Java字节码的虚拟机,负责将字节码转换为机器码并执行,提供了内存管理、垃圾回收等核心功能,是实现Java跨平台特性的基础
2. 映射和反射
映射:
ORM(对象关系映射):
通俗讲就是将数据库的结构转移到java对象结构上,简化数据库操作
数据库的表(table) --> 类(class)
记录(record,行数据)--> 对象(object)
字段(field)--> 对象的属性(attribute)
json映射:
通过json映射库将json数据转换为java对象,或者将java对象转换为json数据,也属于反序列化的一种形式,之后详细讲讲
反射:
把通过new对象并且调用其中的方法的过程叫做“正射”,那么不使用new来创建对象并调用其中方法的过程就叫做“反射”
3. IO流
java包括两种IO流:文件IO流和对象IO流文件IO流:
- FileOutputStream
输出流,向文件写入数据
核心方法有
write(byte[] b) //将字节数组写入文件
close() //关闭流,释放资源
- FileInputStream
输入流,从文件读取数据
核心方法有
read(byte[] b) //从文件读取数据到字节数组
available() //返回可读取的字节数
对象IO流:
- ObjectOutputStream
对象输出流,即序列化。递归地检查对象及其引用的所有属性,将它们转换成 Java 标志性的 AC ED 00 05 开头的字节序列
核心方法有
writeObject(Object obj) //将对象写入流中进行序列化
defaultWriteObject() //只能写在private void writeObject()中,除了自定义序列化,对其他属性进行默认序列化
- ObjectInputStream
对象输入流,即反序列化。读取由 ObjectOutputStream 写入的字节流,并将其转换回原始对象
核心方法有
readObject() //从流中读取对象进行反序列化
defaultReadObject() //只能写在private void readObject()中,除了自定义反序列化,对其他属性进行默认反序列化
default方法这样表现
// 自定义序列化过程 (魔术方法)
private void writeObject(ObjectOutputStream out) throws IOException {
// 1. 一键全自动:调用 defaultWriteObject()
out.defaultWriteObject();
// 2. 手动半自动:处理特殊逻辑
String encryptedPwd = encrypt(this.password);
out.writeObject(encryptedPwd); // 把加密后的密码单独存进去
}
4. 依旧魔术方法
readObject()方法:
反序列化起点,当使用ObjectInputStream.readObject()触发,相当于__wakeup()方法
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { ... }
writeObject()方法:
使用ObjectOutputStream.writeObject(obj)序列化时触发,相当于__sleep()方法
private void writeObject(ObjectOutputStream out) throws IOException { ... }
writeReplace()方法:
当一个对象被序列化时,如果该对象的类定义了writeReplace()方法,那么在序列化之前会调用这个方法来获取一个替代对象进行序列化。相当于在序列化时替换对象
private Object writeReplace() throws ObjectStreamException { ... }
readResolve()方法:
当一个对象被反序列化时,如果该对象的类定义了readResolve()方法,那么在反序列化之后会调用这个方法来获取一个替代对象进行返回。相当于在反序列化时替换对象
private Object readResolve() throws ObjectStreamException { ... }
反序列化利用
其实看完前置知识你已经知道java反序列化原理了,和php差不多。这一节讲一讲如何利用反序列化
反射利用
- Class对象
java里的类其实也是个对象,在编译时java.lang就会实例化一个名为Class的对象,里面储存了该类的方法和属性等信息,我们可以通过反射来调用这些方法。(注:Class也是泛型类,一般用Class<?>)
Class.forName(classname) // 获取classname类中的所有属性包括类名
Class.newInstance() // 实例化classname类,并触发该类的构造方法
Class.getMethod(method name,arg) // 获取classname类中名为method name的public方法,arg是参数类型
Class.getConstructor(List.class) // 获取classname类中的有参构造方法,参数是List类型
Method.invoke(object, args) // 调用object对象中的方法,args是参数值;如果是静态方法则第一个参数是null或者该方法所在的类
obj.getClass() // 获取obj对象的Class对象,前提是上文中obj必须是一个实例化对象
Y1.class // 获取Y1类的Class对象,前提是上文中必须有一个名为Y1的类
演示
import java.lang.reflect.Method;
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz1 = Class.forName("HackerPlayer");
System.out.println("forName 获取成功: " + clazz1.getName());
Class<?> clazz2 = HackerPlayer.class;
System.out.println("类名.class 获取成功: " + clazz2.getName());
// 前提是你手里已经有一个活生生的对象实例了
HackerPlayer existingObj = new HackerPlayer();
Class<?> clazz3 = existingObj.getClass();
System.out.println("对象.getClass() 获取成功: " + clazz3.getName());
// 注意:在 Java 9 之后这个方法被标记为过时
Object playerObj = clazz1.newInstance();
// 获取名为 "play",且需要一个 String 类型参数的方法
Method playMethod = clazz1.getMethod("play", String.class);
// 调用 playerObj 对象里的 play 方法,并传入参数 "127.0.0.1"
playMethod.invoke(playerObj, "127.0.0.1");
// 获取静态方法 "ping",因为它没有参数,所以第二个参数不写(或写 null)
Method pingMethod = clazz1.getMethod("ping");
// 第一个参数(执行主体)直接传 null 即可
pingMethod.invoke(null);
}
}
- forName()更深层利用
Class.forName()方法不仅可以获取类的Class对象,还会触发该类的静态代码块(static block)的执行
它的完成源码如下:
Class.forName(String name, boolean initialize, ClassLoader loader)
Class loader是java中的类加载器,负责告诉JVM从哪里加载类文件,一般填路径
boolean initialize用于forname时的初始化(执行静态代码块),默认为true。也就是说,如果提前也好一个包含恶意代码静态代码块的类,并且在反序列化时调用forName()方法加载这个类,那么就可以直接触发恶意代码的执行,而不需要构造复杂的pop链了
public class EvilClass {
// 这是一个静态代码块,没有名字,没有参数
static {
try {
// 恶意代码
Runtime.getRuntime().exec("calc");
} catch (Exception e) {}
}
}
- runtime()的rce
Runtime类是java.lang中一个非常重要的类,提供了与运行时环境交互的方法,其中最常用的就是exec()方法,可以用来执行系统命令
但如果我们用上述方法,Class.getMethod(method name,arg)获取
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.newInstance(), "id");
就会报错,前面我们说过.invoke第一个参数是对象而newInstance()负责实例化对象并触发构造方法。但Runtime类的构造方法是私有的,只能在类内部被调用,所以无法通过newInstance()来实例化Runtime对象,从而报错
public class Runtime {
private static Runtime currentRuntime = new Runtime();
private Runtime() {}
public static Runtime getRuntime() {
return currentRuntime; // 返回当前Runtime对象实例,关键利用点
}
// ...
}
这时我们就可以利用反射,用下面的静态方法getRuntime()来获取Runtime对象实例,然后再调用exec()方法执行命令:
// 1. 拿到class对象
Class clazz = Class.forName("java.lang.Runtime");
// 2. 找到那个静态的小窗户方法 getRuntime
Method getRuntimeMethod = clazz.getMethod("getRuntime");
// 3. 执行 getRuntime 拿到对象实例
Object runtimeObj = getRuntimeMethod.invoke(null);
// 4. 拿着拿到的对象,去执行 exec("id")
clazz.getMethod("exec", String.class).invoke(runtimeObj, "id");
还有两种反射机制
一个是ProcessBuilder:ProcessBuilder用于创建操作系统进程,它提供一种启动和管理进程的方法,我们可以通过实例化这个类并且通过反射调用其中的start()方法来开启一个子进程,进行rce
public ProcessBuilder(String... command)
// String...指代可变参数,即可以传入任意数量的字符串参数打包成String数组
// ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", "whoami").start();
public ProcessBuilder(List<String> command)
// List<String>指代一个字符串列表,等价于上面那个方法
'''
List<String> cmdList = new ArrayList<>();
cmdList.add("cmd.exe");
cmdList.add("/c");
cmdList.add("ping 127.0.0.1");
ProcessBuilder pb = new ProcessBuilder(cmdList);
pb.start();
'''
就有如下payload
Class clazz = Class.forName("java.lang.ProcessBuilder");
Object obj = clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"));
ProcessBuilder pb = (ProcessBuilder) obj;
pb.start();
forName接手了ProcessBuilder类的加载。getConstructor(List.class)获取了ProcessBuilder类的有参构造方法并通过newInstance()传参并实例化clazz。()强制将obj转化为ProcessBuilder对象,最后调用start()方法执行命令
第二个是getDeclared
这个方法很厉害,它可以获取类中所有的成员方法和成员变量,包括私有的和受保护的(但不包括父类)
它有两个形式
Class.getDeclaredMethod() // 用法同getMethod(),但可以获取所有方法包括私有方法
Class.getDeclaredConstructor() // 用法同getConstructor(),但可以获取所有构造方法包括私有构造方法
但访问私有成员需要调用setAccessible(true)方法来取消访问检查,否则会报IllegalAccessException异常,利用这个方法也可以调用Runtime
