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 variabledata
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 recipientmsg
which corresponds to the content of the messagefile
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 inputto
. - Second line: It writes the contents of
msg
to thisfile
.
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...)
- 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 "
- 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
- https://nodejs.org/api/path.html#pathbasenamepath-suffix
- https://cwe.mitre.org/data/definitions/22.html
————– END OF 1mA5K‘s REPORT —————