JAVA反序列化 - 反射机制
2020-01-03 10:21:15 Author: xz.aliyun.com(查看原文) 阅读量:317 收藏

推荐阅读时间:30min
全文字数:1w

前言

真正反序列化漏洞的利用,肯定需要了解java反射原理。因为java反序列化的payload大多与反射机制密切相关。
那么这篇文章就是主要讲述反射机制,算是基础知识。

除了反射机制之外,后续还基于commons-collections链最后的反射机制触发点,进行了详细的反射机制特性的绕过说明。由于它与反射机制密切相关,就放在这边进行统一归纳理解。
可以配合本人的另一篇文章commons-collections食用

java反射机制

在Java中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
让java可以在运行时,根据传入的类名字符串,去执行这个类存在的方法等。

以下从开发的角度举例了一个反射机制在开发中的作用。简单了解即可。

一个不使用反射机制的例子:

interface fruit{  
    public abstract void eat();  
}  

class Apple implements fruit{  
    public void eat(){  
        System.out.println("Apple");  
    }  
}  

class Orange implements fruit{  
    public void eat(){  
        System.out.println("Orange");  
    }  
}  

// 构造工厂类  
// 也就是说以后如果我们在添加其他的实例的时候只需要修改工厂类就行了  
class Factory{  
    public static fruit getInstance(String fruitName){  
        fruit f=null;  
        if("Apple".equals(fruitName)){  
            f=new Apple();  
        }  
        if("Orange".equals(fruitName)){  
            f=new Orange();  
        }  
        return f;  
    }  
}  
class hello{  
    public static void main(String[] a){  
        fruit f=Factory.getInstance("Orange");  
        f.eat();  
    }  

}

如果我们想要添加新的水果,就需要

  • 添加新的水果类
  • 修改Factory
  • 在main函数中使用新的水果类

修改为反射机制的代码:

interface fruit{  
    public abstract void eat();  
}  

class Apple implements fruit{  
    public void eat(){  
        System.out.println("Apple");  
    }  
}  

class Orange implements fruit{  
    public void eat(){  
        System.out.println("Orange");  
    }  
}  

class Factory{  
    public static fruit getInstance(String ClassName){  
        fruit f=null;  
        try{  
            f=(fruit)Class.forName(ClassName).newInstance();  
        }catch (Exception e) {  
            e.printStackTrace();  
        }  
        return f;  
    }  
}  
class hello{  
    public static void main(String[] a){  
        fruit f=Factory.getInstance("Reflect.Apple");  
        if(f!=null){  
            f.eat();  
        }  
    }  
}

这时候如果我们需要添加水果,只需要

  • 添加水果类
  • 在main函数中使用新的水果类即可

那么好像可以看出一点问题,如果传入的类名可控,再加上一些办法,那就不是可以调用任意类,去运行系统命令了呢。

反射机制的方法

列出在实际利用中重要的方法例子:

public void execute(String className, String methodName) throws Exception {
    Class clazz = Class.forName(className);    
    clazz.getMethod(methodName).invoke(clazz.newInstance()); 
}
  • ឴获取类:forName
  • 获取类下的函数:getMethod
  • 执行类下的函数:invoke
  • 实例化类的对象:newInstance

以下一步步使用反射机制实现Runtime.getRuntime().exec("calc.exe");这个语句

  • getRuntime():其实就是Runtime类获取对象的方式,等于new一个Runtime类。之所以封装成一个函数是为了不调用一次建立一个对象,只获取一个对象来执行操作。
  • exec():调用exec函数
  • calc.exe:调用计算器程序

反射机制的执行顺序跟直接调用有些差别,一步步来

forName

获得一个class对象(java.lang.Class)有三种方法:

  1. obj.getClass():如果上下文中存在某个类的实例obj,那么我们可以直接通过obj.getClass()来获取它的类。
    1. 假如obj是实例:获取该实例的class(如Runtime.getRuntime().getClass()结果就是class java.lang.Runtime类)(此处类的意思实际上时class这个类的对象)
    2. 假如obj是类:获取到java.lang.Class类(class这个类的对象)
  2. Test.class:Test是一个已经加载的类,想获取它的java.lang.Class对象,直接拿取class参数即可。(这不是反射机制)
  3. Class.forName:如果知道类的名字,可以直接使用forname来获取。

forName两种使用形式

Class<?> forName(String name) 
//name:class名称
Class<?> forName(String name, **boolean** initialize, ClassLoader loader)
//name:class名称
//initialize:是否进行“类初始化”
//loader:加载器

//第一种调用形式等同于第二种,其实就是第二种形式的封装,默认进行"类初始化”,默认加载器根据类名(完整路径)来加载
Class.forName(className) 
Class.forName(className, true, currentLoader)

类初始化
类初始化不等于类的实例化,举个例子:

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());
        } 
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
    Class.forName("TrainPrint");
}
// Static initial class TrainPrint
public static void main(String[] args) throws IOException, ClassNotFoundException {
   TrainPrint test= new TrainPrint();
}
// Static initial class TrainPrint
// Empty block initial class TrainPrint
// Initial class TrainPrint

类的实例化:静态初始块->初始块->构造函数
类的初始化:静态初始块

具有父类的类的实例化:父类静态初始块->子类静态初始块->父类初始块->父类构造函数->子类初始块->子类构造函数
具有父类的类的实例化:父类静态初始块->子类静态初始块

以上对于类初始化的说明其实就是说,单独一个Class.forName(),在类静态初始块可控的情况下,可以执行恶意代码。

调用内部类:
Java的普通类 C1 中支持编写内部类 C2 ,而在编译的时候,会生成两个文件: C1.class 和 C1$C2.class,我们可以把他们看作两个无关的类。
Class.forName("C1$C2")可以调用这个内部类。

我们可以通过Class.forName("java.lang.Runtime")来获取类(java.lang.Runtime是Runtime类的完整路径)

getMethod

getMethod 的作用是通过反射获取一个类的某个特定的公有方法。
而Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,我们需要传给他你需要获取的函数的参数类型列表,如下:
Class.forName("java.lang.Runtime").getMethod("exec", String.class)

invoke

invoke方法位于Method类下,其的作用是传入参数,执行方法,
public Object invoke(Object obj, Object... args)
它的第一个参数是执行method的对象:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类(之后会提到,这里其实不用那么死板,这个)
    它接下来的参数才是需要传入的参数。

由于我们的exec函数是一个普通方法,需要传入类对象,即invoke(类对象,exec方法传入的参数)
之前说到Runtime的类对象不能通过newInstance()来获取对象(class.newInstance等于new class),是因为Runtime的类构造函数是一个private构造函数,只能通过getRuntime方法返回一个对象。
获取类对象:Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")) (由于getRuntime是一个静态方法,invoke传入Runtime类,进行调用)
invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),"calc.exe")

那么合成以上的操作:

Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))"calc.exe")

亲测可弹框,再简化一下:

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

以上我们就完成了通过类内置的静态方法获取类的实例,进一步调用一个public方法。
但是假如一个类没有无参构造方法(即不能class.newInstance()),也没有单例模式(只存在一个实例)的静态方法(即不能像getRuntime一样获取实例),那我们该如何实例化这个类呢?

指定的构造方法生成类的实例

继续举一个演化成反射机制的执行命令payload的例子:

List<String> paramList = new ArrayList<>();
paramList.add("calc.exe");
ProcessBuilder pb = new ProcessBuilder(paramList);
pb.start();

可见,其构造函数是写入了一个字符串,不是无参构造方法,接下来我们会一步步进行转化。

getConsturctor()函数可以选定指定接口格式的构造函数(由于构造函数也可以根据参数来进行重载),即:getConsturctor(参数类型)

选定后我们可以通过newInstance(),并传入构造函数的参数执行构造函数,即newInstance(传入的构造函数参数)

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List<String> command)
  • public ProcessBuilder(String... command)(此处,String...这种语法表示String参数数量是可变的,与String[]一样)
    分别执行构造方法获取实例的语句如下:
  • Class.forName("java.lang.ProcessBuilder").getConstructor(String.class).newInstance("calc.exe"))
  • Class.forName("java.lang.ProcessBuilder").getConstructor(List.class).newInstance(Arrays.asList("calc.exe")))

执行完构造方法获取实例之后,其实可以通过类型强制转化,进而执行start()函数,进行执行命令。

Class clazz = Class.forName("java.lang.ProcessBuilder"); 
( (ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")) ).start();

但实际情况下并不一定可以这样利用,所以继续使用反射机制调用start函数,start函数不是一个静态函数,需要传入类的实例:

Class clazz = Class.forName("java.lang.ProcessBuilder"); 
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

亲测成功

另一种形式的构造函数的反射转换:

ProcessBuilder pb = new ProcessBuilder("calc.exe");//这个可以传入多个字符串
pb.start();

因为ProcessBuilder构造函数传入的参数其实不是一个字符串String.class类型,而是一个变长的字符串数组String[].class,所以我们应该如下调用。

Class clazz = Class.forName("java.lang.ProcessBuilder"); 
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[]{"calc.exe"})).start();

但是实际上我们这样调用是会报错的,因为newInstance函数接受参数是一个Object..也就是Object数组,它会完美契合我们提供的String[],剥去一层数组。

来看看之前的List形式构造函数的形式:

可见少了一层,List传入会被当做Object的第一项,而String[]传入,会直接被当作Object。
所以我们需要再加一层[]:

Class clazz = Class.forName("java.lang.ProcessBuilder"); 
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();

亲测可弹计算器。

执行私有方法

以上都是方法或构造方法是public函数,但是如果是私有方法,我们该如何调用?
之前我们用的都是getMethod、getConstructor,接下来需要使用getDeclaredMethod、getDeclaredConstructor:

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

之前说到Runtime的构造方式是一个私有方法,从而不能直接调用,那么接下来我们来调用Runtime的构造方法来获取一个实例来执行calc.exe:

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(true)可以打破私有方法访问限制,从而进行调用。
getDeclaredMethod、getDeclaredConstructor和getMethod、getConstructor使用方法是一致的,就不赘述了。

小结

那么我们已经通过反射机制,调用任意类的任意方法了。
具体的函数说明表格可以参考简书

从commons-collections回来的invoke性质补充说明

一直以来我们都看到这种反射机制的调用来获取Runtime类的实例:

Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))

看到这个例子,我们可能会固定思维的误解:
我们从哪个类中获取方法,在invoke中就应该传入哪个类或这个类的实例(上面说过,这个需要根据方法来定)

我们从源码来看一下Method类中的invoke函数的输入输出说明:

* @param obj  the object the underlying method is invoked from//调用基础方法的对象
* @param args the arguments used for the method call//调用这个方法的传参
* @return the result of dispatching the method represented by//调用这个方法的返回结果
* this object on {@code obj} with parameters

来对照普通调用语句和反射语句之间的对应关系,这里前后两个class都是相同的Runtime class

那么讲道理一般的调用,默认我们的固定思维惯性,也不会有什么问题,因为选中一个类的方法自然是应该传入这个类的实例或这个类。
直到出现一些意外:

//不要把下面的语句当作反射机制,就当作Runtime.getRuntime()是一个普通函数调用
Class.forName("java.lang.Runtime").getMethod(...)
Class.forName("java.lang.Class").getMethod(...)
//好比有一个函数可以在两个class中调用
a.getMethod()
b.getMethod()

如上我们一直忽略了我们反射机制中调用的函数实际上可以在不同的class中调用,这一点在commons-collections中十分关键:
由于当时的环境
不允许我们得到Class.forName("java.lang.Runtime").getMethod(...)
只有Class.forName("java.lang.Class").getMethod(...)

那么我们可以得到以下途径进行变形:

  • 用反射机制去调用反射机制中使用的函数getMethod
  • 使用invoke传入的obj去指定getMethod的当前的调用环境(在实际代码执行中是this变量的区别)

看一看Class类中getMethod方法的接口

public Method getMethod(String name, Class<?>... parameterTypes)//<?>... 表示任意类型任意数量的Class数组

改成反射机制,得到getMethod这个方法

Method method1= Class.forName("java.lang.Class")
        .getMethod("getMethod", new Class[] {String.class, Class[].class });//根据getMethod方法接口进行封装

指定obj:

//以下语句执行结果等同于Class.forName("java.lang.Runtime").getMethod(getRunime)
method1.invoke(Class.forName("java.lang.Runtime"),"getRuntime",new Class[0]);
//用于对比:以下语句等同于Class.forName("java.lang.Class").getMethod(getRunime)
//method1.invoke(Class.forName("java.lang.Class"),"getRuntime",new Class[0]);

合起来就是

// 反射调用
Class.forName("java.lang.Runtime").getMethod("getRuntime")
// 经过变形的反射调用
Class.forName("java.lang.Class")
    .getMethod("getMethod", new Class[] {String.class, Class[].class })//得到getMethod方法
    .invoke(Class.forName("java.lang.Runtime"),"getRuntime",new Class[0]);

现在我们只是以Class.forName("java.lang.Class")开头获取到了Runtime类下的getRuntime方法。还没有执行。

invoke 静态方法的obj不那么严格

在调用invoke执行前,还需要讲另一个invoke非常神奇的特性,不那么严格校验的obj。
回顾之前总结的invoke传参规则:

它的第一个参数是执行method的对象obj:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类
    它接下来的参数才是需要传入的参数。

传入的第一个参数其实不一定要是正确的类或者类对象!
看一下例子:

//main函数
Class.forName("a").getMethod("print").invoke(null);
//内部类:
class a{
    public static void print(){
        System.out.println(1);
    }
}

按照规则,print函数是一个静态方法,实际上我们应该invoke传入一个a的类。但是以上代码的执行结果却是成功的:

但是print不是静态方法时就会执行失败:

这是因为invoke函数null抛出报错的机制导致的:

* @exception NullPointerException      
* if the specified object is null and the method is an instance method. //obj为null,并且method是一个实例method
*/
@CallerSensitive
public Object invoke(Object obj, Object... args){
}

当method是一个普通函数时,传入obj不能为null,并且其类对象要与方法匹配
但是当method是一个静态函数时,就很随便了(可能是因为压根不会被用到吧)。

那么我们在调用getMethod的时候可不可以皮一下呢?

Method method = (Method) Class.forName("java.lang.Class")
    .getMethod("getMethod", new Class[] {String.class, Class[].class })//得到getMethod方法
    .invoke(null,"getRuntime",new Class[0]);//这里invoke传入null

报错自然是因为getMethod并不是static方法,所以不能为null了。
但是为什么getMethod不是static方法,但是invoke中却传入类而不是类实例呢........
我是没有搞懂这个问题....但是感觉是因为是反射机制的函数的问题。由于已经花了太多时间,就不在这里继续纠结了。

感谢蟠大佬,点醒了;其实是看p牛的反射篇产生的误解,上文中所说的类,其实也是一个实例。类实例是其他类的实例,实际上是Class.class这个类的实例。
这边之前的疑问是,getMethod不是静态方法而invoke中传入的是类而不是类实例。
实际上因为getMethod本来就是class类中的方法,而Class.forName("java.lang.Runtime")获取到的class类的实例
我们调用getMethod传入的不是之前的模糊类的概念,而是class类的实例(类实例),所以这里是没毛病的调用class类下的非静态方法,传入class类实例。

但是Runtime.getRuntime就不一样了:

Object object0 = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.String"));
Object object1 = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
Object object2 = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));

突出一个随便,随性。

回来继续构造调用

上面我们只是获取到了getRuntime方法,我们还没有调用这个方法获取其Runtime对象

//普通调用形式
Runtime.getRuntime()
// 反射调用
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))
// 经过变形的反射调用
Method method = (Method) Class.forName("java.lang.Class")
                .getMethod("getMethod", new Class[] {String.class, Class[].class })//得到getMethod方法
                .invoke(Class.forName("java.lang.Runtime"),"getRuntime",new Class[0]);//调用getMethod方法
//以上等于执行Class.forName("java.lang.Runtime").getMethod("getRuntime")
//调用Runtime.getRuntime函数,传入的obj根据上面的分析可以随便写
method.invoke(Class.forName("java.lang.Runtime"),new Object[0]);
method.invoke(null,new Object[0]);
method.invoke(Class.forName("java.lang.String"),new Object[0]);

至此,我们就换了一种方式去获取Runtime实例。
当然最后的getRuntime函数我们是直接反射调用的,在实际commons-collections利用中我们仍然需要跟得到getMethod一样用反射机制去获取invoke,然后再调用。
虽然在这里觉得1+1的问题为什么要1+1000-1000+1,兜兜转转挺傻乎乎的。但是在利用链的场景下,就会觉得:

卧槽....这个POC作者真是个人才。

参考

反射机制参考:
https://www.cnblogs.com/wglIT/p/7590468.html
https://www.cnblogs.com/yrstudy/p/6500982.html
P神-java安全漫谈-反射机制1(知识星球-代码审计)
P神-java安全漫谈-反射机制2(知识星球-代码审计)
P神-java安全漫谈-反射机制3(知识星球-代码审计)


文章来源: http://xz.aliyun.com/t/7029
如有侵权请联系:admin#unsafe.sh