1 Java 基础 ¶
约 8435 个字 783 行代码 预计阅读时间 55 分钟
Note
本文档主要介绍 Java Web 安全相关知识归纳总结,学习路线参考
主要代码示例基于 JDK 7u80,部分代码采用 JDK 22.0.1
推荐使用 Java Tools 中的 JEnv 进行版本管理(也有对应的 linux/MacOS 版本)
1.1 ClassLoader 机制 ¶
JVM 架构图 :
Java 类均要经过 ClassLoader 加载后才能运行,AppClassLoader 是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用 AppClassLoader 加载类,
ClassLoader.getSystemClassLoader()
返回的系统类加载器也是 AppClassLoader。
Java 类加载方式 ¶
- 隐式加载:当程序创建对象实例或使用
ClassName.MethodName()
时,如果该对象的类还没有被加载,JVM 会自动调用类加载器加载该类。 - 显式加载:通过 Java 反射机制或 ClassLoader 来动态加载类。
Example
Class.forName("类名")
默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用 Class.forName("类名", false, 类加载器)
,而 ClassLoader.loadClass
默认不会初始化类方法。
1.2 Java 反射 ¶
Java 反射 ( Reflection
) 是 Java 非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法 (Methods
)、成员变量 (Fields
)、构造方法 ( Constructors
) 等信息,还可以动态创建 Java 类实例、调用任意的类方法、修改任意的类成员变量值等。Java 反射机制是 Java 语言的动态性的重要体现,也是 Java 的各种框架底层实现的灵魂。
获取 Class 对象 ¶
Java 反射操作的是java.lang.Class
对象,所以我们需要先想办法获取到 Class 对象,通常我们有如下几种方式获取一个类的 Class 对象:
类名.class
,如 :cc.classloader.TestHelloWorld.class
。Class.forName("cc.classloader.TestHelloWorld")
。classLoader.loadClass("cc.classloader.TestHelloWorld");
获取数组类型的 Class 对象需要特殊注意 , 需要使用 Java 类型的描述符方式,如下:
Class<?> doubleArray = Class.forName("[D");//相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相当于String[][].class
获取 Runtime 类 Class 对象代码片段:
String className = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);
通过以上任意一种方式就可以获取java.lang.Runtime
类的 Class 对象了,反射调用内部类的时候需要使用$
来代替.
, 如cc.Test
类有一个叫做Hello
的内部类,那么调用的时候就应该将类名写成:cc.Test$Hello
。
反射 java.lang.Runtime ¶
java.lang.Runtime
因为有一个exec
方法可以执行本地命令,所以在很多的payload
中我们都能看到反射调用Runtime
类来执行本地系统命令,通过学习如何反射Runtime
类也能让我们理解反射的一些基础用法。
Example
如上可以看到,我们可以使用一行代码完成本地命令执行操作,但是如果使用反射就会比较麻烦了,我们不得不需要间接性的调用Runtime
的exec
方法。
Example
// 获取Runtime类对象
Class runtimeClass1 = Class.forName("java.lang.Runtime");
// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);
// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();
// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);
// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);
// 获取命令执行结果
InputStream in = process.getInputStream();
// 输出命令执行结果
System.out.println(org.apache.commons.io.IOUtils.toString(in, "UTF-8"));
反射调用Runtime
实现本地命令执行的流程如下:
- 反射获取
Runtime
类对象 (Class.forName("java.lang.Runtime")
)。 - 使用
Runtime
类的 Class 对象获取Runtime
类的无参数构造方法 (getDeclaredConstructor()
),因为Runtime
的构造方法是private
的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限 (constructor.setAccessible(true)
)。 - 获取
Runtime
类的exec(String)
方法 (runtimeClass1.getMethod("exec", String.class);
)。 - 调用
exec(String)
方法 (runtimeMethod.invoke(runtimeInstance, cmd)
)。
反射创建类实例 ¶
在 Java 的任何一个类都必须有一个或多个构造方法,如果代码中没有创建构造方法那么在类编译的时候会自动创建一个无参数的构造方法。
Runtime 类构造方法示例代码片段 :
Runtime
类的构造方法为 private,所以我们没办法new
一个Runtime
类实例,但示例中我们借助了反射机制,修改了方法访问权限从而间接的创建出了Runtime
对象。
runtimeClass1.getDeclaredConstructor
和runtimeClass1.getConstructor
都可以获取到类构造方法,区别在于后者无法获取到私有方法,所以一般在获取某个类的构造方法时候我们会使用前者去获取构造方法。如果构造方法有一个或多个参数的情况下我们应该在获取构造方法时候传入对应的参数类型数组,如:clazz.getDeclaredConstructor(String.class, String.class)
。
如果我们想获取类的所有构造方法可以使用:clazz.getDeclaredConstructors
来获取一个Constructor
数组。
获取到Constructor
以后我们可以通过constructor.newInstance()
来创建类实例 , 同理如果有参数的情况下我们应该传入对应的参数值,如 :constructor.newInstance("admin", "123456")
。当我们没有访问构造方法权限时我们应该调用constructor.setAccessible(true)
修改访问权限就可以成功的创建出类实例了。
反射调用类方法 ¶
Class
对象提供了一个获取某个类的所有的成员方法的方法,也可以通过方法名和方法参数类型来获取指定成员方法。
获取当前类所有的成员方法:
获取当前类指定的成员方法:
Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);
getMethod
和getDeclaredMethod
都能够获取到类成员方法,区别在于getMethod
只能获取到当前类和父类
的所有有权限的方法 ( 如:public
),而getDeclaredMethod
能获取到当前类的所有成员方法 ( 不包含父类 )。
反射调用方法
获取到java.lang.reflect.Method
对象以后我们可以通过Method
的invoke
方法来调用类方法。
调用类方法代码片段:
method.invoke
的第一个参数必须是类实例对象,如果调用的是static
方法那么第一个参数值可以传null
,因为在 java 中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)
的方式调用。
method.invoke
的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型
。
反射调用成员变量 ¶
Java 反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。
获取当前类的所有成员变量:
获取当前类指定的成员变量:
getField
和getDeclaredField
的区别同getMethod
和getDeclaredMethod
。
获取成员变量值:
修改成员变量值:
同理,当我们没有修改的成员变量权限时可以使用 : field.setAccessible(true)
的方式修改为访问成员变量访问权限。
如果我们需要修改被final
关键字修饰的成员变量,那么我们需要先修改方法
// 反射获取Field类的modifiers
Field modifiers = field.getClass().getDeclaredField("modifiers");
// 设置modifiers修改权限
modifiers.setAccessible(true);
// 修改成员变量的Field对象的modifiers值
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
// 修改成员变量值
field.set(类实例对象, 修改后的值);
Java 反射机制总结 ¶
Java 反射机制是 Java 动态性中最为重要的体现,利用反射机制我们可以轻松的实现 Java 类的动态调用。Java 的大部分框架都是采用了反射机制来实现的 ( 如 :Spring MVC
、ORM框架
等 ),Java 反射在编写漏洞利用代码、代码审计、绕过 RASP 方法限制等中起到了至关重要的作用。
1.3 sun.misc.Unsafe¶
sun.misc.Unsafe
是 Java 底层 API( 仅限 Java 内部使用 , 利用时可通过反射调用 ) 提供的一个神奇的 Java 类,Unsafe 提供了非常底层的内存、CAS、线程调度、类、对象等操作。
获取 Unsafe 对象 ¶
Unsafe 是 Java 内部 API,外部是禁止调用的,在编译 Java 类时如果检测到引用了 Unsafe 类也会有禁止使用的警告:Unsafe是内部专用 API, 可能会在未来发行版中删除
。
可以使用反射的方式去获取Unsafe类实例:
Example
allocateInstance 无视构造方法创建类实例 ¶
Google 的 GSON 库在 JSON 反序列化的时候就使用这个方式来创建类实例,在渗透测试中也会经常遇到这样的限制,比如 RASP 限制了java.io.FileInputStream
类的构造方法导致我们无法读文件或者限制了UNIXProcess/ProcessImpl
类的构造方法导致我们无法执行本地命令等。
defineClass 直接调用 JVM 创建类对象 ¶
如果 ClassLoader 被限制,我们可以使用 Unsafe 的 defineClass 方法来实现通过字节码向 JVM 中注册类。
public native Class defineClass(String var1, byte[] var2, int var3, int var4);
public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);
Example
// 获取系统的类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// 创建默认的保护域
ProtectionDomain domain = new ProtectionDomain(
new CodeSource(null, (Certificate[]) null), null, classLoader, null
);
// 使用Unsafe向JVM中注册cc.classloader.TestHelloWorld类
Class helloWorldClass = unsafe1.defineClass(
TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length, classLoader, domain
);
Unsafe 还可以通过 defineAnonymousClass 方法创建内部类,此处暂不演示
Warning
这个实例仅适用于 Java 8 以前的版本,如在 Java 8 中使用应调用需要传类加载器和保护域的方法。
Java 11开始Unsafe类移除了defineClass
方法(defineAnonymousClass
方法还在)。
1.4 Java 文件系统 ¶
在 Java SE 中内置了两类文件系统:java.io
和java.nio
,java.nio
的实现是sun.nio
,文件系统底层的 API 实现如下图:
java.io¶
Java 抽象出了一个叫做文件系统的对象 :java.io.FileSystem
,不同的操作系统有不一样的文件系统 , 例如Windows
和Unix
就是两种不一样的文件系统: java.io.UnixFileSystem
、java.io.WinNTFileSystem
。
FileSystem 最终会通过 JNI 调用 native 方法来实现对文件的操作
Tips
- 并不是所有的文件操作都在
java.io.FileSystem
中定义 , 文件的读取最终调用的是java.io.FileInputStream#read0、readBytes
、java.io.RandomAccessFile#read0、readBytes
, 而写文件调用的是java.io.FileOutputStream#writeBytes
、java.io.RandomAccessFile#write0
。 - Java 有两类文件系统 API!一个是基于
阻塞模式的IO
的文件系统,另一是 JDK7+ 基于NIO.2
的文件系统。
java.nio¶
Java 7 提出了一个基于 NIO 的文件系统,这个 NIO 文件系统和阻塞 IO 文件系统两者是完全独立的。java.nio.file.spi.FileSystemProvider
对文件的封装和java.io.FileSystem
同理。
合理的利用 NIO 文件系统这一特性我们可以绕过某些只是防御了java.io.FileSystem
的WAF
/RASP
1.5 本地命令执行 ¶
Java 原生提供了对本地系统命令执行的支持,黑客通常会 RCE 或者 WebShell 来执行系统终端命令控制服务器
Runtime 命令执行 ¶
在 Java 中我们通常会使用 java.lang.Runtime 类的 exec 方法来执行本地系统命令。
Runtime.exec(xxx) 调用链如下 :
java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
java.lang.ProcessImpl.start(ProcessImpl.java:134)
java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
java.lang.Runtime.exec(Runtime.java:620)
java.lang.Runtime.exec(Runtime.java:450)
java.lang.Runtime.exec(Runtime.java:347)
org.apache.jsp.runtime_002dexec2_jsp._jspService(runtime_002dexec2_jsp.java:118)
通过观察整个调用链我们可以清楚的看到 exec 方法并不是命令执行的最终点,执行逻辑大致是:
- Runtime.exec(xxx)
- java.lang.ProcessBuilder.start()
- new java.lang.UNIXProcess(xxx)
- UNIXProcess 构造方法中调用了 forkAndExec(xxx) native 方法。
- forkAndExec 调用操作系统级别 fork->exec(*nix)/CreateProcess(Windows) 执行命令并返回 fork/CreateProcess 的 PID。
Tips
Runtime 和 ProcessBuilder 并不是程序的最终执行点
如果系统对 runtime 有相应检测防护机制,可以通过反射来绕过检测
ProcessBuilder 命令执行 ¶
注意到 Runtime.exec 会调用 ProcessBuilder.start(),所以我们可以直接使用 ProcessBuilder 来执行命令。
Example
输入请求http://localhost:8080/process_builder.jsp?cmd=/bin/sh&cmd=-c&cmd=cd%20/Users/;ls%20-la
UNIXProcess/ProcessImpl¶
UNIXProcess 和 ProcessImpl 可以理解本就是一个东西,因为在 JDK9 的时候把 UNIXProcess 合并到了 ProcessImpl 当中。ProcessImpl 其实就是最终调用 native 执行系统命令的类,这个类提供了一个叫 forkAndExec 的 native 方法,如方法名所述主要是通过 fork&exec 来执行本地系统命令。
JNI 命令执行 ¶
Java 可以通过 JNI 的方式调用动态链接库,只需要在动态链接库中写本地命令执行即可
1.7 URLConnection¶
Java 抽象出来了一个 URLConnection
类,它用来表示应用程序以及与 URL 建立通信连接的所有类的超类,通过 URL 类中的 openConnection
方法获取到 URLConnection
的类对象。
URLConnection
支持的协议可以在 sun.net.www.protocol
包下找到 (jdk_17.0.11)
其中每个协议都有一个 Handle, Handle 定义了这个协议如何去打开一个连接。
使用 URL 发起一个简单的请求 :
Code
public class URLConnectionDemo {
public static void main(String[] args) throws IOException {
URL url = new URL("https://www.baidu.com");
// 打开和url之间的连接
URLConnection connection = url.openConnection();
// 设置请求参数
connection.setRequestProperty("user-agent", "javasec");
connection.setConnectTimeout(1000);
connection.setReadTimeout(1000);
...
// 建立实际连接
connection.connect();
// 获取响应头字段信息列表
connection.getHeaderFields();
// 获取URL响应内容
StringBuilder response = new StringBuilder();
BufferedReader in = new BufferedReader(
new InputStreamReader(connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
response.append("/n").append(line);
}
System.out.print(response.toString());
}
}
SSRF¶
SSRF(Server-side Request Forge, 服务端请求伪造 )。 由攻击者构造的攻击链接传给服务端执行造成的漏洞,一般用来在外网探测或攻击内网服务。
服务端提供了可以从其他服务器获取资源的功能,然而并没有对用户的输入以及发起请求的 url 进行过滤 & 限制,形成了 SSRF 漏洞。 通常 ssrf 容易出现的功能点如下面几种场景:
- 抓取用户输入图片的地址并且本地化存储
- 从远程服务器请求资源
- 对外发起网络请求
- ...
Java 中 ssrf 漏洞对使用不同类发起的 url 请求有所区别,如果是 URLConnection|URL 发起的请求,那么对于上文中所提到的所有 protocol 都支持,但是如果经过二次包装或者其他的一些类发出的请求,比如 :
- HttpURLConnection
- HttpClient
- Request
- okhttp
- ...
那么只支持发起 http|https 协议,否则会抛出异常。
如果传入 url 端口存在,则会返回网页源码,如果端口提供非 web 服务,则会爆出 Invalid Http response
或 Connection reset
异常,通过对异常的捕获可以探测内网所有的端口服务。
java 中默认对 (http|https) 做了一些事情,比如 :
- 默认启用了透明 NTLM 认证 (1)
- 默认跟随跳转
- 《Ghidra 从 XXE 到 RCE》 (To be read)
默认跟随跳转 其中有一个坑点
它会对跟随跳转的 url 进行协议判断,所以 Java 的 SSRF 漏洞利用方式整体比较有限 :
- 利用 file 协议读取文件内容(仅限使用 URLConnection|URL 发起的请求)
- 利用 http 进行内网 web 服务端口探测
- 利用 http 进行内网非 web 服务端口探测 ( 如果将异常抛出来的情况下 )
- 利用 http 进行 ntlmrelay 攻击 ( 仅限 HttpURLConnection 或者二次包装 HttpURLConnection 并未复写 AuthenticationInfo 方法的对象 )
1.8 JNI 安全基础 ¶
Java Native Interface (JNI) 是 Java 与本地代码交互的一种技术。Java 语言是基于 C 语言实现的,Java 底层的很多 API 都是通过 JNI 来实现的。通过 JNI 接口 C/C++ 和 Java 可以互相调用 ( 存在跨平台问题 )。Java 可以通过 JNI 调用来弥补语言自身的不足 ( 代码安全性、内存操作等 )。
JNI- 定义 native 方法 ¶
首先在 Java 中如果想要调用 native
方法 , 那么需要在类中先定义一个 native
方法。
如上示例代码,我们使用 native
关键字定义一个类似于接口的方法
JNI- 生成类头文件 ¶
如上,我们已经编写好了 TestJNI.java,现在我们需要编译并生成 c 语言头文件。
javac -cp . TestJNI.java -h .
编译完成后会在当前目录下生成 TestJNI.class
和 cc_colemak_TestJNI.h
。
cc_colemak_TestJNI.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class cc_colemak_TestJNI */
#ifndef _Included_cc_colemak_TestJNI
#define _Included_cc_colemak_TestJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: cc_colemak_TestJNI
* Method: exec
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_cc_colemak_TestJNI_exec
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
JNI- 基础数据类型 ¶
需要特别注意的是 Java 和 JNI 定义的类型需要转换 (1) 才能相互使用
参考如下类型对照表 :
Java 类型 | JNI 类型 | C/C++ 类型 | 大小 |
---|---|---|---|
Boolean | Jblloean | unsigned char | 无符号 8 位 |
Byte | Jbyte | char | 有符号 8 位 |
Char | Jchar | unsigned short | 无符号 16 位 |
Short | Jshort | short | 有符号 16 位 |
Int | Jint | int | 有符号 32 位 |
Long | Jlong | long long | 有符号 64 位 |
Float | Jfloat | float | 32 位 |
Double | Jdouble | double | 64 位 |
JNI- 编写 C/C++ 本地命令执行实现 ¶
接下来使用 C/C++ 编写函数的最终实现代码。
cc_colemak_TestJNI.cpp
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <string>
#include "cc_colemak_TestJNI.h"
using namespace std;
JNIEXPORT jstring
JNICALL Java_cc_colemak_TestJNI_exec
(JNIEnv *env, jclass jclass, jstring str) {
if (str != NULL) {
jboolean jsCopy;
// 将jstring参数转成char指针
const char *cmd = env->GetStringUTFChars(str, &jsCopy);
// 使用popen函数执行系统命令
FILE *fd = popen(cmd, "r");
if (fd != NULL) {
// 返回结果字符串
string result;
// 定义字符串数组
char buf[128];
// 读取popen函数的执行结果
while (fgets(buf, sizeof(buf), fd) != NULL) {
// 拼接读取到的结果到result
result +=buf;
}
// 关闭popen
pclose(fd);
// 返回命令执行结果给Java
return env->NewStringUTF(result.c_str());
}
}
return NULL;
}
- Linux 编译:
g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -shared -o libcmd.jnilib cc_colemak_TestJNI.cpp
- Windows 编译:
g++ -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o cmd.dll cc_colemak_TestJNI.cpp
编译参考 Java Programming Tutorial Java Native Interface (JNI)
cc.colemak.TestJNIcmd
package cc.colemak;
import java.io.File;
import java.lang.reflect.Method;
public class TestJNIcmd {
private static final String COMMAND_CLASS_NAME = "cc.colemak.TestJNI";
/**
* JDK8u191 编译的cc.colemak.TestJNI 类字节码
*/
private static final byte[] COMMAND_CLASS_BYTES = new byte[]{
-54,-2,-70,-66,0,0,0,52,0,15,10,0,3,0,12,7,0,13,7,0,14,1,0,6,60,105,110,105,116,62,1,0,3,40,41,86,1,0,4,67,111,100,101,1,0,15,76,105,110,101,78,117,109,98,101,114,84,97,98,108,101,1,0,4,101,120,101,99,1,0,38,40,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,12,84,101,115,116,74,78,73,46,106,97,118,97,12,0,4,0,5,1,0,18,99,99,47,99,111,108,101,109,97,107,47,84,101,115,116,74,78,73,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,0,33,0,2,0,3,0,0,0,0,0,2,0,1,0,4,0,5,0,1,0,6,0,0,0,29,0,1,0,1,0,0,0,5,42,-73,0,1,-79,0,0,0,1,0,7,0,0,0,6,0,1,0,0,0,3,1,9,0,8,0,9,0,0,0,1,0,10,0,0,0,2,0,11
};
public static void main(String[] args) {
String cmd = "ipconfig";// 定于需要执行的cmd
try {
ClassLoader loader = new ClassLoader(TestJNIcmd.class.getClassLoader()) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
return defineClass(COMMAND_CLASS_NAME, COMMAND_CLASS_BYTES, 0, COMMAND_CLASS_BYTES.length);
}
}
};
// 测试时候换成自己编译好的lib路径
File libPath = new File("cmd.dll");
// load命令执行类
Class commandClass = loader.loadClass("cc.colemak.TestJNI");
// 可以用System.load也加载lib也可以用反射ClassLoader加载,如果loadLibrary0
// 也被拦截了可以换java.lang.ClassLoader$NativeLibrary类的load方法。
// System.loadLibrary("D:\\work\\java\\TestJNI\\src\\main\\java\\cc\\colemak\\cmd");
Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class);
loadLibrary0Method.setAccessible(true);
loadLibrary0Method.invoke(loader, commandClass, libPath);
String content = (String) commandClass.getMethod("exec", String.class).invoke(null, cmd);
System.out.println(content);
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.9 Java 动态代理 ¶
Java 反射提供了一种类动态代理机制,可以通过代理接口实现类来完成程序无侵入式扩展。 Java动态代理主要使用场景:
- 统计方法执行所耗时间。
- 在方法执行前后添加日志。
- 检测方法的参数或返回值。
- 方法访问权限控制。
- 方法 Mock 测试。
动态代理 API ¶
在 java 的java.lang.reflect
包下提供了一个Proxy
类和一个InvocationHandler
接口,通过这个类和接口可以生成动态代理类和动态代理对象。
Proxy类中定义的方法如下:
code
public class Proxy implements java.io.Serializable {
...
//获取动态代理处理类对象
public static InvocationHandler getInvocationHandler(Object proxy)
//创建动态代理类实例
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
//创建动态代理类
public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
//检测某个类是否是动态代理类
public static boolean isProxyClass(Class<?> cl)
//向指定的类加载器中定义一个类对象
private static native Class defineClass0(ClassLoader loader, String name, byte[] b, int off, int len)
}
java.lang.reflect.InvocationHandler
接口用于调用Proxy
类生成的代理类方法,该类只有一个invoke
方法。
code
public interface InvocationHandler {
//代理类调用方法时会调用该方法
Object invoke(Object proxy, Method method, Object [] args) throws Throwable
}
参考官方文档:InvocationHandler
使用 java.lang.reflect.Proxy 动态创建类对象 ¶
code
package cc.dynproxy;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import static cc.colemak.TestJNIcmd.COMMAND_CLASS_BYTES;
import static cc.colemak.TestJNIcmd.COMMAND_CLASS_NAME;
public class ProxyDefineClassTest {
public static void main(String[] args) {
// 获取系统的类加载器,可以根据具体情况换成一个存在的类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
try {
// 反射java.lang.reflect.Proxy类获取其中的defineClass0方法
Method method = Proxy.class.getDeclaredMethod("defineClass0", new Class[]{
ClassLoader.class, String.class, byte[].class, int.class, int.class
});
// 修改方法的访问权限
method.setAccessible(true);
// 反射调用java.lang.reflect.Proxy.defineClass0()方法,动态向JVM注册
// cc.classloader.TestHelloWorld类对象
Class helloWorldClass = (Class) method.invoke(null, new Object[]{
classLoader, COMMAND_CLASS_NAME, COMMAND_CLASS_BYTES, 0, COMMAND_CLASS_BYTES.length
});
// 输出TestHelloWorld类对象
System.out.println(helloWorldClass);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果
创建代理类实例 ¶
Example
// 创建UnixFileSystem类实例
FileSystem fileSystem = new UnixFileSystem();
// 使用JDK动态代理生成FileSystem动态代理类实例
FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
FileSystem.class.getClassLoader(),// 指定动态代理类的类加载器
new Class[]{FileSystem.class}, // 定义动态代理生成的类实现的接口
new JDKInvocationHandler(fileSystem)// 动态代理处理类
);
// 创建UnixFileSystem类实例
FileSystem fileSystem = new UnixFileSystem();
// 创建动态代理处理类
InvocationHandler handler = new JDKInvocationHandler(fileSystem);
// 通过指定类加载器、类实现的接口数组生成一个动态代理类
Class proxyClass = Proxy.getProxyClass(
FileSystem.class.getClassLoader(),// 指定动态代理类的类加载器
new Class[]{FileSystem.class}// 定义动态代理生成的类实现的接口
);
// 使用反射获取Proxy类构造器并创建动态代理类实例
FileSystem proxyInstance = (FileSystem) proxyClass.getConstructor(
new Class[]{InvocationHandler.class}).newInstance(new Object[]{handler}
);
动态代理添加方法调用日志 ¶
UnixFileSystem
类实现了FileSystem
接口,通过 JDK 动态代理的方式为FileSystem
接口方法添加日志输出。
code
package cc.dynproxy;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class JDKInvocationHandler implements InvocationHandler, Serializable {
private final Object target;
public JDKInvocationHandler(final Object target) {
this.target = target;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
if ("toString".equals(method.getName())) {
return method.invoke(target, args);
}
System.out.println("即将调用["+target.getClass().getName()+"."+method.getName()+"]");
Object result = method.invoke(target, args);
System.out.println("完成调用["+target.getClass().getName()+"."+method.getName()+"]");
return result;
}
}
package cc.dynproxy;
import java.io.File;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class FSProxyTest {
public static void main(String[] args) {
FileSystem fs=new UnixFileSystem();
FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
FileSystem.class.getClassLoader(),// 指定动态代理类的类加载器
new Class[]{FileSystem.class}, // 定义动态代理生成的类实现的接口
new JDKInvocationHandler(fs)// 动态代理处理类
);
System.out.println("动态代理生成的类名:" + proxyInstance.getClass());
System.out.println("----------------------------------------------------------------------------------------");
System.out.println("动态代理生成的类名toString:" + proxyInstance.toString());
System.out.println("----------------------------------------------------------------------------------------");
// 使用动态代理的方式UnixFileSystem方法
String[] files = proxyInstance.list(new File("."));
System.out.println("----------------------------------------------------------------------------------------");
System.out.println("UnixFileSystem.list方法执行结果:" + Arrays.toString(files));
System.out.println("----------------------------------------------------------------------------------------");
boolean isFileSystem = proxyInstance instanceof FileSystem;
boolean isUnixFileSystem = proxyInstance instanceof UnixFileSystem;
System.out.println("动态代理类[" + proxyInstance.getClass() + "]是否是FileSystem类的实例:" + isFileSystem);
System.out.println("----------------------------------------------------------------------------------------");
System.out.println("动态代理类[" + proxyInstance.getClass() + "]是否是UnixFileSystem类的实例:" + isUnixFileSystem);
System.out.println("----------------------------------------------------------------------------------------");
}
}
运行结果
动态代理生成的 $ProxyXXX 类代码分析 ¶
java.lang.reflect.Proxy 类通过创建一个新的 Java 类 ( 类名为 com.sun.proxy.$ProxyXXX) 实现无侵入的类方法代理功能。
动态代理生成出来的类有如下技术细节和特性:
- 动态代理的必须是接口类,通过动态生成接口实现类来代理接口的方法调用 ( 反射机制 )。
- 动态代理类由
java.lang.reflect.Proxy.ProxyClassFactory
创建。 ProxyClassFactory
调用sun.misc.ProxyGenerator
类生成该类的字节码,并调用java.lang.reflect.Proxy.defineClass0()
方法将该类注册到 JVM。- 该类继承于
java.lang.reflect.Proxy
并实现了需要被代理的接口类,因为java.lang.reflect.Proxy
类实现了java.io.Serializable
接口,所以被代理的类支持序列化 / 反序列化。 - 通过
ProxyGenerator
动态生成接口类的所有方法 - 因为实现了代理的接口类,所以当前类是代理的接口类的实例 (
proxyInstance instanceof FileSystem
为 true),但不是代理接口类的实现类的实例 (proxyInstance instanceof UnixFileSystem
为 false)。 - 该类方法中包含了被代理的接口类的所有方法,通过调用动态代理处理类 (
InvocationHandler
) 的 invoke 方法获取方法执行结果。 - 该类代理的方式重写了
java.lang.Object
类的toString
、hashCode
、equals
方法。 - 如果动过动态代理生成了多个动态代理类,新生成的类名中的数字会自增,如
com.sun.proxy.$Proxy0/$Proxy1/$Proxy2
。
code
package com.sun.proxy.$Proxy0;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements FileSystem {
private static Method m1;
// 实现的FileSystem接口方法,如果FileSystem里面有多个方法那么在这个类中将从m3开始n个成员变量
private static Method m3;
private static Method m0;
private static Method m2;
public $Proxy0(InvocationHandler var1) {
super(var1);
}
public final boolean equals(Object var1) {
try {
return (Boolean) super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String[] list(File var1) {
try {
return (String[]) super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final int hashCode() {
try {
return (Integer) super.h.invoke(this, m0, (Object[]) null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() {
try {
return (String) super.h.invoke(this, m2, (Object[]) null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("cc.dynproxy.FileSystem").getMethod("list", Class.forName("java.io.File"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m2 = Class.forName("java.lang.Object").getMethod("toString");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
动态代理类实例序列化 ¶
Dynamic Proxy Classes-Serialization
1.10 Java 类序列化 ¶
Java 序列化常被用于 Socket 传输。 在 RMI(Java 远程方法调用 -Java Remote Method Invocation) 和 JMX(Java 管理扩展 -Java Management Extensions) 服务中对象反序列化机制被强制性使用。在 Http 请求中也时常会被用到反序列化机制,如:直接接收序列化请求的后端服务、使用 Base 编码序列化字节字符串的方式传递等。
Java 类实现java.io.Serializable
( 内部序列化 ) 或java.io.Externalizable
( 外部序列化 ) 接口即可被序列化。
java.io.Serializable
是一个空接口,仅用于标记类可以被序列化。序列化机制会根据类的属性生成一个serialVersionUID
,用于判断序列化对象是否一致。反序列化时如果serialVersionUID
不一致会导致InvalidClassException
异常。如果未显式声明,则序列化运行时通过内置方法将计算默认 serialVersionUID
值。
code
package cc.serial;
import java.io.*;
import java.util.Arrays;
public class DeserializationTest implements Serializable {
private String username;
private String email;
public DeserializationTest() {
System.out.println("init...");
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public static void main(String[] args) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
// 创建DeserializationTest类,并类设置属性值
DeserializationTest t = new DeserializationTest();
t.setUsername("colemak");
t.setEmail("admin@javaweb.org");
// 创建Java对象序列化输出流对象
ObjectOutputStream out = new ObjectOutputStream(baos);
// 序列化DeserializationTest类
out.writeObject(t);
out.flush();
out.close();
// 打印DeserializationTest类序列化以后的字节数组,我们可以将其存储到文件中或者通过Socket发送到远程服务地址
System.out.println("DeserializationTest类序列化后的字节数组:" + Arrays.toString(baos.toByteArray()));
// 利用DeserializationTest类生成的二进制数组创建二进制输入流对象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
ObjectInputStream in = new ObjectInputStream(bais);
// 反序列化输入流数据为DeserializationTest对象
DeserializationTest test = (DeserializationTest) in.readObject();
System.out.println("用户名:" + test.getUsername() + ",邮箱:" + test.getEmail());
// 关闭ObjectInputStream输入流
in.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

ObjectOutputStream
序列化类对象的主要流程是首先判断序列化的类是否重写了writeObject
方法,如果重写了就调用对象自身的writeObject
方法,序列化时会先写入类名信息,其次是写入成员变量信息 ( 通过反射获取所有不包含被transient
修饰的变量和值 )。
java.io.Externalizable
接口定义了writeExternal
和readExternal
方法需要序列化和反序列化的类实现,其余的和java.io.Serializable
并无差别。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
code
package cc.serial;
import java.io.*;
import java.util.Arrays;
public class ExternalizableTest implements java.io.Externalizable {
private String username;
private String email;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(username);
out.writeObject(email);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.username = (String) in.readObject();
this.email = (String) in.readObject();
}
public static void main(String[] args) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
// 创建ExternalizableTest类,并类设置属性值
ExternalizableTest t = new ExternalizableTest();
t.setUsername("yz");
t.setEmail("admin@javaweb.org");
ObjectOutputStream out = new ObjectOutputStream(baos);
out.writeObject(t);
out.flush();
out.close();
// 打印ExternalizableTest类序列化以后的字节数组,我们可以将其存储到文件中或者通过Socket发送到远程服务地址
System.out.println("ExternalizableTest类序列化后的字节数组:" + Arrays.toString(baos.toByteArray()));
System.out.println("ExternalizableTest类反序列化后的字符串:" + new String(baos.toByteArray()));
// 利用DeserializationTest类生成的二进制数组创建二进制输入流对象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
// 通过反序列化输入流创建Java对象输入流(ObjectInputStream)对象
ObjectInputStream in = new ObjectInputStream(bais);
// 反序列化输入流数据为ExternalizableTest对象
ExternalizableTest test = (ExternalizableTest) in.readObject();
System.out.println("用户名:" + test.getUsername() + ",邮箱:" + test.getEmail());
// 关闭ObjectInputStream输入流
in.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

反序列化类对象时有如下限制:
- 被反序列化的类必须存在。
serialVersionUID
值必须一致。
反序列化类对象不会调用该类构造方法
因为在反序列化创建类实例时使用了sun.reflect.ReflectionFactory.newConstructorForSerialization
创建了一个反序列化专用的Constructor
( 反射构造方法对象 ),其可以绕过构造方法创建类实例 ( 前面章节讲sun.misc.Unsafe
的时候我们提到了使用allocateInstance
方法也可以实现绕过构造方法创建类实例 )。
使用反序列化方式创建类实例
code
package cc.serial;
import sun.reflect.ReflectionFactory;
import java.lang.reflect.Constructor;
public class ReflectionFactoryTest {
public static void main(String[] args) {
try{
// 获取sun.reflect.ReflectionFactory对象
ReflectionFactory factory = ReflectionFactory.getReflectionFactory();
// 使用反序列化方式获取DeserializationTest类的构造方法
Constructor constructor = factory.newConstructorForSerialization(
DeserializationTest.class, Object.class.getConstructor()
);
// 实例化DeserializationTest对象
System.out.println(constructor.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序运行结果如下,观察到未调用构造函数输出 "init":
自定义序列化 (writeObject) 和反序列化 (readObject) 方法 ¶
实现了java.io.Serializable
接口的类,可以定义如下方法 ( 反序列化魔术方法 ),这些方法将会在类序列化或反序列化过程中调用:
- private void writeObject(ObjectOutputStream oos), 自定义序列化。
- private void readObject(ObjectInputStream ois),自定义反序列化。
- private void readObjectNoData()。
- protected Object writeReplace(),写入时替换对象。
- protected Object readResolve()。
具体的方法名定义在java.io.ObjectStreamClass#ObjectStreamClass(java.lang.Class<?>)
,其中方法有详细的声明。
1.11 JNDI¶
JNDI(Java Naming and Directory Interface,Java 命名和目录接口 ) 是 SUN 公司提供的一种标准的 Java 命名系统接口,JNDI 提供统一的客户端 API,通过不同的访问提供者接口 JNDI 服务供应接口 (SPI) 的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。
JNDI(Java Naming and Directory Interface) 是一个应用程序设计的 API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似 JDBC 都是构建在抽象层上。现在 JNDI 已经成为 J2EE 的标准之一,所有的 J2EE 容器都必须提供一个 JNDI 的服务。
JNDI 可访问的现有的目录及服务有: DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。
InitialContext¶
InitialContext()
// 构建一个初始上下文。
InitialContext(boolean lazy)
// 构造一个初始上下文,并选择不初始化它。
InitialContext(Hashtable<?,?> environment)
// 使用提供的环境构建初始上下文。
bind(Name name, Object obj)
// 将名称绑定到对象。
list(String name)
// 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
lookup(String name)
// 检索命名对象。
rebind(String name, Object obj)
// 将名称绑定到对象,覆盖任何现有绑定。
unbind(String name)
// 取消绑定命名对象。
Reference¶
该类表示对在命名 / 目录系统外部找到的对象的引用。提供了 JNDI 中类的引用功能,比如在某些目录服务中直接引用远程的 Java 对象。
Reference(String className)
// 为类名为“className”的对象构造一个新的引用。
Reference(String className, RefAddr addr)
// 为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
// 为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, String factory, String factoryLocation)
// 为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
void add(int posn, RefAddr addr)
将地址添加到索引posn的地址列表中。
void add(RefAddr addr)
将地址添加到地址列表的末尾。
void clear()
从此引用中删除所有地址。
RefAddr get(int posn)
检索索引posn上的地址。
RefAddr get(String addrType)
检索地址类型为“addrType”的第一个地址。
Enumeration<RefAddr> getAll()
检索本参考文献中地址的列举。
String getClassName()
检索引用引用的对象的类名。
String getFactoryClassLocation()
检索此引用引用的对象的工厂位置。
String getFactoryClassName()
检索此引用引用对象的工厂的类名。
Object remove(int posn)
从地址列表中删除索引posn上的地址。
int size()
检索此引用中的地址数。
String toString()
生成此引用的字符串表示形式。
Example
package com.rmi.demo;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class jndi {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1:8080";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("aa",referenceWrapper);
}
}
JNDI 目录服务 ¶
访问 JNDI 目录服务时会通过预先设置好环境变量访问对应的服务, 如果创建 JNDI 上下文 (Context) 时未指定环境变量对象,JNDI 会自动搜索系统属性 (System.getProperty()
)、applet
参数和应用程序资源文件 (jndi.properties
)
Example
Context.INITIAL_CONTEXT_FACTORY
( 初始上下文工厂的环境属性名称 ) 指的是 JNDI 服务处理的具体类名称,如:DNS 服务可以使用com.sun.jndi.dns.DnsContextFactory
类来处理,JNDI 上下文工厂类必须实现javax.naming.spi.InitialContextFactory
接口,通过重写getInitialContext
方法来创建服务。
- JNDI DNS 服务:
JNDI支持访问DNS服务,注册环境变量时设置JNDI服务处理的工厂类为
com.sun.jndi.dns.DnsContextFactory
即可。 - JNDI-RMI 远程方法调用
RMI的服务处理工厂类是:
com.sun.jndi.rmi.registry.RegistryContextFactory
- JNDI-LDAP
LDAP的服务处理工厂类是:
com.sun.jndi.ldap.LdapCtxFactory
- JNDI-DataSource JNDI连接数据源比较特殊,Java目前不提供内置的实现方法,提供数据源服务的多是Servlet容器,以Tomcat为例,参考Tomcat JNDI Datasource
JNDI- 协议转换
如果JNDI
在lookup
时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创建处理对应的服务请求。
JNDI
默认支持自动转换的协议有:
协议名称 | 协议 URL | Context 类 |
---|---|---|
DNS 协议 | dns:// |
com.sun.jndi.url.dns.dnsURLContext |
RMI 协议 | rmi:// |
com.sun.jndi.url.rmi.rmiURLContext |
LDAP 协议 | ldap:// |
com.sun.jndi.url.ldap.ldapURLContext |
LDAP 协议 | ldaps:// |
com.sun.jndi.url.ldaps.ldapsURLContextFactory |
IIOP 对象请求代理协议 | iiop:// |
com.sun.jndi.url.iiop.iiopURLContext |
IIOP 对象请求代理协议 | iiopname:// |
com.sun.jndi.url.iiopname.iiopnameURLContextFactory |
IIOP 对象请求代理协议 | corbaname:// |
com.sun.jndi.url.corbaname.corbanameURLContextFactory |
RMI/LDAP 远程对象引用安全限制 ¶
在 RMI 服务中引用远程对象将受本地 Java 环境限制即本地的java.rmi.server.useCodebaseOnly
配置必须为 false( 允许加载远程对象 ),如果该值为 true 则禁止引用远程对象。除此之外被引用的 ObjectFactory 对象还将受到com.sun.jndi.rmi.object.trustURLCodebase
配置限制,如果该值为 false( 不信任远程引用对象 ) 一样无法调用远程的引用对象。
JDK 5 U45,JDK 6 U45,JDK 7u21,JDK 8u121 开始java.rmi.server.useCodebaseOnly
默认配置已经改为了 true。
JDK 6u132, JDK 7u122, JDK 8u113开始com.sun.jndi.rmi.object.trustURLCodebase
默认值已改为了false。
本地测试远程对象引用可以使用如下方式允许加载远程的引用对象:
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
LDAP 在 JDK 11.0.1、8u191、7u201、6u211 后也将默认的com.sun.jndi.ldap.object.trustURLCodebase
设置为了 false。
高版本 JDK 可参考:如何绕过高版本 JDK 的限制进行 JNDI 注入利用
JNDI 注入漏洞参考:JNDI 注入漏洞
1.12 JShell¶
从 Java 9 开始提供了一个叫 jshell 的功能,jshell 是一个 REPL(Read-Eval-Print Loop) 命令行工具,提供了一个交互式命令行界面,在 jshell 中我们不再需要编写类也可以执行 Java 代码片段,开发者可以像 python 和 php 一样在命令行下愉快的写测试代码了。
命令行执行 jshell 即可进入 jshell 模式:
使用 JShell 执行代码片段
jshell 不仅是一个命令行工具,在我们的应用程序中同样也可以调用 jshell 内部的实现 API,也就是说我们可以利用 jshell 来执行 Java 代码片段
jshell.jsp 一句话木马示例 :
1.13 Java 字节码 ¶
Java 源文件 (*.java) 通过编译后会变成 class 文件,class 文件有固定的二进制格式,class 文件的结构在 JVM 虚拟机规范第四章 The class File Format 中有详细的说明。本章节将学习 class 文件结构、class 文件解析、class 文件反编译以及 ASM 字节码库。(1)
Java class 文件格式 ¶
class 文件结构如下:
ClassFile {
u4 magic; // (1)!
u2 minor_version;
u2 major_version; // (2)!
u2 constant_pool_count; // (3)!
cp_info constant_pool[constant_pool_count-1]; // (4)!
u2 access_flags; // (5)!
u2 this_class; // (6)!
u2 super_class; // (7)!
u2 interfaces_count; // (8)!
u2 interfaces[interfaces_count]; // (9)!
u2 fields_count; // (10)!
field_info fields[fields_count];// (11)!
u2 methods_count;// (12)!
method_info methods[methods_count];// (13)!
u2 attributes_count;// (14)!
attribute_info attributes[attributes_count];// (15)!
}
- 魔数是 class 文件的标识符,固定值为
0xCAFEBABE
,JVM 加载 class 文件时会先读取魔数校验文件类型。 - class 文件的版本号由两个 u2 组成(
u2 minor_version
,u2 major_version
) ,分别表示的是副版本号和主版本号 u2 constant_pool_count
表示的是常量池中的数量,constant_pool_count
的值等于常量池中的数量加 1,需要特别注意的是 long 和 double 类型的常量池对象占用两个常量位。cp_info constant_pool[constant_pool_count-1]
是一种表结构,cp_info 表示的是常量池对象。[u1 tag : u1 info], 每种 tag 表示一种数据类型,具体参考手册u2 access_flags
表示类或者接口的访问权限及属性。u2 this_class
表示当前 class 文件的类名所在常量池中的索引位置u2 super_class
表示当前 class 文件的父类类名所在常量池中的索引位置。java.lang.Object
类的 super_class 的为 0u2 interfaces_count
表示当前类继承或实现的接口数u2 interfaces[interfaces_count]
表示所有接口数组u2 fields_count
表示当前 class 中的成员变量个数field_info fields[fields_count]
表示当前类的所有成员变量属性结构:field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
u2 access_flags;
表示的是成员变量的修饰符;u2 name_index;
表示的是成员变量的名称;u2 descriptor_index;
表示的是成员变量的描述符;u2 attributes_count;
表示的是成员变量的属性数量;attribute_info attributes[attributes_count];
表示的是成员变量的属性信息;
u2 methods_count
表示当前 class 中的成员方法个数。method_info methods[methods_count]
表示的是当前 class 中的所有成员方法 method_info 数据结构:属性结构:method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
u2 access_flags;
表示的是成员方法的修饰符;u2 name_index;
表示的是成员方法的名称;u2 descriptor_index;
表示的是成员方法的描述符;u2 attributes_count;
表示的是成员方法的属性数量;attribute_info attributes[attributes_count];
表示的是成员方法的属性信息;
u2 attributes_count
表示当前 class 文件属性表的成员个数。attribute_info attributes[attributes_count];
表示的是当前 class 文件的所有属性attribute_info
数据结构:u2 attribute_name_index;
表示的是属性名称索引,读取attribute_name_index
值所在常量池中的名称可以得到属性名称。
在 JVM 规范中u1
、u2
、u4
分别表示的是 1、2、4 个字节的无符号数,可使用java.io.DataInputStream
类中的对应方法:readUnsignedByte
、readUnsignedShort
、readInt
方法读取。表结构 ( table
) 由任意数量的可变长度的项组成,用于表示 class 中的复杂结构,如上述的:cp_info
、field_info
、method_info
、attribute_info
。
Java class 文件解析 ¶
- 巨长,特别是属性解析部分,需要时间整理(前面的世界以后再来探索吧)
JVM 指令集 ¶
类型 / 方法描述符 ¶
类型描述符表
描述符 | 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; |
方法描述符示例
方法示例 | 描述符 | 描述 |
---|---|---|
static{...} ,static int id = 1; |
方法名:<clinit> |
静态语句块 / 静态变量初始化 |
public Test (){...} |
方法名:<init> ,描述符()V |
构造方法 |
void hello(){...} |
()V |
V 表示void ,无返回值 |
Object login(String str) {...} |
(Ljava/lang/String;)Ljava/lang/Object; |
普通方法,返回 Object 类型 |
void login(String str) {...} |
(Ljava/lang/String;)V |
普通方法,无返回值 |
JVM 指令 ¶
参见官方文档(之前安卓逆向的时候看过一点)
Java 类字节码编辑 ¶
1.14 Java Agent¶
JDK1.5
开始,Java
新增了Instrumentation(Java Agent API)
和JVMTI(JVM Tool Interface)
功能,允许JVM
在加载某个class文件
之前对其字节码进行修改,同时也支持对已加载的class(类字节码)
进行重新加载 (Retransform
)。
利用Java Agent
这一特性衍生出了APM(Application Performance Management,应用性能管理)
、RASP(Runtime application self-protection,运行时应用自我保护)
、IAST(Interactive Application Security Testing,交互式应用程序安全测试)
等相关产品,它们都无一例外的使用了Instrumentation/JVMTI
的API
来实现动态修改Java类字节码
并插入监控或检测代码。
Java Agent
有两种运行模式:
- 启动
Java程序
时添加-javaagent(Instrumentation API实现方式)
或-agentpath/-agentlib(JVMTI的实现方式)
参数,如java -javaagent:/data/XXX.jar LingXeTest
。 JDK1.6
新增了attach(附加方式)
方式,可以对运行中的Java进程
附加Agent
。
这两种运行方式的最大区别在于第一种方式只能在程序启动时指定Agent
文件,而attach
方式可以在Java程序
运行后根据进程ID
动态注入Agent
到JVM
。
Java Agent 和普通的 Java 类相似premain
(Agent 模式)和agentmain
(Attach 模式)为 Agent 程序的入口,如下:
public static void premain(String args, Instrumentation inst) {}
public static void agentmain(String args, Instrumentation inst) {}
Java Agent 限制必须以 jar 包的形式运行或加载。此外,Java Agent 强制要求所有的 jar 文件中必须包含/META-INF/MANIFEST.MF
文件,且该文件中需要定义Premain-Class
(Agent 模式)或Agent-Class:
(Agent 模式)配置,如:
如果需要修改已经被 JVM 加载过的类的字节码,需要在设置MANIFEST.MF
中添加Can-Retransform-Classes: true
或Can-Redefine-Classes: true
。
Instrumentation¶
java.lang.instrument.Instrumentation
是监测JVM
中运行程序的API
,功能如下:
- 动态添加或移除自定义的
ClassFileTransformer
(addTransformer/removeTransformer
) ,JVM 会在类加载时调用 Agent 中注册的ClassFileTransformer
; - 动态修改
classpath
(appendToBootstrapClassLoaderSearch
、 appendToSystemClassLoaderSearch
) ,将 Agent 程序添加到BootstrapClassLoader
和SystemClassLoaderSearch
(对应的是ClassLoader类的getSystemClassLoader方法
,默认是sun.misc.Launcher$AppClassLoader
)中搜索; - 动态获取所有
JVM
已加载的类 (getAllLoadedClasses
); - 动态获取某个类加载器已实例化的所有类 (
getInitiatedClasses
)。 - 重定义某个已加载的类的字节码 (
redefineClasses
)。 - 动态设置
JNI
前缀 (setNativeMethodPrefix
),可以实现 Hook native 方法。 - 重新加载某个已经被 JVM 加载过的类字节码
retransformClasses
。
ClassFileTransformer¶
java.lang.instrument.ClassFileTransformer
是一个转换类文件的代理接口,我们可以在获取到Instrumentation
对象后通过addTransformer
方法添加自定义类文件转换器。
addTransformer
可以注册自定义的Transformer
到Java Agent
,当有新的类被JVM
加载时JVM
会自动回调Transformer
类的transform
方法,传入该类的transform
信息 ( 类名、类加载器、类字节码
等 ),我们可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后将新的类字节码返回给JVM
,JVM
会验证类和相应的修改是否合法,如果符合类加载要求JVM
会加载修改后的类字节码。
ClassFileTransformer
package java.lang.instrument;
public interface ClassFileTransformer {
/**
* 类文件转换方法,重写transform方法可获取到待加载的类相关信息
*
* @param loader 定义要转换的类加载器;如果是引导加载器,则为 null
* @param className 类名,如:java/lang/Runtime
* @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
* @param protectionDomain 要定义或重定义的类的保护域
* @param classfileBuffer 类文件格式的输入字节缓冲区(不得修改)
* @return 字节码byte数组。
*/
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer);
}
Warning
重写transform
方法需要注意以下事项:
ClassLoader
如果是被Bootstrap ClassLoader(引导类加载器)
所加载那么loader
参数的值是空。- 修改类字节码时需要特别注意插入的代码在对应的
ClassLoader
中可以正确的获取到,否则会报ClassNotFoundException
,比如修改java.io.FileInputStream(该类由Bootstrap ClassLoader加载)
时插入了我们检测代码,那么我们将必须保证FileInputStream
能够获取到我们的检测代码类。 JVM
类名的书写方式路径方式:java/lang/String
而不是我们常用的类名方式:java.lang.String
。- 类字节必须符合
JVM
校验要求,如果无法验证类字节码会导致JVM
崩溃或者VerifyError(类验证错误)
。 - 如果修改的是
retransform
类 ( 修改已被JVM
加载的类 ),修改后的类字节码不得新增方法
、修改方法参数
、类成员变量
。 addTransformer
时如果没有传入retransform
参数 ( 默认是false
) 就算MANIFEST.MF
中配置了Can-Redefine-Classes: true
而且手动调用了retransformClasses
方法也一样无法retransform
。- 卸载
transform
时需要使用创建时的Instrumentation
实例。
Agent 实现破解 License ¶
Todo 感觉很有意思,有时间再研究
创建日期: 2024年7月18日 16:28:43