09-25: Chainfection
Writeup for Chainfection (Web) - YesWeHack Dojo (2025) ๐
Description
Upload your files, share them with the world, and enjoy unlimited safe cloud storage directly from your favorite browser. Free antivirus scans that runs on good vibes. What could possibly go wrong?
Solution
In this writeup, we'll review the latest YesWeHack Dojo challenge, created by Brumens ๐
Follow me on Twitter and LinkedIn (and everywhere else ๐ช) for more hacking content! ๐ฅฐ
Source code review
Starting with the setup code (nodejs), we see the flag is written to /tmp under a randomised filename. As usual, I snipped the index.html because it appears to be purely UI-related.
setup.js
const fs = require("node:fs");
const path = require("node:path");
const crypto = require("crypto");
const ejs = require("ejs");
const process = require("process");
const { Sequelize, DataTypes, Op, literal } = require_v("sequelize", "6.19.0");
const psanitize = require_v("path-sanitizer", "2.0.0");
process.chdir("/tmp");
fs.mkdirSync("view");
fs.mkdirSync("user/files", { recursive: true });
fs.writeFileSync(`flag_${crypto.randomBytes(16).toString("hex")}.txt`, flag);
fs.writeFileSync("user/files/document.txt", "test");
// create a sqlite database
const sequelize = new Sequelize({
dialect: "sqlite",
storage: ":memory:",
logging: false,
});
// define "users" table
const Users = sequelize.define("User", {
name: DataTypes.STRING,
verify: DataTypes.BOOLEAN,
attachment: DataTypes.STRING,
});
async function init() {
await sequelize.sync();
// insert users
await Users.create({
name: "brumens",
verify: true,
attachment: "document.txt",
});
await Users.create({
name: "leet",
verify: false,
attachment: "",
});
}
// Write the design
fs.writeFileSync(
"view/index.ejs",
`
<html>
SNIPPED
</html>
`.trim()
);
return { flag, secrets, fs, path, psanitize, sequelize, ejs, DataTypes, Op, Users, init };Initial thoughts:
SQL database -> SQL injection?
File attachment -> insecure file upload?
Path sanitizer -> directory traversal / LFI?
EJS -> SSTI?
Hardcoded library versions (
sequelize+path-sanitizer) -> known CVE(s)?
app.js
We need to provide valid JSON input
The JSON object must contain four keys:
username,updatedat,attachment,contentThe "leet" user (id=2) will be updated with the new
attachmentA verified user is selected based on
usernameandupdatedatThe returned
user.attachmentis sanitised and used as a filenameThe
contentis written to the constructed filename
Testing functionality
The UI looks fancy and interactive but don't get distracted - it is entirely non-functional.
The only thing we need to worry about is submitting valid JSON with all the relevant keys, e.g.
We get a new error about a null attachment.
Let's change the updatedat value, since the SQL statement uses it in a >= condition.
That clears the error, but we don't get any output. Recall that the returned attachment value is used as the filename and the "brumens" user attachment is set to "document.txt", containing "test".
We don't have any means to update that, since the SQL statement to update the attachment is hardcoded to the second user ID, belonging to "leet". Unfortunately, we also cannot change the username to "leet" because the user is unverified ๐ค
The SQL query will look something like this, where :updatedat is the raw user-supplied updatedat value (swapped in via replacements) and data.username is the user-supplied username.
SQL Injection (CVE-2023-25813)
Checking the known vulnerabilities for sequelize 6.19.0, there's an advisory from 2023 titled "Sequelize vulnerable to SQL Injection via replacements" ๐
The example in the advisory is revealing. The statement replaces ":firstName" in the literal string with firstName:
They supply an SQL statement in the firstName field and then ":firstName" in the lastName field:
The result is that the replacements operation first swaps the ":firstName" with the SQL statement and then swaps the ":firstName" we provided in the last name with the same SQL statement, e.g. it starts as:
Then after the replacement becomes:
Let's apply the same principle to our scenario.
Sequelize will first generate this query:
Then it injects the replacements:
That's not valid SQL syntax, so we trigger an error. Developers fear errors but hackers welcome them - progress! ๐
Let's try again.
After replacement:
Everything after the comment is ignored, so the statement essentially becomes:
If we submit that, we don't get an error. Let's try and change the condition:
Now we get the error again! Just like that, SQLi confirmed โ
We need to select the second user, so let's adjust the payload.
The displayed error is promising - it's finally trying to open our specified file!
Directory Traversal (CVE-2024-56198)
If you thought about directory traversal, that's a good instinct. Unfortunately, any ../ we enter are stripped and URL-encoding doesn't work as a bypass.
That takes us back to the other library we intended to check for known vulns: path-sanitizer 2.0.0
It turns out versions 3.1.0 and below are vulnerable to directory traversal. The referenced PoC is down, we could check wayback/archives but why don't we instead check the git commit ๐
There was already a test for URL-encoded slashes.
Here's the new test they added.
Interesting ๐ it starts with ./ and traverses to a [presumably] non-existent directory, continues traversing backwards and then goes to the target /etc/passwd. I tried multiple variations of vuln #2 but didn't make any progress.
Out of interest, I try vuln #1 (..=%5c..=%5c..=%5c..=%5c..=%5c..=%5c..=%5cetc/passwd) which should already be patched. We did hit a different error.
At this point I realised I was looking at the wrong advisory (and test case).
Where we are (2.0.0)
The
..=%5Cvulnerability is fixedThe
./../../test/../../../../../../../../../../etc/passwdvulnerability is fixed
Maybe vuln #2 was introduced after version 3.0.0? Regardless, vuln #1 works for us ๐
Server-side Template Injection (RCE)
Let's think about which file we might be able to overwrite for RCE. How about /tmp/view/index.ejs, since it's rendered as a template at the bottom of app.js?
We'll try a basic payload to read the contents of a file in the /tmp directory beginning with "flag_"
Put it all together.
The whole HTML response has been replaced with an error ๐
The key part is "require is not defined at eval", lets try to access it from globalThis
We get a new error that "print is not defined at eval".
Let's adjust the payload to try a chain of ways to get a fs handle.
It works! ๐
Flag: FLAG{Bug_C4ins_Br1ng5_Th3_B3st_Imp4ct}
Remediation
Upgrade sequelize to >= 6.19.1 to pick up the replacements fix
Upgrade path-sanitizer to >= 3.1.0 and add an explicit server-side allowlist for writable upload targets (never allow writes to template/view directories)
Keep uploads and templates strictly separate; never render uploaded files as templates
Enforce filesystem permissions: process should not be able to write into the template/view directory
Summary (TLDR)
The challenge combined two vulnerable libraries to create a vulnerability chain: SQL injection -> file write + path traversal -> SSTI (RCE) ๐
Last updated