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

  1. We need to provide valid JSON input

  2. The JSON object must contain four keys: username, updatedat, attachment, content

  3. The "leet" user (id=2) will be updated with the new attachment

  4. A verified user is selected based on username and updatedat

  5. The returned user.attachment is sanitised and used as a filename

  6. The content is 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).

  1. Where we are (2.0.0)

  2. The ..=%5C vulnerability is fixed

  3. The ./../../test/../../../../../../../../../../etc/passwd vulnerability 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