Mình đã từng làm về dạng Prototype Pollution trong CTF rất nhiều nhưng có lẽ đây là lần đầu mình research CVE về lỗi này ADVISORY DETAILS. Khi mình được @n3mo rủ làm về CVE này thì chúng mình đã bắt tay vào làm ngay và sau 1 tuần debug vào mỗi đêm thì cuối cùng cũng đã build được POC. Mình viết bài này để mong muốn chia sẻ và lưu lại kiến thức, biết đâu sau này sẽ cần >_<. Vào việc thôi nào …
Lỗ hổng này cho phép kẻ tấn công từ xa thực thi mã tùy ý trên Parse Server bị ảnh hưởng. Authentication là không cần thiết để khai thác lỗ hổng này.
Lỗ hổng cụ thể tồn tại trong hàm transformUpdate. Do thiếu kiểm soát đối với các các thuộc tính trong object. Kẻ tấn công có thể tận dụng lỗ hổng này để ghi đè thuộc tính tùy ý và impact cao nhất là dẫn đến thực thi mã từ xa.
Danh sách các phiên bản bị ảnh hưởng:
Điểm CVSS 3.1: 9.8
(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
Advisories: https://www.zerodayinitiative.com/advisories/ZDI-22-1590/
Commit: https://github.com/parse-community/parse-server/compare/5.3.0…5.3.1
Đầu tiên cần tải source code ở trên github về.
CVE-2022-39396 có bản vá ở 2 versions 4.10.18 và 5.3.1, nên mình quyết định checkout ở 5.3.0.
Sau khi checkout thì sửa file Dockerfile nhằm remote debug:
EXPOSE 9229
ENTRYPOINT ["node","--inspect=0.0.0.0:9229", "./bin/parse-server"]
Build parse-server docker
docker build --tag parse-server .
Run mongo docker
docker run --name my-mongo -d mongo
Cuối cùng chạy lệnh sau để run parse server
docker run --name my-parse-server -v config-vol:/parse-server/config -p 9229:9229 -p 1337:1337 --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test
Check server đã up chưa bằng lệnh
Ở đây mình dùng WebStorm, mở project source code đã checkout ở trên, sau đó add config -> search chữ chrome ( attach to nodejs/chrome) -> sửa host tương ứng vs ip server, port là 9229
.
Ở CVE này thì source code có trên github và các commit fix đều nằm ở trên đó nên mình không cần phải sử dụng phần mềm khác đề diff mà compare 2 bản ở trên github luôn.
transformUpdate
.Đầu tiên mình focus vào đoạn diff ở trên github thì thấy được bản mới chỉ thực sự fix ở file src/Routers/FilesRouter.js
bằng cách thêm đoạn check từ dòng 144-168
Cụ thể đoạn code này như sau:
metadata
và tags
nếu như chứa key
và value
nằm trong requestKeywordDenylist
thì sẽ hiện thị lỗi.requestKeywordDenylist
được set mặc định trong file src/Options/Definitions.js
constructor
và __proto__
nên mình đã tưởng rằng tìm được chỗ prototype pollution và đã bám vào chỗ này rất lâu.Khi tới đây, mình tưởng rằng chỉ cần pollution chỗ này là xong với đặt câu hỏi trong đầu:
Sink đầu tiên mà mình tưởng prototype pollution là tại đoạn code sau
Với fileOptions
và fileTags
được xử lí như dưới đây.
setTags
và setMetadata
đều có chức năng giống nhau là gán giá trị _metadata
hoặc _tags
theo key-value.setMetadata
setTags
fileOptions
và fileTags
có attribute metadata
và tags
được lấy từ fileObject.file._metadata
và fileObject.file._tags
được set ở trên.Với key
và value
mình có thể control thì hiện tại mình có thể chèn key
bằng __proto__
với value
mình muốn.
Để có thể truy cập vô đây thì mình sử dụng router /files/:filename
với method POST
handleParseHeaders
để xác thực auth. Hàm này chỉ thực hiện check header X-Parse-Master-Key
và tham số truyền vào ở json là _ApplicationId
để xác thực auth.createHandler
là đoạn code xử lí mà mình cần vào.
Đặt breakpoint tại dòng 483 ở file node_modules/parse/lib/node/ParseFile.js
và send với request sau để xác định xem mình đã có thể nhảy vào đúng thứ mình muốn chưa:
Tiếp tục đặt breakpoint ở dòng 183 và nhảy đến đây.
Nhưng khi check thì thấy thực sự ở đây không thể pollution toàn bộ object.
Giống với một ví dụ trên mạng mình đã tìm được và test Ref
Example:
__proto__
của object đó chứ không phải là prototype pollution.Tới đây mình khá là bế tắc không phải làm gì tiếp theo, vẫn như thường lệ khi làm 1day thì sẽ tìm mục đích cần làm là gì. Đây là lí do mình thích làm 1day vì nó khá giống chơi CTF, biết được mục tiêu cuối cùng mình làm là gì. Vậy ở đây trước khi mình pollution được toàn bộ object thì mình sẽ tìm sink RCE nằm ở đâu.
Để tìm được chỗ có thể exploit RCE thì mình đã đọc spec mà được cung cấp cùng với trong diff github
Qua đây thì có thể đoán được có vẻ là liên quan đến evalFuntions
và mình có tham khảo các CVE trước đây của parse server thì hầu hết nó vẫn liên quan đến đoạn này. Ref1, Ref2
Sau khi search evalFuntions
trong source và đọc những CVE trước đó thì thấy được đoạn code như sau:
deserializeObject
khi evalFunctions
được set thành true thì sẽ nhảy vào hàm isolateEval
Function.prototype.bind()
nên mình có thể lợi dụng ở đây để RCE.isolateEval
sử dụng eval
nên RCE dễ dàng hơn.Mình đọc các CVE cũ của những người khác phân tích thì thấy rằng đoạn này như sau:
node_modules/mongodb/node_modules/bson/src/parser/serializer.ts
Cụ thể như nào thì mình sẽ debug dưới đây.
Đặt breakpoint tại 2 dòng 183 và 185
và dòng 542 của file serializer.ts
Thực hiện send request với _bsontype
là Code
.
Sau khi f9 qua dòng 185 thì chương trình sẽ nhảy vô hàm serializeCode
Khi duyệt qua serializeInto
lần thứ 3 thì chương trình có value['_bsontype'] === 'Code'
và sẽ nhảy vào serializeCode
.
Stack Frame của khi gọi serializeCode
:
Tiếp đến mình sẽ debug xem deserialize sẽ được gọi như nào.
Đặt breakpoint tại dòng 577 của file node_modules/mongodb/node_modules/bson/src/parser/deserializer.ts
chính là sink mà mình cần vào.
Với request ở trên serialize thì reponse trả về sẽ chứa url truy cập vô file. Thực hiện truy cập vào url trả về đó.
evalFunctions
lúc này được set là false nên không thể nhảy vô hàm isolateEval
.evalFunctions
được setVậy mình đã tìm được giá trị cần pollution để vào được sink RCE là evalFunctions
=> giờ cần tìm chỗ prototype pollution toàn bộ object sau đó ghi đè evalFunctions
thành true thì sẽ hoàn thành.
Sau khi lỗ hổng này được public trên ZDI thì mình mới biết được nó có liên quan đến hàm transformUpdate
, lúc đầu mình làm chỉ focus vào những đoạn patch và thực sự trong đoạn patch thì lại không nhắc đến hàm này.
Search hàm transformUpdate
trong source thì cuối cùng cũng đã tìm được sink cho việc prototype pollution tại hàm transformUpdate
trong file src/Adapters/Storage/Mongo/MongoTransform.js
.
Vậy là đã về dạng pollution mà mình thường gặp. Vậy việc bây giờ cần làm là control được những giá trị sau:
out.value.__op
-> __proto__
out.key
-> evalFunctions
out.value.arg
-> true
Bây giờ mình cần đi tìm route nào sẽ xử lí và gọi đến hàm transformUpdate
.
Search transformUpdate
để xem được sử dụng ở đâu thì thấy được chủ yếu được gọi ở trong file src/Adapters/Storage/Mongo/MongoStorageAdapter.js
.
Có 3 hàm gọi đến là:
Tiếp tục 3 hàm này đều được gọi ở trong hàm update
tại src/Controllers/DatabaseController.js
.
Trace tiếp xem hàm update
này sẽ được gọi những nơi nào. Ở đây rất nhiều chỗ sẽ gọi đến hàm này nhưng mình đã focus vào 2 chỗ mà 2 route sử dụng mà mình thấy là hợp lí, trong đó 1 route auth và 1 route unauth.
Mình sẽ đi vào route /graphql-config
params
__proto__
vì đây là 1 atribute đặc biệt nên được chấp nhận và vượt qua đoạn check này.update
transformUpdate
ở src/Adapters/Storage/Mongo/MongoTransform.js
luôn, sau đó mình sẽ xem stack frame biết được nó sẽ đi qua những hàm nào.Như đã nói ở trên có 3 hàm gọi đến transformUpdate
thì ở route này đã sử dụng upsertOneObject
.
Các giá trị mà chúng ta muốn thì hiện tại không thể control. Khi mình đi đọc stackframe và debug thì thấy được ở route này chúng ta chỉ có thể control được out.value.__op
và out.value.arg
theo flow như sau:
transformKeyValueForUpdate
.restKey
lúc này là config
không nằm trong switch thì sẽ nhảy xuống đoạn code tiếp theo.transformTopLevelAtom
với restValue
là một object của mình truyền vào.isValidJSON
, cụ thể hàm này sẽ check xem value.__type === 'File'
và sau đó sẽ nhảy vào JSONToDatabase
. atom
chính là input của mình nhập vào, nên mình có thể control được giá trị trong này.JSONToDatabase
trả về json.name
. Vậy ở đây mình có thể dựa vào name
để control __op
và arg
.Send request với __type
là File
để thoải điều kiện và nhảy vào được JSONToDatabase
:
Khi debug tới hàm isValidJSON
thì thấy được với request trên thì mình đã vô được JSONToDatabase
, vậy bây giờ mình sẽ thêm atribute name
với giá trị __op
và arg
mình muốn.
Tới sink thì thấy được giá trị của __op
và arg
đã thay đổi theo ý mình.
Nhưng ở đây mình không thể control key
, vì vậy mình chỉ pollution config
cho toàn bộ object.
Lí do ở đây không thể control được key
vì nó đã được set cứng ở file src/Controllers/ParseGraphQLController.js
.
=> Mình không thể prototype pollution ở route này.
Flow của route graphql-config
.
Tiếp tục đến với route thứ hai là /classes/:className/:objectId
Ở đây sẽ gọi đến handleUpdate
và có gọi đến update
Tiếp tục chương trình sẽ gọi đến hàm execute
của RestWrite
Rồi sau đó chương trình sẽ về dạng gần giống như ở route trên để xử lí
Ở route này mình có thể control được đầu vào là className
,objectId
và req.body
là tùy ý của mình.
Như route trước mình đặt breakpoint ở hàm transformUpdate
chứa sink để khi cho chương trình chạy đến đây thì mình sẽ đọc các stack frame và xem input của mình đi qua những chỗ nào và xử lí ở đâu.
Vậy ở đây khi mình nhập body là một object thì restUpdate
sẽ nhận tất cả object đó.
Tới đoạn này thì thấy được restKey
sẽ đượclấy ra từ restUpdate
=> đã có thể control được key
=> hoàn thành việc prototype pollution. Ở đây mình sẽ không nói tiếp việc control __op
và arg
vì khi nhảy vào hàm transformKeyValueForUpdate
thì sẽ xử lí như nhau. Với việc mình có thể control toàn bộ đầu vào là 1 object bằng cách nhập vào key-value tương ứng thì để vượt qua các đoạn check trong hàm transformTopLevelAtom
để tới JSONToDatabase
đã là điều dễ dàng.
evalFunctions
tại route /classes/:className/:objectId
để nhảy vào được hàm isolateEval
có chứa sink để dẫn đến RCE.isolateEval
thì sẽ mục đích của mình là sẽ lợi dụng .bind
để gọi một function khác -> dẫn đến RCEmetadataHandler
và có một route gọi đến hàm này nên mình có thể lợi dụng chỗ này để thực thi một function khác. Nếu như chúng ta send với một atribute toJSON
thì khi code chạy tớires.json
thì trong hàm này có chứa JSON.stringify(obj)
vì vậy nó sẽ tự động được gọi tới.