freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Z3专栏 | Java代码审计之反射
2021-12-12 21:51:12
所属地 辽宁省

介绍

本篇学习java的反射

将介绍通过反射 ,无视作用域,调用对象方法,调用构造函数实例化对象,获取、修改对象私有变量等操作

java代码审计学习(二、反射)

上一篇留下了个问题
如果一个对象或方法,作用域是default或private,当前代码不在作用域内,怎么调用?

在解决问题之前,先来点预备知识:反射

什么是反射

在我理解其实很简单,就是通过字符串去调用函数,变量或类
什么意思?
举个例子
例如本地有getAge、getName、getSex。。。等等,很多函数,用于用于查询个人信息
当用户查询信息时会调用这些方法,例如用户传入age,则调用getAge方法
怎么实现?
笨方法:使用if
if(userin == "age"){
getAge();
}else if(userin == "name"){
getName();
}else .......
有多少函数就写多少if,这不累死
所以,反射就派上用场了:
首先将用户输入转为函数名("get"+首字母大写的userin),然后通过函数名去调用对应函数
怎么用代码实现?
先看下用php如何实现

$userin = "age";
$func = "get".ucfirst($userin); // 将age变为getAge,这里.是字符串拼接,ucfirst是首字母大写
$func();

ok,就这么简单,拼接出函数名,然后就可以将这个字符串变量作为函数调用了,所以上面的问题可以用这句话解决"get".ucfirst($userin)();

java的反射

下面再来看看java怎么实现反射

  1. 因为java所有函数都在类里,所以第一步,要获取到类,调用forName方法,通过类名hanhan获取到类对象

  2. 再用类对象的getMethod方法,获取到类里的han函数的对象

  3. 调用hanhan对象的han方法
    不过这里有个疑问invoke为什么要传入clazz.newInstance(),直接调用hanhan类的han方法不行吗
    这里newInstance调用了类的无参构造函数创建了hanhan的对象,clazz.newInstance()等价于new hanhan()。为什么要传这个参数?
    调试一下,看这个参数最终用来做什么了
    private static native Object invoke0(Method m, Object obj, Object[] args);
    调试发现,最终传给了native方法invoke0,好吧,,看不见native方法的代码,那猜测一下原因
    因为这里调用的是"方法",而不是"函数",方法在执行时,会用到类的成员变量或方法,它属于对象,和对象是一个整体,它的执行可能是受对象里的成员变量影响的,而"函数"就是"单打独斗",可以直接调用
    例如通过invoke创建数据库对象,连接数据库
    可能先创建对象,然后手动设置类的成员变量,如数据库地址、账号密码等
    这些都做好后,通过invoke调用这个对象的connect方法,connect方法读取类成员变量,连接数据库
    这也是java面向对象编程和面向过程编程的一种区别吧

反射是非常有用的一种机制,它允许我们"动态的"去执行代码。执行什么代码,可以通过用户指定
对比各种语言的反射
c和go这种编译型语言的反射我愿称之为伪反射,它们反射的原理是需要手动建一个映射表,例如age->getAge(),在使用反射时需要查表,新的方法需要用到反射,那就要手动将新函数写到反射表中
而php、python、java等解释型语言的反射,不需要手动去建反射表
猜测因为编译型语言在编译后是汇编代码,运行时直接执行汇编代码,而汇编调用函数时通过地址调用,与函数名无关,所以想通过函数名调用,只能做一个函数名与函数地址的映射表实现反射
而解释型语言运行代码是靠解释器运行的,解释器看一眼代码后,根据它的"理解"去做相应"动作",执行的不是代码本身。相当于代码指挥解释器去做事,这种当然自由度高,当然可以通过函数名调用函数,即反射

回到正题

从上面图中可以看到,clazz.newInstance()方法有删除线
在java9之后推荐使用.getDeclaredConstructor().newInstance()代替.newInstance(),后面会介绍getDeclaredConstructor方法

现在知道了如何通过反射来调用指定类的指定方法
下面再试一下通过反射的方式,实现上一篇中提到的三种命令执行方法

通过反射,再现三种命令执行方式

1、先看下ProcessBuilder方式

public static void main(String []args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
    Class clazz = Class.forName("java.lang.ProcessBuilder"); // 获取类对象
    Method m = clazz.getMethod("start"); // 获取方法对象
    Process p = (Process) m.invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("whoami"))); // 创建ProcessBuilder对象,再调用对象的start方法
    show(p);
}

分析下这里的clazz.getConstructor(List.class).newInstance(Arrays.asList("whoami"))
getConstructor方法是获取类的构造方法,参数是构造函数的参数类型,然后通过newInstance实例化对象
ProcessBuilder构造方法有两个

public ProcessBuilder(List<String> command);和public ProcessBuilder(String... command);

所以getConstructor(List.class)选择了第一种构造方法
然后newInstance传入List类型数据(List是接口,可以接收列表类型对象)

2、使用runtime

public static void main(String []args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
    Class clazz = Class.forName("java.lang.Runtime"); // 获取Runtime类对象
    Method execMethod = clazz.getMethod("exec", String.class); // 获取exec方法对象
    Method getRuntimeMethod = clazz.getMethod("getRuntime"); //获取getRuntime方法对象
    Object runtime = getRuntimeMethod.invoke(clazz); // 调用getRuntime方法
    Process p = (Process) execMethod.invoke(runtime, "whoami"); //调用exec方法
    show(p);
}

又有新知识点,
知识点一
这个步骤比ProcessBuilder方式多了两个:获取getRuntime方法,和调用getRuntime方法
为什么?
因为Runtime类用的是单例模式(设计模式中的一种),构造函数是private Runtime() {}
其他类不能调用构造方法,就没办法创建Runtime对象
那其他类怎么使用Runtime?
Runtime类有一个静态变量private static final Runtime currentRuntime = new Runtime();
这里要讲一下static修饰符
被static修饰符修饰的代码在类初始化时被执行
什么时候类初始化时?当使用到这个类内的东西(变量或方法)时就会初始化

Runtime类还有一个静态方法

public static Runtime getRuntime() {
    return currentRuntime;
}

当调用getRuntime时,类开始初始化,就会从上到下执行静态代码,先执行private static final Runtime currentRuntime = new Runtime();创建了Runtime对象
然后getRuntime()返回Runtime对象
类只会初始化一次,所以Runtime对象只会被创建一次,,其它代码想获得Runtime对象,只能调用getRuntime()方法
这种单例模式还可以用在数据库类,保证数据库对象只执行一次连接,只有一个数据库对象,对数据库的操作都通过这个对象进行

所以,开始的代码,多出的两个步骤:获取getRuntime方法,和调用getRuntime方法,是为了获取Runtime对象
知识点二
注意这段代码Object runtime = getRuntimeMethod.invoke(clazz); // 调用getRuntime方法
上面学习invoke方法时,invoke传入参数是clazz,而不是该方法的实例
之前不是分析invoke第一个参数应该是该方法的实例对象吗?因为调用方法时,可能会用到对象的成员变量,所以要先创建对象
但这次调用的方法是getRuntime,一个静态方法
静态方法和变量属于类,所有static修饰的变量或方法都会在类加载时执行,静态方法或变量是一个类的所有实例共用的
静态方法不需要实例化就可以调用,所以静态方法调用方法外的方法或变量都必须要是静态的(因为非静态变量或方法需要实例化对象才能用)
所以静态方法,更像是"函数"的概念,写在类里,作用域为当前类的函数

所以
静态方法的invoke不需要传入该方法的实例化对象,但是invoke需要至少一个参数,所以,这个参数可以为任意值,可以为null,12312,"12321"
如图,这样也可以正常执行

3、使用processImpl

上一篇在使用processImpl命令执行时遇到了问题,当前代码不在processImpl的作用域,下面看看如何通过反射解决这个问题

public static void main(String []args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
    Class clazz = Class.forName("java.lang.ProcessImpl"); // 获取ProcessImpl的Class对象
    Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class); // 获取start方法对象
    method.setAccessible(true); // 设置方法允许在当前代码使用
    Process p = (Process) method.invoke(null, new String[]{"whoami"}, null, ".", null, true); // 调用start方法
    show(p);
}

可以看到之前的getMethod函数变为了getDeclaredMethod
getMethod函数是从public方法中获取方法,包括父类中的public方法
getDeclaredMethod函数是从当前类所有声明的方法中获取方法,不包括父类声明的方法
getXXX系列和getDeclaredXXX系列函数,可以获取Method(方法)、Constructors(构造函数)、Fields(成员变量),区别和上面一样

setAccessible(true)是修改方法的访问权限,允许当前代码调用
运行一下
如图,命令执行成功,在执行setAccessible(true)时,java发出了警告
这里invoke第一个参数是null,看一下ProcessImpl的start方法,果然,是静态方法

上面就是通过反射,实现三种命令执行方式

反射的更多用途

Constructor和Fields

上面的例子中用到了getDeclaredMethod
下面再学习下
getXXX系列和getDeclaredXXX系列的Constructor(构造函数)和Fields(成员变量)
就以Runtime为例
上面提到过,Runtime是单例模式,因为构造函数是private,所以只能在加载类时,实例化一个currentRuntime对象,因为这个对象是private,所以之后只能通过getRuntime获取
如图

但是学会反射就可以为所欲为了
构造函数是private?那我可以通过getDeclaredConstructor来调用构造函数
如图

currentRuntime对象是private?
那可以通过getDeclaredFields获取

这里field的get方法,本应传入类对象的,但因为currentRuntime是静态变量,所以get可以传任意值
field还有set方法,可以修改变量值、getType方法,获取变量类型
这里就不演示了

反射获取内部类

示例如图
注意这里使用的内部类的名称为"Student$Family"
为什么用这个名呢?
如图,使用javac对当前java文件编译,每个类都生成对应class文件,内部类Family的命名方式是 外部类$内部类.class

所以,forName方法是通过class文件名来加载Class对象的

之前提到了static修饰符,再补充下知识点

static修饰符

static修饰符修饰的变量或方法会在类加载时调用
static还可以这样用,叫做静态代码块

static int a = 1
static{
    a += 1;
}

这里的{}是作用域,在{}内声明的变量是局部变量,作用域为当前大括号,外界不能访问,{}内可以像在函数内一样执行代码
如果去掉static,{}内的代码叫做构造代码块

int a = 1
{
    a += 1;
}

静态代码块是初始化类用的,在类初始化时从上到下执行
构造代码块是给对象初始化用的,在对象初始化时,在构造方法之前执行
所以各种代码块相当于被指定了加载顺序,自动被调用的匿名方法

静态代码块在类加载时就会调用
所以可以像下面这样,写一个恶意类,当这个类被加载时,就会命令执行

import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
 static {
 try {
 Runtime rt = Runtime.getRuntime();
 String[] commands = {"touch", "/tmp/success"};
 Process pc = rt.exec(commands);
 pc.waitFor();
 } catch (Exception e) {
 // do nothing
 }
 }
}

至此,反射篇,结束了

总结

学会了反射简直可以为所欲为,很强大,但使用不当也很危险
而且感觉它不符合面向对象的思想

通过反射,可以无视作用域修饰符,调用方法,实例化对象,调用、修改变量等

# java漏洞 # java # Java代码审计 # Java反序列化漏洞分析 # JAVA安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录