M0deration's blog.

java反序列化-反射篇

字数统计: 3k阅读时长: 12 min
2022/02/26 Share

读p牛的java安全漫谈笔记

01 反射篇

p牛对于动态特性的解释

⼀段代码,改变其中的变量,将会导致 这段代码产⽣功能性的变化,我称之为动态特性

比如php中的一句话

1
<?php eval($_POST[1]); ?>

就可以通过post传入1执行其他的功能,在没传入值之前你也不知道他要干啥

下面看一下java的

1
2
3
4
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}

获取类 的⽅法: forName

实例化类对象的⽅法(调用该类的无参构造函数): newInstance

获取 函数的⽅法: getMethod

getMethod 的作用是通过反射获取一个类的某个特定的公有方法

执⾏函数的⽅法: invoke

这里感觉有点像python的获取基类的那种感觉,在另一篇先知社区的文章里,把forName的方法叫做获取类对象

有三种获取类对象的方法

  • class.forName(“”)
  • obj.class

    如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接 拿它的 class 属性即可。这个⽅法其实不属于反射

  • obj.getClass()

    有某个类实例后对象,可以通过这种方法

    1
    2
    3
    4
    Integer i=1;
    Class runtimeClass=i.getClass();
    System.out.println(runtimeClass);
    //class java.lang.Integer

    然后再通过forName方法就可以获取执行命令的java.lang.Runtime

    1
    2
    3
    4
    5
    Integer i=1;
    Class runtimeClass=i.getClass().forName("java.lang.Runtime");
    System.out.println(runtimeClass);

    //class java.lang.Runtime

forName(“”)

forName有两个函数重载:

  • Class forName(String name)
  • Class forName(String name, boolean initialize, ClassLoader loader)

ClassLoader它就是⼀个“加载器”,告诉Java虚拟机如何加载这个类。关于这个点,后⾯还 有很多有趣的漏洞利⽤⽅法,这⾥先不展开说了。Java默认的 ClassLoader 就是根据类名来加载类, 这个类名是类完整路径,如 java.lang.Runtime

1
2
3
4
5
6
7
8
9
10
11
public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
}

⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数。
其中, static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯,
但在当前构造函数内容的前⾯。
所以说, forName 中的 initialize=true 其实就是告诉Java虚拟机是否执⾏”类初始化“。
那么,假设我们有如下函数,其中函数的参数name可控:
我们就可以编写⼀个恶意类,将恶意代码放置在 static {} 中,从⽽执⾏

1
2
3
4
5
6
7
8
9
10
Person p = new Person("zhangsan",20);
该句话都做了什么事情?
1,因为new用到了Person.class.所以会先找到Person.class文件并加载到内存中。
2,执行该类中的static代码块,如果有的话,给Person.class类进行初始化。
3,在堆内存中开辟空间,分配内存地址。
4,在堆内存中建立对象的特有属性。并进行默认初始化。
5,对属性进行显示初始化。
6,对对象进行构造代码块初始化。
7,对对象进行对应的构造函数初始化。
8,将内存地址付给栈内存中的p变量。
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
/**
* @Author M0deration
* @Date 2021/12/31 9:05
*/
public class test {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("eval");
}
}


//恶意类
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
* @Author M0deration
* @Date 2021/12/31 9:06
*/
public class eval {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"ipconfig", "/all"};
Process pc = rt.exec(commands);
InputStream fis=pc.getInputStream();
//用一个读输出流类去读
InputStreamReader isr=new InputStreamReader(fis, "gbk");
//用缓冲器读行
BufferedReader br=new BufferedReader(isr);
String line=null;
//直到读完为止
while((line=br.readLine())!=null)
{
System.out.println(line);
}
pc.waitFor();
} catch (Exception e) {
System.out.println(e);
// do nothing
}
}
}

02.反射篇(2)

在看这篇之前先学习一下链式编程,如果了解直接跳过,不了解看一下方便后面理解

java链式编程

参考:https://www.jianshu.com/p/a8c51f220189

链式编程比较简单,只要return this即可具有相应的编程模式

只介绍简单的链式编程

简单链式编程

使用return this可以实现链式

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
public class Book {
private String bookId;
private String title;
private String cover;

public String getBookId() {
return bookId;
}

public Book setBookId(String bookId) {
this.bookId = bookId;
// 返回当前对象
return this;
}

public String getTitle() {
return title;
}

public Book setTitle(String title) {
this.title = title;
// 返回当前对象
return this;
}

public String getCover() {
return cover;
}

public Book setCover(String cover) {
this.cover = cover;
// 返回当前对象
return this;
}
@Override
public String toString() {
return "Book{" +
"bookId='" + bookId + '\'' +
", title='" + title + '\'' +
", cover='" + cover + '\'' +
'}';
}
}

调用

1
2
3
4
5
public static void main(String[] args) {
Book book = new Book().setBookId("b.001").setTitle("庆余年").setCover("http://localhost/qyn.jpg");
System.out.println(book);
}
// Book{bookId='b.001', title='庆余年', cover='http://localhost/qyn.jpg'}

反射

反射1提到可以通过forName来获取类,获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法

class.newInstance()可以调用类的无参构造函数,但是很多时候这样调用并不能成功,可能的原因

  1. 你使用的类没有无参构造函数
  2. 使用的类,它的构造函数是私有的

最后payload用的比较多的就是java.lang.Runtime

但是不能通过newInstance利用

1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

原因是 Runtime 类的构造方法是私有的,构造函数私有主要是单例模式会使用

例如jdbc连接数据库只希望初始化建立一次,就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取

1
2
3
4
5
6
7
8
9
public class TrainDB {
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}

这样,只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance 获取这个对象,避免建立多个数据库连接

Runtime类也是单例模式,所以无法通过getInstance 获取对象,只能通过 Runtime.getRuntime() 来获取到 Runtime 对象

1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");

getMethod

getMethod 是通过反射获取一个类的公有方法,但是java通过重载可以出现几个相同函数名但是不同参数的函数,无法通过函数名确定函数,所以调用 getMethod 的时候,需要传给他你需要获取的函数的参数类型列表

invoke

invoke作用是执行方法,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数是类

正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是
method.invoke([1], [2], [3], [4]...)

[类].静态方法(参数)===》静态方法.([类],参数)

[对象].普通方法(参数)===》普通方法.([对象],参数)

因为需要使用的exec方法是普通方法,需要得到Runtime对象,但是Runtime构造函数私有(通过类内的构造函数得到类对象),所以通过静态方法getRuntime来获取类对象

上面简写的payload可以拆分为

1
2
3
4
5
6
7
        Class clazz = Class.forName("java.lang.Runtime");
Method execMethod =clazz.getMethod("exec", String.class);
// java.lang.Runtime无参构造函数私有,通过静态函数getRuntime获取类对象
Method getRuntimeMethod = clazz.getMethod("getRuntime");
// 获取类对象,类实例化是对象
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime,"calc.exe");

03.反射篇(3)

反射2解决的是类里面构造函数私有,利用该类中单例模式静态方法实现方法的调用

这篇解决这两个问题

  • 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
  • 如果一个方法或构造方法是私有方法,我们是否能执行它呢?

问题一

想要解决第一个问题需要用到一个新的反射方法getConstructor

和反射2用的Runtime类不同的是

Runtime类有私有无参构造函,找到静态函数getRuntime获取类对象

getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数

获取构造函数后使用newInstance执行

另一种常用的执行命令的方式ProcessBuilder使用反射来获取其构造函数,然后调用start() 来执行命令

1
2
3
Class clazz = Class.forName("java.lang.ProcessBuilder");

((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

ProcessBuilder有两个构造函数

1
2
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)

上面payload用到的是第一个形式的构造函数,所以在getConstructor()时候传入list.class

前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射用invoke方法来完成这一步

1
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

通过getMethod("start") 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是ProcessBuilder Object

第二个构造函数public ProcessBuilder(String... command)参数是可变长参数

对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的

因此我们只要传个数组就可以了

将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二种构造函数

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下

1
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();

改成反射来写

1
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));

问题二

一个方法或构造方法是私有方法,涉及到 getDeclared 系列的反射了,与普通的 getMethod 、 getConstructor 区别是:

  • getMethod系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
  • getDeclaredMethod系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了

getDeclaredMethod的具体用法和getMethod类似,getDeclaredConstructor的具体用法和getConstructor类似

getDeclaredMethod遇到的问题

之前在反射2中的Runtime类的构造方法就是私有的,这里基于可以用getDeclaredMethod来解决

这里有个问题用getDeclaredMethods方法并没有获取到私有的构造方法

1
2
3
4
Method[] me = clazz.getDeclaredMethods();
for(int i=0;i<me.length;i++){
System.out.println(me[i]);
}

这里java基础太弱了,没有找到原因,哪个大表哥知道可以联系我呀

Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime()来获取对象。其实现在我们也可以直接用 getDeclaredConstructor来获取这个私有的构造方法来实例化对象,进而执行命令:

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

这里使用了一个方法setAccessible,这个是必须的。我们在获取到一个私有方法后,必须用setAccessible修改它的作用域,否则仍然不能调用

CATALOG
  1. 1. 01 反射篇
    1. 1.1. forName(“”)
  2. 2. 02.反射篇(2)
    1. 2.1. java链式编程
      1. 2.1.1. 简单链式编程
    2. 2.2. 反射
      1. 2.2.1. getMethod
      2. 2.2.2. invoke
  3. 3. 03.反射篇(3)
    1. 3.1. 问题一
    2. 3.2. 问题二
      1. 3.2.1. getDeclaredMethod遇到的问题