2019年九月月 发布的文章

通过HashMap触发DNS检测Java反序列化漏洞

我们常说的反序列化漏洞一般是指readObject()方法处触发的漏洞,而除此以外针对不同的序列化格式又会产生不同的出发点,比如说fastjson会自动运行setter,getter方法。之后又有各种RMI,JNDI姿势去执行命令。现在常见的黑盒检测Java反序列化方式就是执行命令API,比如用一个gadget去执行nslookup xxx 最终通过服务器记录去判断。
但这种方式可能出现的一种问题是,你选择测试的gadget服务器正好没这个jar包或者更新过了,但却有另一个存在漏洞的jar包。这时候单一的gadget构造出的执行命令payload就会漏报。所以为了解决这种问题这里分享一个通过HashMap结合URL触发DNS检查的思路。在实际过程中可以首先通过这个去判断服务器是否使用了readObject()以及能否执行。之后再用各种gadget去尝试试RCE。

HashMap readObject & URLStreamHandler hashCode

HashMap最早出现在JDK 1.2中,底层基于散列算法实现。而正是因为在HashMap中,Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的。所以对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。因此,HashMap实现了自己的writeObject和readObject方法。
因为是研究反序列化问题,所以我们来看一下它的readObject方法Xnip2019-09-27_17-02-15
前面主要是使用的一些防止数据不一致的方法,我们可以忽视。主要看putVal时候key进入了hash方法,跟进看

这里直接调用了key的hashCode方法。那么我们现在就需要一个类hashCode可以执行某些东西即可。
很幸运的我们发现了URL类,它有一个有趣的特点,就是当执行hashCode方法时会触发当前URLStreamHandler的hashCode方法。

我们可以继续跟进

主要就是这句代码了

屏幕快照 2019-09-27 下午5.15.04
很简单,就是这里最后触发了DNS查询。

也就是说我们现在思路是通过hashmap放入一个URL的key然后会触发DNS查询。这里需要注意一个点,就是在URLStreamHandler的hashCode方法中首先进行了一个缓存判断即如果不等于-1会直接return。

因为在生成hashMap put时候会调用到hashCode方法,所以会缓存下来,即hashcode不为-1。所以为了让被接收者触发DNS查询,我们需要先通过反射把hashcode值改为-1,绕过缓存判断。

最后生成的代码为

测试代码

调用栈
屏幕快照 2019-09-28 下午12.38.19

可以看到触发成功
屏幕快照 2019-09-28 下午12.32.02

ysoserial已经有加入这个方式 具体可见:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java

参考:

https://www.gosecure.net/blog/2017/03/22/detecting-deserialization-bugs-with-dns-exfiltration
https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/

深入理解Java反射中的invoke方法

什么是反射

反射(Reflection)是Java程序开发语言的特征之一,它允许运行中的Java程序获取自身的信息,并且可以操作类或对象的内部属性。主要是指程序可以访问、检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。

Oracle 官方对反射的解释是:

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。

反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

Java 反射主要提供以下功能:

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 在运行时调用任意一个对象的方法

重点:是运行时而不是编译时

反射的主要用途

很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。当我们在使用 IDE(如 Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,编译器就会自动列出它的属性或方法,这里就会用到反射,当然也有的用到了语法树。

在 Web 开发中,我们经常能够接触到各种可配置的通用框架。为了保证框架的可扩展性,它们往 往借助 Java 的反射机制,根据配置文件来加载不同的类。举例来说,Spring 框架的依赖反转 (IoC),便是依赖于反射机制。

反射invoke实现原理

invoke方法用来在运行时动态地调用某个实例的方法
它的实现代码如下:

1.权限检查

通过代码我们可以看到,首先invoke方法会检查AccessibleObject的override属性的值。而AccessibleObject类是实现了AnnotatedElement,它是Field、Method和Constructor对象的基类。它提供了将反射的对象标记为在使用时取消默认Java语言访问控制检查的能力。对于公共成员、默认(打包)访问成员、受保护成员和私有成员,在分别使用 Field、Method或Constructor对象来设置或获得字段、调用方法,或者创建和初始化类的新实例的时候,会执行访问检查。
override的值默认是false,表示需要权限调用规则。我们常使用的setAccessible方法就是将其设置为true,从而忽略权限规则,调用方法时无需检查权限。
继续往下看,当其需要权限调用则走Reflection.quickCheckMemberAccess,检查方法是否为public,如果是的话跳出本步。如果不是public方法,那么用Reflection.getCallerClass()方法获取调用这个方法的Class对象

这是一个native方法,我们从openJDK源码中去找它的JNI入口(Reflection.c)

具体实现在hotspot/src/share/vm/prims/jvm.cpp
屏幕快照 2019-09-08 下午4.10.28

获取了这个Class对象caller后用checkAccess方法做一次快速的权限校验

这里主要是进行一些基本的权限检查,以及使用缓存机制。

2.调用MethodAccessor的invoke方法

我们主要关注这个流程中的操作

首先要了解Method对象的基本构成,每个Java方法有且只有一个Method对象作为root,它相当于根对象,对用户不可见。当我们创建Method对象时,我们代码中获得的Method对象都相当于它的副本(或引用)。root对象持有一个MethodAccessor对象,所以所有获取到的Method对象都共享这一个MethodAccessor对象,因此必须保证它在内存中的可见性。
而当第一次调用一个Java方法对应的Method对象的invoke()方法之前,实现调用逻辑的MethodAccessor对象还没有创建,所以通过reflectionFactory创建MethodAccessor并更新给root,然后调用MethodAccessor.invoke()完成反射调用。
可以看到invoke方法实际是委派给了MethodAccessor类型的ma对象来处理。MethodAccessor是一个接口,有两个实现类。一个委派实现(DelegatingMethodAccessorImpl),一个本地实现(NativeMethodAccessorImpl)。这里调用的委派实现主要是为了在本地实现和动态实现之间做切换。考虑到许多反射调用仅会执行一次,Java虚拟机设置了一个阈值15(是从0开始计算,>15),当某个反射调用的调用次数<=15 时,采用本地实现;当大于15时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现。这个过程我们称之为Infation。
这里我们先通过demo测试看一下委派实现对本地实现和动态实现的切换,再具体分析其两种invoke的底层实现。

运行结果

可以看到如果第一次invoke某方法,它实际调用的是本地实现。
接着我们修改一下代码

结果
Xnip2019-09-08_17-06-41
可以看到版本15后切换为动态实现

下面我们来从源码层面分析上述过程
因为methodAccessor实例由reflectionFactory对象操控生成,所以我们先来看一下ReflectionFactory类的源码
屏幕快照 2019-09-08 下午5.23.45
inflationThreshold就是我们之前说的阈值

可以看到newMethodAccessor根据不同的条件分别选择了本地实现和动态实现,动态实现和本地实现相比,其运行效率要快上20倍。这是因为动态实现无需经过Java到C++再到Java的切换,但由于生成字节码十分耗时,仅调用一次的话,反而是本地实现要快上3到4倍。
为了尽可能地减少性能损耗,HotSpot JDK采用我们之前提到的“inflation”的技巧,也就是让Java方法在被反射调用时,开头若干次使用native版,等反射调用次数超过阈值时则生成一个专用的MethodAccessor实现类,生成其中的invoke()方法的字节码,以后对该Java方法的反射调用就会使用Java版本。 这项优化是从JDK 1.4开始的。
实际上如果你看过fastjson源码,就能发现它为了优化性能在json转对象时也加入了这种动态生成的方法,主要也通过使用asm完成。

本地实现

一开始(native版)会生产NativeMethodAccessorImplDelegatingMethodAccessorImpl两个对象。DelegatingMethodAccessorImpl是一个中间层,是为了在native版与Java版的MethodAccessor之间进行切换。
我们主要关注NativeMethodAccessorImpl

每次调用其invoke时会做一个累加,判断是否到达阙值,如果没有则调用native的invoke0方法,当超过时则调用MethodAccessorGenerator.generateMethod(),并将其设置到DelegatingMethodAccessorImpl的delegate,这样下次就会直接调用到动态实现的位置。

下面我们从jvm底层分析这里的native实现invoke0

JVM_InvokeMethod在这里 hotspot/src/share/vm/prims/jvm.cpp 实现
屏幕快照 2019-09-08 下午6.06.08
其关键是

跟进hotspot/src/share/vm/runtime/reflection.cpp

在这之前先对JVM的oop-klass做一个简单的介绍:
1.oop(ordinary object pointer):类型其实是 oopDesc*,在 Java 程序运行的过程中,每创建一个新的对象,在 JVM 内部就会相应地创建一个对应类型的 oop 对象。实际上就是普通对象指针,用于描述对象的实例信息。各种 oop 类的共同基类为 oopDesc 类。
一个OOP对象包含以下几个部分:

  • 对象头 (header)
    • Mark Word,主要存储对象运行时记录信息,如hashcode, GC分代年龄,锁状态标志,线程ID,时间戳等
    • 元数据指针,即指向方法区的instanceKlass实例
  • 实例数据。存储的是真正有效数据,如各种字段内容,各字段的分配策略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类定义的变量的前面。
  • 对齐填充。仅仅起到占位符的作用,并非必须。

2.klass:Java类在JVM中的表示,是对Java类的描述。简单的说是Java类在HotSpot中的c++对等体,用来描述Java类。klass是什么时候创建的呢?一般jvm在加载class文件时,会在方法区创建instanceKlass,表示其元数据,包括常量池、字段、方法等。
网上的oop-klass模型示例图很多,我随便找了一个:
2579123-146cd049168f02e
JVM就是用这种方式,将一个对象的数据和对象模型进行分离。普遍意义上来说,我们说持有一个对象的引用,指的是图中的handle(存放在栈区),它是oop(存放在堆区)的一个封装。

我们再看代码Reflection::invoke_method()中接受的method_mirror(oop)就是我们要反射调用的方法。然后代码调用了Reflection::invoke()
跟进之后最终到JavaCalls::call()执行
位于hotspot/src/share/vm/runtime/javaCalls.cpp

最后的os_exception_wrapper其实就是调用了call_help,也就是说本地实现的反射最终的方法执行是通过JavaCalls::call_helper方法来完成的

上面代码中StubRoutines::call_stub()返回的是一个函数指针,在执行上面的call_stub()时,会先将参数先压入堆栈。
这个函数指针指向什么地方呢,这是和机器类型有关的,以X86-32为例,看hotspot/src/cpu/x86/vm/stubGenerator_x86_32.cpp里面
下面大家会看到很多类似汇编指令的代码,其实这些不是指令,而是一个个用来生成汇编指令的方法。JVM是通过MacroAssembler来生成指令的。我会将具体的执行过程通过注释的方式插入到代码中

可以简化来看

它实际首先建立了一个栈帧,这个栈帧里面保存了一些重要的数据,再把Java方法的参数压入栈。当这一步完成,栈帧变成了这个样子:
20190522101723741_YEBRCZ

代码中最终会走call rax相当于call entry_point,entry_point例程和call_stub例程一样,都是用汇编写的来执行Java方法的工具。这个entry_point即解释器入口点,最终的方法执行过程其实是在这里面的。
这个entry_point从何而来,从method->from_interpreted_entry(),从methodOopDesc::link_method中获取address entry = Interpreter::entry_for_method(h_method),也就是说如果不用jit的,直接调用解析器的入口,由解释器再进行操作。

动态实现

主要看MethodAccessorGenerator类generate方法
实际就是通过asm来生成对应的java字节码来调用方法
我们可以简单来分析一下关键代码
屏幕快照 2019-09-09 下午6.49.27
可见动态生成的类名为GeneratedMethodAccessor+编号

我们找到对应的类 来看看它写入字节码的几个关键操作
Xnip2019-09-09_19-04-09
可见代码生成对应的字节码,这段代码主要是需要调用的方法signature等信息放到常量池里,用于后续code区调用

后续生成code区字节码的方法是emitInvoke

可以看到生成的字节码,里面引入了HowReflect.targetMethod
Xnip2019-09-10_22-37-39

最后可以整体看一下字节码反编译成的Java文件,即可以理解为动态实现就是通过生成这样一个java类去调用指定方法。

参考

https://www.sczyh30.com/posts/Java/java-reflection-1/
https://www.jianshu.com/p/4eeb057b509e
https://www.jianshu.com/p/b6cb4c694951
https://www.jianshu.com/p/3b311109050b
https://blog.csdn.net/qq_26222859/article/details/81335542
http://hsmemo.github.io/articles/nolC3LIO6Q.html
https://blog.csdn.net/wenyuan65/article/details/81145900
https://www.iteye.com/blog/xieyj-236704
https://www.jianshu.com/p/b5967bbe02c4
http://www.ishenping.com/ArtInfo/497633.html

LOGO

近期文章

标签

分类目录

文章归档

近期评论

功能

博客统计

  • 21,247 点击次数