Jdk17 反射限制与绕过

反射的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@CallerSensitive
@ForceInline // to ensure Reflection.getCallerClass optimization
@IntrinsicCandidate
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz,
Modifier.isStatic(modifiers) ? null : obj.getClass(),
modifiers);
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

获取到反射调用的对象和方法的时候,首先进行了checkAccess方法,对反射进行了检验

image-20250211204934670

然后调用了 MethodAccessor 类的 invoke 方法进行进一步处理

1
2
3
4
5
MethodAccessor 是一个接口,定义了方法调用的具体操作,而它有三个具体的实现类:
sun.reflect.DelegatingMethodAccessorImpl
sun.reflect.MethodAccessorImpl
sun.reflect.NativeMethodAccessorImpl

判断的逻辑在acquireMethodAccessor() 中

image-20250211205518886

判断缓存中是否存在对应的MethodAccessor 对象,存在就使用代码复用,不存在就调用 ReflectionFactory生成一个

image-20250211205659744

这里把NativeMethodAccessorImpl做为DelegatingMethodAccessorImpl的参数

image-20250211210124562

实际上就是给delegate参数赋值 newMethodAccessor 方法最终返回 DelegatingMethodAccessorImpl 类对象

image-20250211210228196

走到DelegatingMethodAccessorImpl 类的 invoke 中,根据delegate 属性调用invoke方法

image-20250211210311731

在 NativeMethodAccessorImpl 的 invoke 方法里,其会判断调用次数是否超过阀值,然后进行调用

image-20250211210334531MethodAccessor 实际上就是生成了具体反射类的入口

反射的限制

Migrating From JDK 8 to Later JDK Releases

jdk17是强封装的,意味着java*包下的所有非public字段,在我们尝试反射调用的时候会报错

1
2
3
4
5
6
7
8
9
public class UserService {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException {
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Desktop\\life\\ctf\\target\\classes\\evil.class"));
Method defineClass= ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class<?> shell = (Class<?>) defineClass.invoke(ClassLoader.getSystemClassLoader(), "evil", bytes, 0, bytes.length);
shell.newInstance();
}
}

调用的字节码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import java.io.IOException;

public class evil {
public evil() {
}

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
var1.printStackTrace();
}

}
}

在jdk8版本可以正常rce

image-20250213162218560

在jdk17 会抛出如下错误

image-20250213162302862

我们无法通过反射调用java.lang.ClassLoader.defineClass,这是一个protect的属性

简单调试一下

image-20250213163150972

关注这个checkCanSetAccessible函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
final void checkCanSetAccessible(Class<?> caller, Class<?> declaringClass) {
checkCanSetAccessible(caller, declaringClass, true);
}

private boolean checkCanSetAccessible(Class<?> caller,
Class<?> declaringClass,
boolean throwExceptionIfDenied) {
if (caller == MethodHandle.class) {
throw new IllegalCallerException(); // should not happen
}

Module callerModule = caller.getModule();
Module declaringModule = declaringClass.getModule();

if (callerModule == declaringModule) return true;
if (callerModule == Object.class.getModule()) return true;
if (!declaringModule.isNamed()) return true;

String pn = declaringClass.getPackageName();
int modifiers;
if (this instanceof Executable) {
modifiers = ((Executable) this).getModifiers();
} else {
modifiers = ((Field) this).getModifiers();
}

// class is public and package is exported to caller
boolean isClassPublic = Modifier.isPublic(declaringClass.getModifiers());
if (isClassPublic && declaringModule.isExported(pn, callerModule)) {
// member is public
if (Modifier.isPublic(modifiers)) {
return true;
}

// member is protected-static
if (Modifier.isProtected(modifiers)
&& Modifier.isStatic(modifiers)
&& isSubclassOf(caller, declaringClass)) {
return true;
}
}

// package is open to caller
if (declaringModule.isOpen(pn, callerModule)) {
return true;
}

if (throwExceptionIfDenied) {
// not accessible
String msg = "Unable to make ";
if (this instanceof Field)
msg += "field ";
msg += this + " accessible: " + declaringModule + " does not \"";
if (isClassPublic && Modifier.isPublic(modifiers))
msg += "exports";
else
msg += "opens";
msg += " " + pn + "\" to " + callerModule;
InaccessibleObjectException e = new InaccessibleObjectException(msg);
if (printStackTraceWhenAccessFails()) {
e.printStackTrace(System.err);
}
throw e;
}
return false;
}

可以看到返回true的条件如下

1
2
3
4
5
6
1.调用的变量所在模块和调用者所在模块相同
2.调用者所在模块跟Object所在模块相同
3.调用模块没有定义
4.调用属性值是public
5.调用属性是protected并且是static
6.在模块define中,定义了该属性值是open的

unsafe类

​ Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。

需要注意的是仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常

该类位于sun.misc下,所以不受反射限制,我们可以通过反射获取相关类的实例

1
2
3
4
5
Class<?> unsafe=Class.forName("sun.misc.Unsafe");
Field field= unsafe.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafeclass = (Unsafe) field.get(null);
System.out.println(unsafeclass);

image-20250215132821576

unsafen内部提供了很多系统层的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
public native long objectFieldOffset(Field f);

// 获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);

// 给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);

// 从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
public native Object getObjectVolatile(Object o, long offset);

// 存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
public native void putObjectVolatile(Object o, long offset, Object x);

// 有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效
public native void putOrderedObject(Object o, long offset, Object x);

// 绕过构造方法、初始化代码来创建对象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

Module_bypass

我们可以获取到当前调用对象之后,使用unsafe类修改当前的module(调用类)与object类相同,这样就可以绕过bypass

1
2
3
4
5
6
7
8
9
10
        Class<?> unsafe=Class.forName("sun.misc.Unsafe"); //通过反射获取 sun.misc.Unsafe 的 Class 对象
Field field= unsafe.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafeclass = (Unsafe) field.get(null);// 反射获取 Unsafe 的唯一实例
// System.out.println(unsafeclass);
Module baseModule=Object.class.getModule();//获取Object类的Module地址
Class<?> currentClass= UserService.class;//获取当前运行类,也就是UserService
long addr=unsafeclass.objectFieldOffset(Class.class.getDeclaredField("module")); //使用objectFieldOffset方法,获取 module 字段在 Class 对象中的偏移地址
// System.out.println(addr);
unsafeclass.getAndSetObject(currentClass,addr,baseModule);//这里吧object的值设置到里面去

再次尝试反射调用defineclass加载恶意字节码看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserService {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException, ClassNotFoundException, NoSuchFieldException {

Class<?> unsafe=Class.forName("sun.misc.Unsafe"); //通过反射获取 sun.misc.Unsafe 的 Class 对象
Field field= unsafe.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafeclass = (Unsafe) field.get(null);// 反射获取 Unsafe 的唯一实例
// System.out.println(unsafeclass);
Module baseModule=Object.class.getModule();//获取Object类的Module地址
Class<?> currentClass= UserService.class;//获取当前运行类,也就是UserService
long addr=unsafeclass.objectFieldOffset(Class.class.getDeclaredField("module")); //使用objectFieldOffset方法,获取 module 字段在 Class 对象中的偏移地址
// System.out.println(addr);
unsafeclass.getAndSetObject(currentClass,addr,baseModule);//对对象的字段进行安全的更新操作,这里吧object的值设置到里面去
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Desktop\\life\\ctf\\target\\classes\\evil.class"));
Method defineClass= ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class<?> shell = (Class<?>) defineClass.invoke(ClassLoader.getSystemClassLoader(), "evil", bytes, 0, bytes.length);
shell.newInstance();
}
}

成功反射,绕过限制

image-20250215135212681

调试以下,看看区别。

image-20250215141148886

可以看到成功修改了这里的module

也可以修改被调用的对象与当前的对象相同,也就是获取到java.*下的module和当前这个代码的相同,不过可能会报错(

利用

patch当前对象类的module