Xalan 您所在的位置:网站首页 漏洞产生的主要原因 Xalan

Xalan

2023-04-09 14:30| 来源: 网络整理| 查看: 265

TLDR

本文是对CVE-2022-34169的学习,主要参考thanat0s大佬的文章这是第一次遇到与 Java Class 字节码相关的漏洞,漏洞类型有点偏向于二进制溢出漏洞。在分析漏洞过程中加强了对Java字节码的了解,学到了很多。后面也尝试并成功构造了JDK-Xalan的paylod。

0x01 概述什么是XSLT

可扩展样式表转换语言(英语:Extensible Stylesheet Language Transformations,缩写XSLT)是一种样式转换标记语言,可以将XML资料档转换为另外的XML或其它格式,如HTML网页,纯文字。XSLT最末的T字母表示英语中的“转换”(transformation)。它是XSL规范中的一部分,目前最新的建议版本为XSL 3.0。

什么是Xalan-J

Xalan-J 是 Apache 开源项目下的一个 XSLT 处理器的 Java 版本实现

漏洞产生原因

Xalan-Java 即时编译器(JIT) 会将传入的 XSLT 样式表使用 BCEL 动态生成 Java Class 字节码文件(Class 文件结构如下),XSLT 样式表中的字符串(String) 以及 >32767 的数值将存入到字节码的常量池表(constant_pool) 中。漏洞产生的原因在于 Class 字节码规范中限制了常量池计数器大小(constant_pool_count) 为 u2 类型(2个无符号字节大小),所以 BCEL 在写入 > 0xffff(65535)数量的常量时需要进行截断处理,但是通过上面 dump() 方法中的代码可以看到,BCEL 虽然对 constant_pool_count 数值进行了处理,但实际依旧写入了 > 0xffff 数量的常量,因此大于 constant_pool_count 部分的常量最终将覆盖 access_flags 及后续部分的内容

12345678910BCEL 的内部常量池表示使用标准的 Java 数组来存储常量,并且不对其长度施加任何限制。当生成的类文件在编译过程结束时被序列化时,数组长度被截短,但完整的数组被写出:// org.apache.bcel.classfile.ConstantPool#dumppublic 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); } }} 123456789101112131415161718ClassFile { 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]; // 属性表} 1234cp_info { u1 tag; u1 info[];} 0x02 常量池 常量池:用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区/元空间的运行时常量池中存放 常量池计数器:从 1 开始,也即 constant_pool_count=1 时表示常量池中有 0 个常量项,第 0 项常量用于表达不引用任何一个常量池项目的情况,常量池对于 Class 文件中的字段和方法等解析至关重要

可以使用 Java 自带的工具 javap 查看字节码文件中的常量池内容:javap -v select.class也可以使用 Classpy GUI 工具进行查看,该工具在点击左侧相应字段信息时会在右侧定位出相应的十六进制范围,在构造利用时提供了很大的帮助但是这两个工具无法对首部结构正确的畸形字节码文件进行解析(只输出正确结构的部分),并且未找到合适的解析工具

常量池表中具体存储的数据结构如下,根据 tag 标识来决定后续字节码所表达的含义:

根据参考文章测试分析得出结论:

增加常量池计数器的值的方法一:使用不同的字符串可以字符串数量x2的形式增加常量池计数器的值。AA 和 AAA 实际属于不同的常量。但以这种方式增加常量(),随着 n 不断的增加,所花费的时间也越来越大. 增加常量池计数器的值的方法二:解决方法是使用增加属性替代增加元素的方式增加常量池(每增加一对属性,常量池+4)。原因在于每新增一个元素(element)都将有translate()方法调用的开销,而新增属性只是增加一个 Hashtable#put()方法调用,因此将大大减少执行时间。方式如下12//下面增加了[(tn-1)/2+2] 增加常量池计数器的值的方法三:除了可以通过字符串的形式增加常量池,根据漏洞作者的提示可以通过方法调用的形式添加数值类型的常量(数值需要 >32767 才会存储至常量池表中),如通过调用 java.lang.Math#ceil(double) 方法传入double数值类型,因为double属于基本数据类型,因此只会增加一个CONSTANT_Integer_info数据结构,所以每增加一个double数值,常量池+1

具体原因:尝试在 select.xslt 文件中添加 并生成 Class 文件。查看Class文件字节码可以发现,对应到常量池中实际将增加 CONSTANT_String_info 和 CONSTANT_utf8_info 两项,其中 #092(CONSTANT_utf8_info) 中存储着字面量 AAA,#093(CONSTANT_String_info) 的 string_index 则指向 AAA 字面量所处的下标。为了节省空间,对于相同的常量在常量池中只会存储一份,所以如下内容所生成的 Class 文件中的常量池计数器值依旧为 139

123456789

需要注意的是 AA 和 AAA 实际属于不同的常量,将得到的常量池计数器值为:139+2=141,因此:使用不同的字符串可以 字符串数量x2 的形式增加常量池计数器的值

0x03 Class结构图

这里先展示一下整个 Class 文件最终构造的结构图,接下来将针对各个部分进行说明

0x04 payload构造

想详细了解 Java Class 字节码文件结构的可以参考链接:The Class File Format

access_flags & this_class

access_flags 第一个字节对应常量池的 tag,而 tag 值将决定后续的数据结构(查阅前面常量池结构表)access_flags 的值决定了类的访问标识,如是否为 public ,是否为抽象类等等,如下为各个标识对应的mask 值,当与操作值 != 0时则会增加相应的修饰符在确定 access_flag 第一个字节的值(后续使用x1,x2..代替)之前,需要知道编译后的字节码会被进行怎样的处理。可以看到最终将得到 TemplatesImpl 对象,其中 _bytecodes 即为 XSLT 样式表编译后的字节码内容,熟悉 Java 反序列化漏洞的应该对 TemplatesImpl 类不陌生,之后 newTransformer() 方法调用将会触发 defineClass() 及 newInstance() 方法的调用由于 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 的数量,所以一旦发生变动,后续内容就会需要跟着变动,不太好控制。而 access_flags.x2 的值这里将其设置为 0x07,而不使用 0x01 的原因在于,其值的设定会影响到常量池的大小,根据后续构造发现常量池大小需要满足 > 0x0600(1536) 大小,这部分后续methods[1].attributes[0]也会再进行说明

通过写入 tag = 6 的 double 数值常量(java.lang.Math#ceil(double)),可以实现连续控制 8 个字节内容,可借助如下脚本实现十六进制转换:

1234567891011import structimport decimalctx = decimal.Context()ctx.prec = 20def to_double(b): f = struct.unpack('>d', struct.pack('>Q', b))[0] d1 = ctx.create_decimal(repr(f)) return format(d1, 'f') to_double(0x0006000000000002)

所以 this_class.x2 = 0x06,根据前面可知,this_class 是一个指向常量池的 常量池索引,所以为了使得截断后的常量池最小,所以这个值需要尽可能的小,由于0x0006这个常量池已经被占用了,无法进行截断,所以最终确定值为 this_class = 0x0106(262)

这里的08070106中的0701还有另一层含义:String 类型的 string_index 指向前一项 Utf8 字面量的下标,因此 tag = 8 string_index = 0x0701 则表示前一项是下标为 0x0701 = #1793 的 Utf8 字面量,当前下标为 #1794,所以得出结论是 access_flags 之前应有 1794(包含第 0 项) 个常量,则 constant_pool_count 截断后的值固定为 1794(0x0702),access_flags.x2 间接控制了常量池的大小(此时constant_pool_count的值应为0x0702,所以到t1051处截断,下面的字节就不是常量池里的内容了,直接赋值给access_flags及之后的字段)

根据字节码规范要求,this_class 应指向一个 CONSTANT_Class_info 结构的常量,也即如下图中 Class 对应的下标#0006但是这里并不能选择常量池已有的这些Class常量,原因在于这些Class常量是XSLT解析的过程中会使用到的类,而字节码最终会被defineClass()加载为Class,将会导致类冲突问题(TODO:为啥会导致类冲突?)解决方法是通过如下方法调用的方式加载一些XSLT解析过程不会引用的类,因为类是懒加载的,只有在被使用到的时候才会被加载进JVM,所以defineClass()调用时并不会存在com.sun.org.apache.xalan.internal.lib.ExsltStrings,从而解决了类冲突的问题,之后通过在其之前填充一些常量,使得 this_class = 0x0106(#262) 刚好指向 (Class): com/sun/org/apache/xalan/internal/lib/ExsltStrings 即可。

123456 super_class

super_class 同样也需要指向 CONSTANT_Class_info 类型索引,并且因为 TemplatesImpl 的原因依旧需要继承 org.apache.xalan.xsltc.runtime.AbstractTranslet 抽象类,所以直接指向 #0006 即可(位置固定不变)因为主要目的是控制方法,并通过 newInstance() 触发恶意代码,所以对于接口和字段都可以不需要,直接设置为 0 即可:

interfaces_count = 0x0000 fields_count = 0x0000 method_count

经测试发现 static{} 方法块()执行必须要有合法的构造函数存在,所以直接通过触发恶意代码即可,除此之外还需要借助一个方法的attribute部分进行一些脏字符的吞噬(后续解释),所以类中至少需要 2 个方法。经测试发现:在字节码层面,非抽象类可以不实现抽象父类的抽象方法,所以可以不实现抽象父类 AbstractTranslet 的 transform 方法,设置 method_count = 0x0002 即可

methods[0]

首先看到 method_info 结构:

1234567method_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 属性即可 methods[0].name_index(Utf8):选择指向了父类抽象方法名 transferOutputSettings,实际指向任何合法 Utf8 常量均可 methods[0].descriptor_index(Utf8):选择指向了 transferOutputSettings 方法描述符,实际指向任何合法 Utf8 方法描述符均可

methods[0].attributes_count 表示当前方法体中attribute的数量,每个attribute都有着如下通用格式,根据attribute_name_index来决定使用的是哪种属性格式(如下表)

12345attribute_info { u2 attribute_name_index; # 属性名索引 u4 attribute_length; # 属性个数 u1 info[attribute_length]; # 属性集合}

这里主要关注 Code 属性,其中存储着方法块中的字节码指令

12345678910111213141516Code_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 属性结构

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

可以看到构造函数中attributes_count = 1说明只包含一个属性,attribute_nam_index指向常量池#10(Utf8) Code,表示当前为Code属性,code_length 表示字节码指令长度为 17,code 部分则存储了具体的字节码指令这里需要注意的是:如果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 来进行控制 methods[1]

接下来看到第二个方法methods[1],首部这 8 个字节就可直接通过一个 double 数值类型进行控制,这里将构造所需的构造函数方法:

access_flags = 0x0001:需要给与 PUBLIC 属性才能通过 newInstance() 实例化 name_index:需要指向 的 Utf8 常量池下标,这里通过 代码提前添加 常量,否则只有编译到构造函数方法时才会添加该常量 descriptor_index:需指向()V的 Utf8 常量池下标 attributes_count = 0x0003:这里将使用 3 个 attribute 构造出合法的方法块 attributes[0]:用于吞噬 double 常量的 tag attributes[1]:用于构造 Code 属性块 attributes[2]:用于吞噬后续垃圾字符 methods[1].attributes[0]

可以看到 methods[1].attributes[0].attribute_name_index.x1 = 0x06,是为了设置double的tag,又因为 attribute_name_index 是指向常量池的索引,所以需要常量池需要 > 1536(0x0600),这就是前面 access_flags.x2 >= 0x06 的原因使用同样的方式,通过控制 attributes[0].attribute_length 吞噬掉下一个 double 常量的 tag这样就可以完全控制 attributes[1].attribute_name_index,使其指向Utf8 Code常量,后续数据将以Code_attribute结构进行解析

attribute_length 和 code_length 都得在 code[] 部分内容确定后进行计算 max_stack = 0x00FF:操作数栈深度的最大值,数值计算,方法调用等都需要涉及,稍微设置大一些即可 max_locals = 0x0600:局部变量表所需的存储空间,主要用于存放方法中的局部变量,因为不会涉及使用大量的局部变量,所以0x0600 完全够用了 exception_table_length = 0x0000:异常表长度,经测试发现,在字节码层面,java.lang.Runtime.exec() 方法调用实际可以不进行异常捕获,所以这里也将其设置为 0 attributes_count = 0x0000:Code 属性中的内部属性,用于存储如 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

接着看到最重要的字节码指令构造部分,可以通过Java字节码指令列表获取相关的 Opcode并非需要每个字节挨个自行进行构造,可以直接编写一个恶意方法,然后提取其中 code 字节码指令部分即可,编写如下代码并获取其字节码指令:

1234567891011121314151617181920212223import 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 { }}

根据上面的字节码指令即可构造出如下代码结构,其中有几点需要注意:

空操作可以使用 nop(0x00) 指令 对于 tag = 6 所对应的指令 iconst_6 需要配对使用 istore_1 指令 不使用 istore_0 的原因在于,局部变量表 0 位置存储着 this 变量引用 使用 ldc_w 替换 ldc,可以扩大常量池加载的范围 因为可以不涉及异常表,所以 goto 指令可以去除 根据前面的说明,末尾的 double 常量需要占用首部 3 个字节

对于 Methodref 方法引用类型,可以使用如下方法调用的方式进行添加

1

但是这里唯一存在问题的是:如何添加AbstractTranslet.方法引用,这里需要看到org.apache.xalan.xsltc.compiler.Stylesheet#translate()方法,构造函数总是最后才进行编译,添加的AbstractTranslet.方法引用总是位于常量池末尾,所以这将导致截断后的常量池中很难包含MethodRef: AbstractTranslet.方法引用然而构造函数中必须要调用super()或this()方法,否则会报错。这里有两种解决办法:

漏洞作者的解决办法:JVM 会检查构造函数中 return 操作之前是否有调用 super() 方法,所以可以通过 return 前嵌入一个死循环即可解决这个问题 thanat0s大佬的解决办法:通过如下代码可提前引入 AbstractTranslet. 方法引用:1 可以将AbstractTranslet.方法引用设置到一个比较低位的常量池位置。但是对于org.apache.xalan.xsltc.runtime.AbstractTranslet类来说,由于是抽象类,按理说不能调用new()方法进行实例化操作。但是从 org.apache.xalan.xsltc.compiler.FunctionCall#findConstructors() 中可以看到,通过反射的方式获取了构造方法并且直到添加方法引用之前(org.apache.xalan.xsltc.compiler.FunctionCall#translate) 都不会检查 XSLT 样式表中传入的类是否为抽象类,因此通过这种方式解决了 AbstractTranslet.方法引用加载的问题 methods[1].attributes[2]

同样通过控制 attribute_length 长度吞噬掉剩余的垃圾字符,由于需要保留 ClassFile 尾部的 SourceFile 属性,所以长度设置为:从 0x12345678 -> 保留尾部 10 个字节(attributes_count + attributes),至此完整的利用就构造好了

CheckList

这里总结一下需要检查的一些项:

#262 (0x0106) 需要指向 Class 引用 com.sun.org.apache.xalan.internal.lib.ExsltStrings 引用父类和当前类需要提前调用类方法加载 确认 methods[0].attribute_name_index 指向正确的 Utf8 引用 确认 access_flags 位于常量池 #1794 项 确认常量池大小为0x0702 (可以 Debug org.apache.bcel.classfile.ConstantPool#dump 方法) 确认各个所需常量是否指向正确的常量池位置 确认 methods[1].attributes[2].attribute_length 是否为:从 0x12345678 -> 保留末尾 10 个字节 构造两个方法,method[0]用来清理脏字符06,method[1]是无参构造器在里面exec 由于文件名也会添加至常量池,为避免影响对其他常量位置造成变动,长度需保证一致(6),select -> abcdef(文件名为test12和select时长度不一样) 运行前最好删除已生成的 *.class 文件(文件内容发生变动则不用) 最后清除脏数据只保留末尾10个字节 JDK-Xalan的paylod

对于JDK-Xalan paylod的构造主要参考于JDK-Xalan的XSLT整数截断漏洞利用构造同时对工具进行了debug,包括但不限于修复了常量池长度大于0x0702时的处理异常、初始payload不存在标识位等情况。最终成功在mac上实现了payload的自动生成项目地址https://github.com/altEr1125/AutoGenerateXalanPayload



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有