从0到1掌握某Json-TemplatesImpl链与ysoserial-jdk7u21的前因后果

本文首发于先知社区:

https://xz.aliyun.com/t/7096

前言

作为一名安全研究人员(java安全菜鸡),知道拿到exp怎么打还不够,还得进一步分析exp构造原理与漏洞原理才行。本篇文章主要分析FastJson1.2.24中针对TemplatesImpl链的构造原理以及ysoserial中针对jdk7u21基于TemplatesImpl加动态代理链的构造原理。内容可能巨详细,希望没接触过这部分的同学可以耐心看下去。

1.TemplatesImpl初相识

FastJson1.2.24中基于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这条链的入口点在TemplatesImpl的getOutputperties函数。当然本篇文章不再描述具体如何到这一步,有兴趣的可以参考我之前的一篇文章。一步一步学习某Json1.2.47远程命令执行漏洞
因此在下图所示下断点在此,这里环境为jdk7u21。

这里首先将调用newTransformer(),首先定义类TransformerImpl的对象,其入口参数第一个为Translet的对象

因为此时调用了getTransletInstance(),跟进看看

其返回的是一个Translet类的对象,这里面判断_name 不能为null,否则就拿不到Tranlet对象,后面_class能不能为null,先留着,但是这里明显看到后面要用到_class

这里要用到_class,说明_class里面肯定是有东西的,要么是我们自己赋值,要么就是通过上面的defineTransletClasses()得到了,我们先跟进defineTransletClasses()看看

这里首先判断_bytecodes不能为null,为null将抛出错误信息

接着这里将拿到类加载器,通过它我们就可以对目标类进行加载,我们知道classloader除了调用loadclass来加载类以外,还可以调用findclass里的definedclass来通过加载字节码来在jvm中加载类,那么它肯定是classloader的子类,跟进看看其确实继承自ClassLoader,并且也看到了熟悉的defineclass函数,我们只要知道此时经过该类的definclass就能进行类的加载

继续往下看,下图中实际上取的是_bytecodes[i],并且i的范围也是我们可控的,这里我们知道defineClass是可以通过字节数组来在JVM创建类的,所以这里通过将恶意类的字节码放到_bytecodes里面就能够加载到JVM里

接着将拿到刚刚加载到JVM中的类的父类要求其父类必须是ABSTRACT_TRANSLET,不满足则放到_auxlclass里面,要是新加载的所有类的父类没有一个是ABSTRACT_TRANSLET的话,后面此时_transletIndex < 0处的判断就要报错,因为_transletIndex初始值为-1


此时我们已经知道通过defineTransletClasses函数我们可以通过defineclass来从_bytecodes中加载恶意类,所以我们肯定要让这里的_class为null

我们已经知道_class数组中存储的是加载进来的恶意类,下标_transletIndex就是该恶意类对应的下标,所以接着就调用newInstance()来实例化我们的恶意类,那么我们把要执行的命令放在恶意类的static区或者构造方法中都可以

其中我们的其中恶意类如下,至于要声明两个transform是由于这里是继承自抽象类,所以在其子类中必须实现,这里不声明的话idea也会提示让你实现,idea真香2333~

事实也证明如此,我们可以clac了

并且经过以上分析最终的payload可以缩减为以下形式:
整个调用过程挺短的,实际上就是
TemplatesImpl -> getOutputProperties()
TemplatesImpl -> newTransformer()
TemplatesImpl -> getTransletInstance()
此时在getTransletInstance()函数中将调用恶意类的构造函数,即
_class[_transletIndex].newInstance()导致RCE

2.AnnotationInvocationHandler完美链接

这一部分的分析主要就是通过最外层的readObject反序列化直达getOutputProperties()的调用,即newTransformer()的调用。而ysoserial中已经包含了该链的构造过程,其getobject函数就能拿到该链最外层的对象,而调试ysoserial也很容易,不传命令的话会默认传calc.exe

这里我们传入要执行的命令后调用createTemplatesImpl即可,那么我们可以看看yso中是如何构造该对象的

这里首先通过class.forname获得了三个我们构造该链要用到的类,然后将要执行的命令和这三个类传入createTemplatesImpl 来返回一个经过精心构造的TemplatesImpl对象

这里首先newInstance()获得一个初始的TemplatesImpl对象,用于后面的装饰,然后创建首先创建一个CtClass的容器,我们可以用它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用

Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。
由上面的解释也可以看出来这是一个功能强大的类。这里将首先会插入一个最原始的模板类,这个也就是作为我们用来执行命令的恶意类

通过以下这句代码我们就能够获得恶意模板类的对象,通过它我们就能够对该类的结构进行修改

然后将为该恶意模板类创建静态代码块,并插入rce的代码

这里rce的代码可以后面自己改,因此也可以自己根据需求定制

接着就是为该类起名字和设置该类的父类为abstranlet

接着就是获取该经过加工以后的类的字节码,以字节数组的形式保存,并通过反射的方式来设置templatesImple对象的_bytecodes变量值

其中setFieldValue函数第一个参数就是我们要设置的对象,第二个为属性,第三个参数为要设置的值

以上就完成了templatesImple的构造和恶意类的构造,但是如果不结合fastjson的反序列化特点的话就要找到一个新的readobject来链接到该templatesImple触发点,我们直接在hashset的readObject的中下断点进行调试,因为最终返回的是linkedHashSet的对象,因此入口点即在HashSet的readObject()函数

这里实际上将hashSet中的对象调用readObject()函数反序列化读出来然后放到有序列表的map中

由map.put就即将进入漏洞触发分析,因为后面要用到动态代理。所以这里简单分析一下这个技术:
首先要定义被代理的接口及其实现该接口的子类

接着要定义代理类,需继承自InvocationHandler,也就是位于被代理类处理顺序之前的类,在其构造函数中传入被代理类的对象,当调用被代理类的函数时将触发代理类的invoke函数,此处是重点,通过反射机制来实现

定义完被代理类以及代理类之后,还需通过Proxy类将两者进行绑定方可使用,这里要用Proxy.newProxyInstance来创建代理对象,通过其即可完成被代理的类与动态代理的绑定,然后通过该proxy对象就可能对被代理的类的函数进行调用,从而触发动态代理

运行结果如下图所示

在invoke处下个断点也可以清晰的看到此时method为hello,this.subject为SubjectImpl对象,args为world,即通过为被代理类绑定代理将可以在代理中运行新的代码块

了解了动态代理技术之后,就可以顺理成章地引入AnnotationInvocationHandler了,它就是一个动态代理,其继承自InvocationHandler

在其构造函数中有两个成员变量,两个均可控,并且在yso的payload中也通过反射机制为其this.type赋值为Templates类,并在newInstance中为memberValues赋值为只有一个键为f5a5a608,值为foo的map,当然后面将会对该键对应的值进行覆盖,放入恶意templatesImpl的对象,至于为什么要这样赋值后面说

目前我们只要知道这里是让AnnotationInvocationHandler作为Templates接口的代理。回到yso的paylaod,继续往下看,这里在linkedhashset中放了两个对象,其中linkedhashset是继承自hashset类的,放入的元素第一个是恶意的templateImpl,第二个是代理类handler

那么因为这里为了调试我们之前已经直接在hashset的readobject处下过断点

此时第一次反序列化得到的即为放入的恶意templateImpl类的对象,然后将其放到map中

第二次反序列化得到的即为临时的proxy对象,代表的接口Templates类,这里的map.put即是漏洞的入口点

跟进map.put看看,我们知道一个Map中不能包含相同的key,每个key只能映射一个value,但是两个key的hash可以一样,那么就放在相同的hash桶里,那么能不能插入新的值,put内部肯定是有一定的判断逻辑的,那么这里面就包含了动态代理的触发

put函数首先要对要放入的key计算一个hash,此时key为proxy对象,跟进此函数看看

其将会调用key.hashCode函数,那么我们知道当调用proxy对象的函数时将触发动态代理类的invoke函数,因此此时此时从下图①或②中都能够看到已经成功通过proxy对象进入到动态代理类AnnotationInvocationHandler当中

此时判断我们调用的是hashcode函数,将会进一步调用hashCodeImpl函数

在这个函数内部才是真正对map的key进行一个hash的计算

这里实际上将用127乘对AnnotationInvocationHandler的memberValues的键计算的hash以及值计算的hash,那么之前我们分析yso的payload时知道赋值给membervalue的键为键为f5a5a608,值为恶意的templateImpl对象,那么此时这个循环将执行一次并且计算key的hascode为0,那么实际上var1的值即为membervalue的键对应值的hascode,其值为恶意的TemplatesImpl对象

此时计算得到的hash为106298691,那么为什么要这么设置呢,之后就可以明白

计算完的hash还要经过移位操作然后得到最终的hash值为104622798


回到map.put函数的if判断,那么此时e.hash就是计算map第一个键的hash,而map第一个键就是恶意的TemplatesImpl对象,因此计算其hash肯定为104622798

所以之所以yso的payload要这么设置正是因为如此,也就是map的键为什么要设置为f5a5a608的原因,继续往下看

这里用Entry来对要放入的map的中的键进行遍历,其Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>,它表示Map中的一个实体(一个key-value对),接着看这个判断:

其中有&&连接两部分
① e.hash == hash
② ((k = e.key) == key || key.equals(k))
那么此时先理清key和k都是什么,这里的key和k如下图所示,这里的key就是Templates类型的proxy对象,k就是之前第一次放入map中的恶意的Templates对象,此时作为equals函数的入口参数,实际上调用的被代理对象的equals方法,那么这里正和我们的思路,那么想要触发动态代理,我们知道e.key是TemplatesImpl的对象,key是Templates类型的proxy对象,那么判断肯定不成立,那么就能够执行或逻辑右边的表达式,那么此时条件①的已经满足,因此直接调用key.equals(k)

那么实际上这里就跳又到annotation这里了,就是我们之前设置的动态代理类,直接到invoke函数处,判断调用的是equals函数

这里将会又再次跳到equalsImpl函数,其中入口参数var3[0]为传入的TemplateImpl恶意类,继续跟进

此时1处templateImpl肯定不等于annotation并且2处这里this.type在yso的payload中设置为下图所示,通过反射令其type为Templates类

所以这里就是判断templateImpl是不是Templates类的对象,因为Templates是TemplatesImpl的父类,那么这肯定为true

注:
class.inInstance(obj)
这个对象能不能被转化为这个类
1.一个对象是本身类的一个对象
2.一个对象能被转化为本身类所继承类(父类的父类等)和实现的接口(接口的父接口)强转
3.所有对象都能被Object的强转
4.凡是null有关的都是false ,即class.inInstance(null)



此时将会调用getMemberMethods()函数,这个函数内部实际上就是返回annotation这个类的type变量对象的类的所有方法,那么这里实际上返回的就是Templates这个类的两个方法了
①.Transformer newTransformer()
②.Properties getOutputProperties()

接下来就到了最终的漏洞触发点,我感觉叫链接点比较好,在这里通过反射机制来调用newTransformer(),其中var1就是我们之前构造的恶意的TemplatesImpl类

这里我们来回顾一下getOutputProperties处,其中下面的newTransformer和上面反射的newTransformer完美的符合在一起

此时由函数调用栈也可以看到此时回到了TemplateImpl这个类中,至此利用链分析结束

3.从jdk7u25和jdk7u21的对比中分析修复

jdk7u25是jdk7u21的后一个版本,运行后结果如下图所示

那么上面分析的利用AnnotationInvocationHandler作为动态代理打到newTransformer在jdk7u25中已经被修复,运行时将会报错如上图所示,其中是在反序列化的过程中有一步是通过反射机制调用了AnnotationInvocationHandler的readObject函数

我们知道在yso的payload中通过反射机制来给AnnotationInvocationHandler的type赋值为Templates类

那么再次执行下图代码:

当执行hashset中第二个proxy对象的readObject时,实际上将会在其中调用AnnotationInvocationHandler的readObject函数来恢复动态代理,最终到readObject函数处

此时的type为javax.xml.transform.Templates,进一步调用AnnotationType.getInstance

跟进看看getInstance函数

此时进一步调用Templates.getAnnotationType()函数

此时返回为null,继续返回getInstance中

此时var1为null,那么将Templates实例传入Annotationtype的构造函数

此时在AnnotationType的构造函数将调用isAnnotation对var1进行判断

那么明显Templates类跟annotation没关系,可以看到annotation和其他两种数据类型就是java类类型里面定义的,属于class类,即引用数据类型。比如最常见的枚举,枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。这里实际getModifiers()就是取Templates的修饰符,而Templates是接口类型的,所以两者无关,所以二进制位不可能有所重合,即肯定&&后为0。

所以此处必定进入if抛出错误

所以回到AnnotationInvocationHandler的readObject中将捕获到该错误从而抛出Non-annotation type in annotation serial stream

再回到jdk7u21里面,我们可以看到这里虽然在annotationtype中也捕获到了type不满足条件,并抛出了错误

但是catch以后直接retuen了,并没有再次抛出错误,让上层捕获,因此流程将继续走下去

所以反序列化将会继续执行,并且能够恢复我们的动态代理Templates,感觉是个逻辑错误,开发人员可能一不注意就会犯错,要发现这些点对于安全研究人员来说开发技能也是必备的。

总结

前前后后分析下来也花了几个晚上,真是学到了不少。整个漏洞利用中包含了很多java中的技术点,最大的感受就是Java的反射特性真的是太重要了,可以说是无处不在2333。挖掘漏洞需要大量调试和分析,补漏洞在这里抛出一个错误就可以让漏洞消失,当然调试的过程中也更加熟悉了java这门语言。

 

 

补充:

hashset用来存储对象(内部还是基于hashmap存储)

hashmap存储键值对,hashmap速率高于hashset

 

 在hashset的readobejct时map.put放入获取的临时内部proxy类来表示被代理的接口为Templates,代理类为一个hashmap,里面存的键为f5...值为templatesImpl的对象

posted @ 2020-01-16 16:33  tr1ple  阅读(1413)  评论(0编辑  收藏  举报