Mozilla Rhino是一个使用 Java 语言编写的开源 JavaScript 引擎。ysoserial 中收录了 Rhino 的反序列化 Gadget,我们来一起分析下这个 Gadget。
零、NativeError 的继承关系
首先来看类的继承关系。它继承自,后者继承自。而实现了接口和接口。因此,可以进行序列化和反序列化操作。
一、分析
1、首先,反序列化攻击的入口在的函数。
中调用了函数,传入参数为的 this 对象。看下。
调用了两次函数,传入的参数是对象和字符串 name/message,继续跟进。
中调用的是父类的函数,入参没有变化。跟进去看看。
其中调用的是接口的函数。这个的实现在类。
最后调用的是父类的函数,再次回到类。
继续跟进函数。
其中的关键在于 2007 行到 2026 行的这部分。先看 2009 行到 2020 行的第一个分支。
这个分支中有的调用,看上去有戏。但有一个问题在于,是变量,在反序列化过程中无法赋值。
这会导致 2013 行的判断恒真,就被赋值为最初的对象。这就导致 2020 行的无法调用我们期望的目标对象的函数,只能调用静态函数或者类的内置函数。这当然不是我们期望的结果。
再来看 2021 行到 2026 行的分支。
这个分支中需要将设置为对象,并最终调用的函数。先看看如何赋值。
通过的。是的内部类,支持序列化。
可以通过来进行赋值。
那么要赋值成的哪个对象呢?是个接口,看下它的实现类。
我们选择类。这个类继承自,后者同样继承自,因此同样可以进行序列化和反序列化处理。
函数挺长,翻一翻会发现在 247 行调用了。
这个的调用,其实是函数,其中直接调用了我们熟悉的函数。
看起来很有希望。为了能成功调用到我们期望的目标函数,我们需要关注中里的三个变量:、和。
一个一个来,先看。
2、的值来自类的成员变量,通过查找到索引。
成员变量是类的对象数组,本身可以通过反序列化赋值。
至于的内容要设置成什么样,来看下函数。其中来自函数,而后者是直接返回了变量。
是个 transient 变量,要怎么赋值呢?
答案就在中。这里先通过得到了 member 对象,再通过函数将赋值给。
继续跟进函数,就是一个反序列化的实现。因此,通过反序列化给的赋值,不存在问题。
也就是说,我们可以通过反序列化给赋值为期望的目标函数。
结论
设置中的需要:
构造对象 m
设置 m 的成员变量为目标函数
构造对象 n
设置 n 的成员变量的 0 号元素为 m
3、javaObject 涉及的代码,都在的 222~247 行。
关键的部分就是 225~242 行的分支里。
如果要把赋值为我们期望的对象,就是要在 235 行完成这个赋值。但是这里有一个问题:我们知道就是对象,同理也是。但没有实现接口,这样一来 234 行的判断条件就不能满足了。
转机在于,这个判断身处循环之中,240 行的给了我们希望。查看一下的实现类。
看下的函数,直接返回了成员变量。
而成员变量可以通过反序列化的函数直接赋值。
也就是说,如果我们让对象的返回特定的对象,就可以完成的赋值。看看的实现,在类中。
这个来自 ScriptableObject 的成员变量,可以通过反序列化赋值。
结论
设置中的需要:
构造对象 o
设置 o 的成员变量为目标对象
构造对象 e
设置 e 的成员变量为 o
4、最后看一下。来自入参,其实就是调用者传入的。
这就决定我们要寻找的目标函数,必须是一个无参函数。
5、再回到开头,通常反序列化的入口都是函数,而文章开头说的反序列化入口在函数。怎么才能从入口转到呢?
答案就在 JDK 中的类的函数。
也就是说,只要将的设置为对象,就可以在反序列化的过程中调用了。
6、结论
如果要完成反序列化POC,需要:
构造对象 m
设置 m 的成员变量为目标函数
构造对象 n
设置 n 的成员变量的 0 号元素为 m
构造对象 o
设置 o 的成员变量为目标对象
构造对象 a
设置 a 的成员变量为 o
通过 a 的函数,设置 a 的属性为对象 n
构造对象 b
设置 b 的成员变量为对象 a
前面说过,需要寻找的目标函数,应当是一个无参函数。同时,这个无参函数所属的目标类,还得是实现了接口、支持序列化和反序列化的类。
因此,首先想到的就是,使用类作为目标类,使用它的作为目标函数。
二、填坑
完成了上述分析,我们开始写POC。途中暗坑无数,逐一填之。
1、无法实例化
声明对象,直接报错:。
报错原因:
类不是,不能直接引用。
解决方案:
通过反射,实例化对象。
2、反射实例化的运行失败
运行这段代码:
报错原因:
没有提供默认的public 无参构造函数,无法直接调用 newInstance()。
解决方案:
通过反射设置构造函数为 public,再进行调用。
反射在 ysoserial 中被大量的使用,原因也就在此。
3、执行POC失败:No Context
按照“分析”部分的结论,结合大量的反射调用,完成POC如下。
编译运行。呃,序列化成功,可是反序列化的时候却没看到计算器,只看到了报错:“No Context associated with current Thread”。
报错原因:
问题在哪里呢?就在的 else 分支中。
我们期望进入 2024 行的,结果在 2023 行抛出了异常,因为 Context 对象为空。
构造需要调用函数。
怎样在反序列化的时候插入的调用呢?
重新看下调用栈,发现调用了两次函数,分别传入字符串 “name”和“message”。
因此,我们可以把作为 “message”的属性,把作为 “name” 的属性,这样就可以先执行,再执行进行 Payload 执行。
解决方案:
按照 POC 中设置的方法,设置为 “name” 属性,将设置为 “message” 属性。
4、执行POC仍然失败:No Context
增加的调用之后,重新运行POC,呃,问题依旧……
报错原因:
为什么新增的调用无效呢?因为设置函数的方法错了。
无论是还是,我们都是通过函数进行设置。而这个函数设置的属性,是类型的。
回到报错的地方看。 2009 行 if 分支的判断条件是,属性的值必须是类型,而并没有实现接口,所以无论进来的是还是,代码流程都会走到 2021 行的 else 分支中。
我们期望流程走到 2024 行的,遇到的问题是在 2023 行就报错了。我们增加的调用,期望他能解决无法通过来调用的问题。
但是对的调用遇到了一样的问题,在 2023 行就抛出了异常,无法走到 2024 行去执行我们期望的函数。
所以,对的设置,就不能像一样,去通过函数进行设置,只能让他通过 2009 行的 if 分支去调用。但是要怎么去设置呢?ysoserial 通过反射进行强制设置属性来解决这个问题。
解决方案:
参考 ysoserial 中的方法,通过反射进行强制设置属性为 MemberBox 对象的方法:
现在再执行 POC,终于可以看到计算器了。
三、POC
完整POC参见Github (https://github.com/yaojieno1/rhinoPoc)。
主要函数:
四、心得
1、有作为反序列化的入口,也成为了之外的另一个反序列化攻击触发点。
2、反射功能,很好很强大。
领取专属 10元无门槛券
私享最新 技术干货