0%

nacosRCE到Springboot通杀

nacosRCE到Springboot通杀

标题党文章捏,这个通杀要看一定的运气(虽然我本地猛通猛通,但是实际上还是需要一点运气成分的),本文的起因是wjh和我说springboot通杀这么牛逼的技术怎么感觉都没什么反响,然后天哥说这个不是很通杀,有概率问题,进而导致了疑惑,从而对该方法是否通杀以及其原因进行的研究。(实际上是好久没跟博客了感觉这个稍微有点内容能水一下)

最近nacos rce好像很火,然后有一天whj好兄弟给我发了知名Java卷王y4er的文章里的一句话
Nacos Hessian 反序列化 RCE(说起来这篇文章里面居然还引了我这个不知名Java垃圾的文章。我不好说)

于是和@X1r0z讨论了一下,nacos是springboot,内置了jackson,可以用jndi lookup配合jackson POJONode的gadget打rce

看这意思,jndi lookup是打本地链,jackson的POJONODE能直接RCE。springboot自带jackson,那还要什么nacos,言外之意不是springboot有一个反序列化就直接走POJONode直接通?

反手就是一个搜索,这个点在前不久的阿里云CTF中出现过,我当时也去坐牢了,不过因为Java居多所以没干啥事。。。哈哈做出来的题没什么意义就没写wp,当时的bypass1这个题似乎就是裸Springboot反序列化rce。当时我都没注意到,现在想想这个通杀不是小无敌,可以非预期一堆Java题了(以前的Java题)

贴两个链接,就不具体展开了
Jackson反序列化通杀Web题(过时)
从bypassit1了解POJONode#toString调用getter方法原理

类似于fastjson的原生反序列化利用,jackson也可以进行原生反序列化利用。触发toString方法时调用对应的getter,所以思路也很简单,从BadAttributeValueExpException直接调用POJONodetoString,此时会将该node中的对象转换成字符串,自然就会对各属性调用getter,能rce且jdk自带的getter就是我们人见人爱的TemplatesImplgetOutputProperties

具体利用时在序列化POJONode时,需要重写一个writeReplace方法,该方法不删掉会导致序列化的时候出错,因为是序列化时的修改,所以该改动可以直接在攻击者方完成,并不影响利用。

至于怎么删方法,比较推荐的是直接javassist改字节码

CtClass pojoClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = pojoClass.getDeclaredMethod("writeReplace");
pojoClass.removeMethod(writeReplace);
// 将修改后的CtClass加载至当前线程的上下文类加载器中
pojoClass.toClass();

总之,本文的内容并非讨论POJONode的具体利用,所以从POJONode的toString是怎么到getter的具体流程也不会说。重点在于,这个方法通杀吗,如果是或不是,为什么?

通杀?

看起来该方法是稳定通杀的,jackson springboot自带,用来触发的BadAttributeValueExpExceptionTemplatesImpl也是jdk自带,全套都可用。然而天哥和我说这个利用是不稳定的,因为jackson调用getter的顺序是随机的,如果先调用其他的getter并抛出了错误,就会导致反序列化在还没调用到getOutputProperties就终止。并且由于缓存的存在,第一次调用后后续就会直接使用缓存,顺序也不会变动,所以是一个需要运气的利用,并非通杀。

怎么会有这么奇怪的东西,随机顺序调用getter呢?这不太符合代码的稳定性,设计上也不应如此,那就只能开调了。

首先找到调用getter的地方,位于serializeFields方法,部分调用栈如下

serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)

prop.serializeAsField方法中,对prop反射调用了getter方法,继续寻找props的来源

image-20230701184506361

寻找this的来源,往上退两层栈,this来自于defaultSerializeValue中的findTypedValueSerializer

image-20230701185031911

defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)

defaultSerializeValue往下走过一堆ser的创建函数,到_createSerializer2

_createSerializer2:224, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
createSerializer:173, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
_createUntypedSerializer:1495, SerializerProvider (com.fasterxml.jackson.databind)
_createAndCacheUntypedSerializer:1443, SerializerProvider (com.fasterxml.jackson.databind)
findValueSerializer:544, SerializerProvider (com.fasterxml.jackson.databind)
findTypedValueSerializer:822, SerializerProvider (com.fasterxml.jackson.databind)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)

ser = this.findSerializerByAnnotations(prov, type, beanDesc);中,beacDesc被赋值了props,并在ser = this.findBeanOrAddOnSerializer(prov, type, beanDesc, staticTyping);中,最后将beanDesc的props赋值给了ser
image-20230701190003725

先跟进findSerializerByAnnotations最后会进入collectAll中,通过field和method添加props。要是一直往下翻,能翻到是用的经典getDeclaredFieldsgetDeclaredMethods,然后再逐个迭代,使用一些逻辑进行筛选。此处的this是beanDesc下的_propCollector成员

image-20230701190453895

再进入findBeanOrAddOnSerializer,进一步到constructBeanOrAddOnSerializer中,该函数先通过findBeanProperties从beanDesc中取出props

image-20230701190926403

在这之前,props一直是一个hashmap,而经过了该函数的调用,变成了一个List,一直往下跟,最后会走到getProperties方法,此处将hashmap转换成了ArrayList

image-20230702161433705

回到constructBeanOrAddOnSerializer中,调用builder.setProperties(props);给builder设置props属性,最后调用builder.build()创建出ser。

prop后续的处理都是用iter对数据进行迭代,进行删改,理论上不会影响顺序,那么props的顺序就只可能由hashmap生成时或hashmap转List时确定。

一开始以为是hashmap转list的时候可能会出现的问题,但是仔细一想也不对,无论是直接按字典序排列,还是通过计算哈希排列,不可能出现同一份数据在不同情况下计算出的顺序不一致的情况。

所以最后的问题是出在hashmap的生成上,但实际上这里的hashmap是LinkedHashMap,也是一个链表一样的存在,按理说往里面添加数据的时候也不会产生乱序。但实际上,Java真的并未保证数据的稳定性,在Java文档中有提到,getDeclaredFields和getDeclaredMethods

The elements in the returned array are not sorted and are not in any particular order

JavaDoc getDeclaredFields

搜一下getDeclaredFields顺序之类的关键字,也能找到一大把解释。。。
这就是一切随机的罪魁祸首,在一开始的props生成的时候,Java可能会随机的返回字段的顺序,进而导致最后调用getter的顺序变得随机。(虽然话是这么说,但是我本地无论怎么重启顺序都是固定的。。。?但是确实其他人环境下偶尔会出现顺序错误的问题)不过我直接对POJONode进行测试,和套一层springboot环境再测试时,props的初始顺序有所变化,但是经过一系列的处理后,getOutputProperties还是在最前面,可以稳定利用。

一次运行环境中理论上来说jvm的状态就是固定的,所以即使没有缓存,如果一次利用不成功,应该就意味着之后的利用都不会成功了,即本次运行的getDeclaredFields顺序不支持利用。

结论

POJONode在toString时会对Node中的Object的各prop调用getter,而props是由getDeclaredFields等反射方法生成的,该函数不保证返回值的顺序,因此造成了getter随机调用的假象,导致该利用没有百分百的稳定性。(虽然在我的机器上是百分百的稳定性)

该顺序问题可以通过重启应用进行刷新,也许之前打不通的重启一下就又通了,但由于缓存和java运行时状态的问题,如果一次攻击不通那么后续攻击也不会通。

后续更新

逛街看到了这个,去年九月份的文章,把七月份的不稳定利用变成了稳定利用。也就是说现在是真的springboot原生超级通杀?这么牛逼的文章怎么都没听人提起过。。。?搞这么卷吗java
从JSON1链中学习处理JACKSON链的不稳定性

如果有空的话可能会复现一下,(应该是不会有了)