当前位置: 首页 > news >正文

Java漏洞原理与实战

一、基本概念

1、序列化与反序列化

(1)序列化:将对象写入IO流中,ObjectOutputStream类的writeobject()方法可以实现序列化

(2)反序列化:从IO流中恢复对象,ObjectinputStream类的readObject()方法用于反序列化

(3)意义:序列化机制允许将实现序列化的Java对象转换为字节序列,这些字节序列可以保存到磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在

(4)序列化与反序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化储存。主要应用在以下场景:

HTTP:多平台之间的通信,管理等,也可以用于流量带外

RMI:是Java的一组拥护开发分布式应用程序的API,实现了不同操作系统之间程序的方法调用。值得注意的是,RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口

JMX:JMX是一套标准的代理和服务,用户可以在任何Java应用程序中使用这些代理和服务实现管理,中间件weblogic的管理页面就是基于JMX开发的,而JBoss则整个系统都基于JMX框架

(5)Java代码审计思路

如果是Java原生类,则需要入口类readObject方法,同时实现了序列化接口,使其可以进行有效的反序列化,此时如果存在DNS解析,或者实现反序列化(利用Runtime对象进行类反射操作)

需要有最终的执行函数(可以执行代码或者命令),比如Runtime.getRuntime().exec,ProcessBuilder().start,getHostAddress,文件读写...等等,这些函数需要自己平常去收集,这样审计起来会更得心应手

2、Java类反射机制

(1) 反射机制的作用:通过Java语言中的反射机制可以操作字节码文件(可以读和修改字节码文件),可以通过另外的方式调用到类的属性和方法,甚至私有属性和方法

(2)反射机制的相关类在java.lang.reflect.*包下面

(3)反射机制的相关类有哪些:Constructor、Field、Method、Class等类

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

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

(6)Java中为什么要使用反射机制,直接创建对象不是更方便?

如果有多个类,每个用户所需求的对象不同,直接创建对象,就要不断的去new一个对象,非常不灵活。而Java反射机制,在运行时确定类型,绑定对象,动态编译最大限度发挥了java的灵活性

(7)获取成员变量

(8)获取并调用方法

(9)获取构造方法

(10)访问私有属性

二、类反射机制实践

package com.woniu.vul;import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;class Test{public String name = "蜗牛学苑";public int age = 8;private String addr = "西安";private int price = 10000;public Test() {}public Test(int price){this.price = price;}public int setPrice(int price){System.out.println("新价格为:" + price);return price;}public int getPrice(){return this.price;}private void getAddr(){System.out.println("私有方法:" + addr);}}public class Reflect {public static void main(String[] args) throws Exception {/*Test t = new Test();System.out.println(t.getPrice());Test t1 = new Test(15000);System.out.println(t1.getPrice());System.out.println(t.name);*///使用反射机制实现属性和方法的调用(包括构造方法)//使用Class.forName可以获取到类本身,在JVM中动态加载Test类//Class clazz = Class.forName("com.woniu.vul.Test");//Class clazz = Test.class;//使用new Instance进行实例化//Test t = (Test) clazz.newInstance();//System.out.println(t.getPrice());//Object o = clazz.newInstance();  //实例化动态加载的类,类型必须是Object//        Method m1 = clazz.getMethod("getPrice");
//        int price1 = (int)m1.invoke(o,null);
//        System.out.println(price1);
//
//        Method m2 = clazz.getMethod("setPrice",int.class);
//        int price2 = (int)m2.invoke(o,15000);
//        System.out.println(price2);//调用price私有属性和getAddr私有方法,getFiled只能调用公有属性,getDeclareField才能调私有属性//Field f1 = clazz.getDeclaredField("price");//f1.setAccessible(true);  //设置私有属性可访问//System.out.println(f1.get(o));//getMethod只能调用公有方法,而getDeclareMethod才能嗲用私有方法//Method m1 = clazz.getDeclaredMethod("getAddr");//m1.setAccessible(true);//m1.invoke(o,null);//构造方法如果有参数,怎么办?Class clazz = Class.forName("com.woniu.vul.Test");Constructor c = clazz.getConstructor(int.class);  //获取到一个带参数的构造器Object o = c.newInstance(10); //用构造器去构造一个动态加载的类Method m1 = clazz.getDeclaredMethod("getPrice");int price = (int) m1.invoke(o,null);System.out.println(price);//遍历所有方法或操作Method[] methods = clazz.getDeclaredMethods();for (Method method : methods) {System.out.println(method + "   " + method.getName() + "    " + method.getModifiers());}Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {System.out.println(field + "   " + field.getName() + "    " + field.getModifiers());}}
}

三、序列化与反序列化

序列化的实现代码

package com.woniu.vul;import java.io.*;class Student implements Serializable {public String name = "";public int id = 0;public String phone = "";public Student(){System.out.println("构造方法运行");}public void study(){System.out.println("学生正在学习");}public void sleep(){System.out.println("学生正在休息");}
}public class Unserial {public void serial() throws Exception {Student s = new Student();s.name = "张三";s.id = 12345;s.phone = "188123456786";FileOutputStream fos = new FileOutputStream("./data/student.ser");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(s);}public void unserial() throws Exception {FileInputStream fis = new FileInputStream("./data/student.ser");ObjectInputStream ois = new ObjectInputStream(fis);Student obj =(Student) ois.readObject();System.out.println(obj.name);obj.study();}public static void main(String[] args) throws Exception {Unserial us = new Unserial();//us.serial();us.unserial();}
}

序列化的内容如下:

 然后进行反序列化,切记不要去改我们的序列化内容,不然无法反序列化回去

 我们再补充两个小问题

正常情况下,高亮部分是可以被序列化的,但是如果在这之前加上transient来修饰的话就无法序列化

高亮部分的意思就是定义一个序列化版本的编号,也就是唯一标识

 

这个标识是用来干嘛的

接下来我们看看

我们重新反序列化看看效果

报错信息告诉我们是一个不可用的类

为什么不可用,让我们继续看报错信息

序列化的时候类的ID是前面的那一个,但是反序列的时候类的ID是后面那一个,序列化和反序列化的时候标识号是不一样的,意思就是这个类并不是我们需要反序列化的类

我们将代码中的ID改为其序列化时候原本的ID,那我们就可以完成反序列化的操作了

也就是说这个类在序列化的时候会记录下其类的标识UID

接下来我们看看反序列化产生的机制

首先这个序列化对象一旦有重写的方法,那我们在反序列化的时候会优先调用重写的readObject


 因为我们重写了readObject,所以就会先调用readObject,这就是Java反序列化的起点,也是唯一的起点

也就是说Java反序列化漏洞能够被利用,我们得有一个最基本的前提,就是目标类必须重写readObject方法,只有这样,代码才会被自动调用,否则就没有起点

如果不重写readObject方法的话,就不会发生Java反序列化漏洞

而我们的代码已经重写了readObject方法,所以我们可以对其利用

我们可以直接在重写方法的下面加上终点,也就是攻击者想要达到的效果,有始有终,整个攻击链才算完整

当然我们也可以使用类反射机制的手段去执行命令

因为getRuntime的实例不是纯粹的new出来的,而是通过调用getRuntime这个方法来获取其实例的,然后再通过这个实例去调用exec

加载java.lang.Runtime类

获取Runtime类中的getRuntime方法

调用Runtime方法,获取Runtime类的实例

获取Runtime类中的exec(string)方法

调用exec(String)方法,运行外部命令ifconfig

 运行代码,发现没有报错,说明应该是利用成功了,我不知道为啥不会显示执行ifconfig命令的内容,如果是Windows的话,可以将ifconfig改为calc.exe,大概率会显示出计算器

当然为了执行命令,不仅仅只有Runtime,还有ProcessBuilder

接下来我们看看其反射的调用

根据正常的调用来构造反射

先使用Class.forName这个方法来加载java.lang.ProcessBuilder这个类

然后使用Class对象的getConstructor来获取Processbuilder类的构造函数

接着使用Constructor对象的newInstance方法来创建ProcessBuilder的实例

然后使用Class对象的getMethod方法去获取ProcessBuilder中的start方法

最后使用Method对象的invoke方法去调用start方法

 然后运行,发现报错,是类型出现了错误

 我们先去看看ProcessBuilder的构造方法,它不是严格意义上的String,是String...(可变长的字符串)如果是whoami /user这条命令的话,我们得写到两个字符串里面,在Java中,对于可变长的字符串是将其放入到数组当中去

 然后我们将其修改为String[].class

然后继续运行,然后还是报错说类型不匹配

就是因为我们上面定义的是数组,下面是字符串,所以会报错

所以我们要将下面的类型转换为数组类型就可以了,也就是将其放到数组中就可以了,如下

继续运行,发现还是报错,报错信息还是类型不匹配

 我们去看看newInstace的构造方法,发现还是一个数组,它的类型是数组,数组里面的参数还是一个数组

所以这个cmd的类型要定义成二维数组,二维数组只需加两个{}即可,如下

然后去运行,发现没有报错,但是也没有回显命令的内容,应该是电脑的问题,如果是Windows的话在命令那一块改为calc.exe就可以打开计算器了

 

相关文章:

  • RT-DETR源码学习bug记录
  • 51单片机实验七:EEPROM AT24C02 与单片机的通信实例
  • 【系统架构设计师】统一过程模型(RUP)
  • python 对接支付宝账单流程及问题处理
  • 告别Feign:基于Spring 6.1 RestClient构建高可用声明式HTTP客户端
  • VUE快速入门-4:简单入门案例
  • postman使用设置
  • ChatGPT-o3辅助学术写作的关键词和引言效果如何?
  • 解锁古籍中的气候密码,探索GPT/BERT在历史灾害研究中的前沿应用;气候史 文本挖掘 防灾减灾;台风案例、干旱案例、暴雨案例
  • 面试面试面试new
  • docker 安装prometheus普罗米修斯
  • 疑难问题解决(2)
  • git常用的命令
  • CSS 美化页面(五)
  • PD分离:优化大语言模型推理效率
  • MDA测量数据查看器【内含工具和源码地址】
  • ARINC818协议的帧格式
  • ARINC818协议(五)
  • Superduper - 在数据上构建端到端AI工作流和应用
  • AI问答收集
  • 云南:近两千亩拍得土地因规划变更不得开发,政府要求转型、企业无力为继
  • 杜甫、韦应物背后的世家大族,在这个展览上一览传奇
  • 男子拍摄女性视频后在网上配发诱导他人违法犯罪文字,已被警方行拘
  • 农业农村部原党组书记、部长唐仁健被提起公诉
  • 青岛:多孩家庭购房最高补贴10万元,推出青年群体“低月供”住房贷款金融产品
  • 国家药监局通告18批次化妆品检出禁用原料,含婴儿护肤霜