前言
Apache Flink最近有师傅披露了一个漏洞,可以调用任意类的main方法,不过在披露中所利用到的类只有在jdk11以上才存在。这时我考虑的是,我们常见的jdk8种能否找到利用类。经过寻找后找到了com.sun.org.apache.xalan.internal.xsltc.cmdline.Compile的main方法,这个方法可以把xslt编译成class,让我想到了之前爆出来的CVE-2022-34169。这个漏洞可以是apache xalan在把xslt转换成class的时候存在溢出(使用的类是:org.apache.xalan.xslt.Process),导致攻击者可以上传精心构造的恶意xslt,通过xalan转换后得到恶意的class,这个过程中,恶意的class会被实例化导致代码执行。通过一系列实践后我发现com.sun.org.apache.xalan.internal.xsltc.cmdline.Compile转换xslt和apache xalan是类似的,不过不同的是他没有实例化过程导致无法执行代码,虽然Flink这个漏洞利用没戏了,不过在这个中学习到了通过溢出来制作恶意class文件的技巧,还是收益匪浅的。后面又观察了一下,发现jdk中也有xalan的这个类(com.sun.org.apache.xalan.internal.xslt.Process),同样也是和apache xalan一样也会溢出。我在使用thanat0s师傅在文章中使用的exp复现时,发现apache xalan确实可以成功的弹出计算器,不过我在使用jdk xalan时,生成的class是损坏的,当然也无法弹出计算器。因为实战中还是见到过jdk xalan解析xslt的代码的,为了方便以后遇到了直接能利用,所以准备把我把apache xalan的payload改造成jdk xalan的payload的过程记录下来。
环境准备
首先先学习了下以下文章的构造过程:
https://blog.noah.360.net/xalan-j-integer-truncation-reproduce-cve-2022-34169/
jdk版本为:1.7.0_21
十六进制编辑器:HxD
开始改造
测试代码如下
1 | import org.apache.xalan.xslt.Process; |
Select.xslt地址为https://gist.githubusercontent.com/thanatoskira/07dd6124f7d8197b48bc9e2ce900937f/raw/b06ba31af6e269e500802af69f62f7192ca6d749/select.xslt ,首先把原版的select.xslt中的org.apache.xalan.xsltc.runtime.AbstractTranslet修改成com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,我测试用的系统是windows,所以把open -a calculator改成了calc
得到的class如下
按照参考文章的利用,常量池的大小应当为0702,而我用jdk生成的class常量池为06FF,还差三个,所以需要在xslt文件中补3个常量。如下图:
添加之后再编译一遍,可以看到此时常量池的长度已为0702了
access_flags & this_class
在apache版本中access_flags和this_class为08070106
这个值在jdk版本中是需要保持和apache版本一致的,现在我们需要检查一下当前编译出来的class这里的值为多少,如果不对的话需要填充或者删减常量来进行调整。这个漏洞构造比较麻烦的一个地方是,我们构造出来的class通常是损坏的,没办法用 Classpy GUI 工具来查看class格式哪里错误了。所以我们只能通过16进制编辑器来对比查看。
首先使用测试代码获得一个apache xalan编译的select.class
1 | import org.apache.xalan.xslt.Process; |
可以看到08070106在字符串t1051后面
再看jdk编译出来的select.class,在16进制编辑器中搜索t1051字符串,其后面的也就是他的access_flags和this_class了,值为08070106
this_class->0106(#262)这个值在apache编译的版本中是刚好指向(Class): com/sun/org/apache/xalan/internal/lib/ExsltStrings的,我们需要检查一下在jdk编译的版本中262号常量是否指向(Class): com/sun/org/apache/xalan/internal/lib/ExsltStrings。和之前一样,我们首先找到apache版本中262号常量附近的十六进制信息。262号常量自身的hex为070105。表示他的name_index也就是UTF8索引在0105(261)号常量的位置。
他的前面是字符串com/sun/org/apache/xalan/internal/lib/ExsltStrings
在jdk版本编译的select.class搜索com/sun/org/apache/xalan/internal/lib/ExsltStrings字符串,其后面就是(Class): com/sun/org/apache/xalan/internal/lib/ExsltStrings,可以看到其对应的值是070102,表示他指向的UTF8索引为0102(258)。而我们需要的其所在的位置应当为261,所以我们需要在com/sun/org/apache/xalan/internal/lib/ExsltStrings前面塞入三个常量
这里我们把之前塞入的三个常量换个位置,提到com.sun.org.apache.xalan.internal.lib.ExsltStrings的前面
调整过后,0106(262)的位置就是(Class): com/sun/org/apache/xalan/internal/lib/ExsltStrings了
super_class
因为 TemplatesImp的原因依旧需要继承 org.apache.xalan.xsltc.runtime.AbstractTranslet,原来是06,06号常量池对应着(Class): (Class): org/apache/xalan/xsltc/runtime/AbstractTranslet
按照和之前一样的方法对比apache和jdk编译的16进制内容得到jdk编译的select.class中06号常量为
(Class): com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet,所以0x0006不需要变更
method_count
无变化
methods[0]
无变化
methods[1]
这里原来<init>常量位置为8f(143),现在需要看一下jdk编译版本的<init>是否在143号常量池位置
还是先看apache编译出来的class,144号常量为(String):<init>常量,string_index值为143,表示143号常量为(Utf8):<init>常量。
(String):<init>后面为(Integer):133829,hex值为0300020AC5,在jdk编译的class中进行搜索。其前面的三个16进制值就是(String):\<init>,所以推理得(Utf8):<init>所在常量池位置为003E(62)
因此原来的0x008f要修改为0x003e,0x0001008f001e0003修改为0x0001003e001e0003
class的16进制和xslt ceiling中要填写的值的转换脚本如下
1 | import struct |
将得到的值填入下方
methods[1].attributes[0]
不变
methods[1].code
这里的Methodref(com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet.<init>)在apache中的常量池位置为0x89(137),在jdk编译出来的class中,位置有所改变,确定其位置的方式和之前雷同。
首先找到apache编译的版本中这个方法的位置。(Utf8):AAA在apache版本常量池中所处的位置为139。
在jdk版本中搜索AAA,从apache版本中可以看出来,(Utf8):AAA后面是(String):AAA,(String):AAA的hex值为08008A,8A就是jdk版本中(Utf8):AAA所处的常量池中的位置。而(Utf8):AAA在常量池中的位置(8A)减去2就是Methodref(com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet.<init>)在常量池中所处的位置,也就是88
其他的就和apache版本中的一致了,所以0x3C2AB70089000000调整为0x3C2AB70088000000
methods[1].attributes[2]
这里要控制attribute_length的长度去吞噬掉剩余的垃圾字符,长度为从 0x12345678 -> 保留尾部 10 个字节(attributes_count + attributes)
首先看apache编译出来的class这个长度的计算过程
红框标注的为尾部10个字节,尾部倒数第11个字节为000805AC
0x12345678前面一个字符的十六进制位置为00002818
000805AC-00002818=7DD94
因此apache版本的待定这里填的是0007DD94
jdk版本我们用同样的方式计算
倒数第11个字符位置为0008079E
0x12345678前面一个字符为29CF
最后是:0008079E-29CF=7DDCF
所以将0x0007dd9412345678修改为0x0007ddcf12345678,用python脚本生成后进行替换
最后成功弹出计算器
Payload
https://github.com/flowerwind/AutoGenerateXalanPayload/blob/master/select.jdk1.8_20.xslt
版本问题对于payload结果的影响
在使用上面的payload进行测试的时候发现,有一些版本的jdk在使用这个payload的时候无法触发命令执行,甚至class的格式都是错误的。在我用十六进制编辑器打开后发现不用版本的JDK生成的常量池数组的数目不同,并且一些常量的偏移也会发生改变。这让我想到了windows上面的一些溢出漏洞,不同的操作系统版本比如win7/win10,所需要的rce的exp都是不一样的,而我们的也是溢出场景,所以感觉挺像的。
切换所有的jdk用手工来调试出所有jdk适应的payload显然是不现实的,后面和@c0ny1师傅闲聊的时候他提出能否弄一个自动化的工具来自动计算偏移然后进行调整。后面我想了下,确实可以自动的去填充删减常量池,填充到满足0702个常量的时候终止填充或者删减常量池。然后开始计算后续所有payload所需要的偏移位置,调整完毕即可。
Coding
这里主要描述设计思路,代码会放github
我的思路是以原版的select.xslt为模版,编译一边后得到select.class,计算常量池长度,如果小于0702就在select.xslt中增加一些常量,大于0702就删除一部分select.xslt中的常量。其次,有一些固定的位置需要指向一些固定的常量比如#262 (0x0106) 需要指向 Class 引用 com.sun.org.apache.xalan.internal.lib.ExsltStrings,这些要特别的考虑在计算范围内,要在其或前或后的位置进行填充指定数量,才能达到我们的要求。
因此我在ExsltStrings的前面加了一个点位,后面如果要在ExsltStrings的前面添加常量,添加到<!--{before附近即可}–>
而删除的话我也设置了一些数组,这些数组存的是select.xslt中设置的常量,从select.xslt中删除后,我会把这个常量从数组中同步剔除保持一致。具体怎么用的,可以看代码,这里不做赘述。
增删完毕后就是计算偏移、调整偏移。这里我用的方法和手动改造中计算偏移的方式是一样的,先找到要计算的常量附近的一些好找的标志性常量,得到这个标志性常量的位置,然后推算我们要计算的常量所在的常量池位置。
代码见github:
https://github.com/flowerwind/AutoGenerateXalanPayload
JDK版本对比
jdk版本 | xsls类型 |
---|---|
Jdk-8u301 | A |
Jdk-8u202 | A |
Jdk-8u162 | A |
Jdk8u152 | A |
Jdk8u151 | B |
Jdk8u144 | B |
Jdk8u131 | B |
Jdk8u121 | C |
Jdk8u111 | C |
Jdk8u102 | D |
Jdk8u101 | D |
Jdk8u91 | D |
Jdk8u60 | D |
Jdk8u20 | D |
Jdk7u40 | D |
Jdk7u21 | D |
Jdk7u10 | D |
Jdk7u05 | D |
Jdk7u04 | D |
Jdk7u03 | E |
Idk7u02 | E |
Jdk7u0 | E |
Jdk6u45 | E |
Jdk6u20 | E |
Jdk6u17 | F |
Jdk6u15 | F |
Jdk6u10 | F |
Jdk6u0 | F |
不同SAML中对于xalan解析的实现
之前这个漏洞有一个场景就是saml的解析过程中会调用xalan进行解析其中的xslt内容,找了一些SAML实现,抽出对xslt内容解析的实现:
1 | Source stylesheet; |
先跟进TransformerFactory.newInstance方法,这里的TransformerFactory全限定类名为javax.xml.transform.TransformerFactory
跟进之后发现会先使用SPI机制获取provider,SPI检索的接口是javax.xml.transform.TransformerFactory,如果SPI没获取到,则会实例化一个com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl类
当系统的classpath中存在apache-xalan的时候,provider获取到的是org.apache.xalan.processor.TransformerFactoryImpl的实例
回归到一开始的代码,所以可以得出当classpath中存在apache-xalan和不存在apache-xalan的时候获取到的tFactory的对象是不同的,一个是org.apache.xalan.processor.TransformerFactoryImpl,一个是com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl
com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl.newTransformer方法是可以触发到xslt编译为class的过程的,而org.apache.xalan.processor.TransformerFactoryImpl没有把xslt编译为class的过程,所以就触发不了一处漏洞。
在和同事讨论的过程中,他发现了org.apache.xalan.xsltc.trax.TransformerFactoryImpl会触发xslt编译为class。可能某些情况下的saml的实现会调用org.apache.xalan.xsltc.trax.TransformerFactoryImpl吧,这个暂时是个疑问。
参考
https://blog.noah.360.net/xalan-j-integer-truncation-reproduce-cve-2022-34169/
总结
这个漏洞比较偏向二进制而非之前熟悉的web,亲自分析构造一下受益匪浅