Dojo challenge #35 winners!

September 25, 2024

The 35th Dojo Challenge, Chatroom, invited participants to exploit a CWE-73: External Control of File Name or Path vulnerability and read a file containing the challenge flag.

We are delighted to announce the winners of Dojo Challenge #35 below.

💡 Want to create your own monthly Dojo challenge? Send us a message on X (Twitter)!

3 BEST REPORT WRITE-UPS

Congrats to G6G, skewen and 1mA5K for the best write-ups 🥳

The swag is on its way! 🎁

Subscribe to our X (Twitter) and/or LinkedIn feeds to be notified of upcoming challenges.

Read on to find out how one of the winners managed to solve the challenge.

The challenge

We asked you to produce a qualified report explaining the logic allowing exploitation, as set out by the challenge.

This write-up serves two purposes:

  • To ensure contestants actually solved the challenge themselves rather than copy-pasting the answer from elsewhere.
  • To determine contestants' ability to properly describe a vulnerability and its vectors within a professionally redacted report. This gives us invaluable hints on your unique talent as a bug hunter.

OVERALL BEST WRITE-UP

We want to thank everyone who participated and reported to the Dojo challenge. Many other high quality reports were submitted alongside those of the three winners. 😉

Below is the best write-up overall. Thanks again for all your submissions and thanks for playing!

1mA5K‘s Write-Up

————– START OF 1mA5K‘s REPORT —————

Description

An Arbitrary Local File Overwrite occurs when an attacker can overwrite specific local files on a target system with arbitrary content. This vulnerability typically arises due to insecure handling of file paths or poor validation in web applications, software, or systems.

This attack may lead to the disclosure of confidential data via local file inclusion, denial of service or remote code execution and other system impacts.

Exploitation

We are presented with a web application allowing us to send messages on a chat. We can give it a JSON file with the message content and recipient as input.

Code analysis - Part 1: User Input

When analyzing source code, it's important to start with the entry point. In our case, this means retrieving user input. This user input first passes through the Web Application Firewall, which URL-encodes it. User input has been replaced in the following code by the <USER_INPUT_URL_ENCODED> tag.

const userData = decodeURIComponent(<USER_INPUT_URL_ENCODED>)

var data = {"to":"", "msg":""}
if ( userData != "" ) {
    try {
        data = JSON.parse(userData)
    } catch(err) {
        console.error("Error : Message could not be sent!")
    }
}

var message = new Message(data["to"], data["msg"])

This code does the following steps:

  • URL-Decoding: User input <USER_INPUT_URL_ENCODED> is first decoded using the decodeURIComponent() function. This acts as the inverse function of the URL encoding performed by the Web Application Firewall.
  • JSON parsing: If the user input is non-empty, it is parsed as JSON using JSON.parse(). If the input format is not a valid JSON, then an error log is generated.
  • Use JSON object: At the last line, the “to” and “msg” arguments of this JSON are retrieved and used into the Message class constructor. The initialization of the variable data was used as a hint to identify the JSON format, i.e. {"to":"", "msg":""}.

Code analysis - Part 2: Message class & its usage

The previous piece of code is followed by:

var message = new Message(data["to"], data["msg"])
message.makeDraft()

This is the only call to a method of the Message class in the whole provided code. This method makeDraft() should therefore be analyzed in details.

Here is the code of the class:

class Message {
    constructor(to, msg) {
        this.to = to;
        this.msg = msg;
        this.file = null
    }
    send() {
        console.log(`Message sent to: ${this.to}`)
    }
    makeDraft() {
        this.file = path.basename(`${Date.now()}_${this.to}`)
        fs.writeFileSync(this.file, this.msg)
    }
    getDraft() {
        return fs.readFileSync(this.file)
    }
}

It has three arguments:

  • to which corresponds to the message recipient
  • msg which corresponds to the content of the message
  • file which corresponds to the where the message is stored (in reality, in this code: the message is never really sent, it is simply stored in a file)

Let's now analyze the makeDraft() method, which consists of two lines whose purpose is:

  • First line: It gives a value to the file argument, using a timestamp followed by the user input to.
  • Second line: It writes the contents of msg to this file.

The line of code of interest here is the first one, let's see in details:

 this.file = path.basename(`${Date.now()}_${this.to}`)

Here, the file argument, corresponding to the location where the file will be saved on the filesystem, takes as its value the return value of the path.basename function.

The path.basename function is defined like this in NodeJs:

In our case, its path argument is ${Date.now()}_${this.to}. Which means that a file is displayed in a format <Timestamp>_<Recipient>, example: 1724402892063_Brumens.

In its documentation, we can see that only the last part of the path is retained. Which means that adding a directory separators "/" would allow us to modify the return value of the function and retain only the part to the right of the separator.

In practice it means that if we send a value in the format "aaa/bbb" in the to parameter:

  • We will have a path parameter with a value like "1724402892063_aaa/bbb"
  • The function path.basename() will truncate the path and return "bbb".

This vulnerability allows us to write to a file in the target "file-write folder". Also, thanks to the second line of code, we can fill this file with any content we like.

If the newly created file was publicly accessible with execution rights, then we could have had a Remote Code Execution.

Code analysis - Part 3: Render of index.ejs

The code ends with the following line:

console.log( ejs.render(fs.readFileSync('index.ejs', 'utf8'), {message: message.msg}) )

It renders and executes the EJS template present in the index.ejs file by giving it the msg argument of the message object created previously.

Code analysis - Conclusion

From our analysis, we note that:

  • The input must be in the JSON format: {"to":"<RECIPIENT>", "msg":"<MESSAGE>"}
  • The code allows to write to file in the local folder with control over its name in <RECIPIENT> and content in <MESSAGE>
  • index.ejs is executed at the end of the code

Putting it together, we create a payload to overwrite the index.ejs file with any content:

{"to":"/index.ejs", "msg":"test"}

Which gives us:

The index.ejs file has been replaced by our test content.

PoC

EJS is a templating language that lets us generate HTML markup with plain JavaScript. It has an include function that allows us to include any file in the file system if it has an extension (i.e. allowing Local File Inclusion). By default, the .ejs extension is used, but it can be replaced by any other extension.
In order to retrieve the content of the flag.txt file, we use the following JSON payload :

{"to":"/index.ejs", "msg":"<%- include(`/tmp/flag.txt`); %>"}

Retrieving the flag : FLAG{W1th_Cr34t1vity_C0m3s_RCE!!}

I was unable to recover any other files:

  • Files such as /etc/passwd have no extension and are not recoverable via this type of payload.
  • Some sensitive files could be recoverable but are not available in this challenge. Example files:
    • .env with environment variable present at the root of the project (either in the write folder or one of its parent folder)
    • Server configuration files like /etc/apache2/httpd.conf or /etc/nginx/nginx.conf
    • Server logs like /var/log/nginx/access.log
    • Other sensitive info like .ssh/id_rsa.priv, files .db and .bak

Risk

Arbitrary Local File Overwrite vulnerabilities pose significant security risks to systems. By exploiting these vulnerabilities, attackers can gain access to sensitive files on the server, execute remote code, or retrieve confidential data.
This can lead to privacy breaches, data integrity violations, or even complete compromise of the system. Vulnerable applications often include web services and other applications that handle user input data without properly controlling it, exposing these systems to significant risks of exploitation by malicious actors.

Remediation

To protect against this vulnerability, we recommend following these measures:

  • Validate and filter user inputs:
    • Rigorously validate and filter the user input before processing it. Ideally, compare the user input with a whitelist of permitted values. If that isn't possible, verify that the input contains only permitted content, such as alphanumeric characters only. In our case it would prevent the directory separator "/".
    • After validating the supplied input, append the input to the base directory and use a platform filesystem API to canonicalize the path. Verify that the canonicalized path starts with the expected base directory and that the file respects the expected standards. In our case it would be a verification that the timestamp is present and that file is what we expect it to be (filetype/extension is correct, content is text only...)
  • Awareness and training: Educate developers and security teams on best practices for securing user inputs in web services.

By implementing these measures, you can effectively reduce the risks of such vulnerabilities in your applications and systems.

References

————– END OF 1mA5K‘s REPORT —————