Prototype Pollutionを理解したい
プロトタイプ汚染
Protorype Pollutionを理解したい。
攻撃者がJavaScriptオブジェクトのプロトタイプを書き換えることで
オブジェクトの振る舞いを変更したりできるやつ。
CTFやHTBで見かけるけど、自力で解けたためしがない。
解きたい。頑張る。
Prototype
まずはプロトタイプの挙動について。
Object.prototype
Hoge
というクラスを定義している。
クラスHoge
にはprototype
というプロパティが存在し、そこにshout
メソッドが入っている。
よってHoge.prototype.shout()
とすると実行できる。
class Hoge { shout() { console.log('Fooooooo!!!'); } } Hoge.prototype.shout(); // Fooooooo!!!
Object.prototype.__proto__
fuga
を作成した際、__proto__
というプロパティが作成され、Hoge.prototype
への参照がセットされる。
よって、fuga.__proto__.shout()
とすることでHoge.prototype.shout()
が実行できる。
class Hoge { shout() { console.log('Fooooooo!!!'); } } const fuga = new Hoge(); fuga.__proto__.shout(); // Fooooooo!!!
method
メソッドを普通に実行しようとするとfuga.shout()
とすると思うが、実はこの時実行されているのはfuga.__proto__.shout()
。
class Hoge { shout() { console.log('Fooooooo!!!'); } } const fuga = new Hoge(); fuga.shout(); // Fooooooo!!! console.log(Hoge.prototype.shout === fuga.shout) // true
fuga.shout
がない場合、fuga.__proto__.shout
を見に行くようになっている。
fuga.__proto__
はHoge.prototype
への参照。
そうしていろいろたらい回しになってHoge.prototype.shout()
が実行されるという流れ。
Prototype Pollution
ここまでで見たプロトタイプの挙動を悪用するのがプロトタイプ汚染。
シンプルな例を見ていく。
Hoge.prototype.shout を上書き
Hoge
でfuga
を作成したあと、Hoge.prototype.shout
を別の関数で上書きする。
実行すると「Hmm...」が出力される。シャウトしてない。
class Hoge { shout() { console.log('Fooooooo!!!'); } } const fuga = new Hoge(); Hoge.prototype.shout = function() { console.log('Hmm...'); } fuga.shout(); // Hmm...
fuga
を作った時点でshout
は「Fooooooo!!!」のはずだ。
だがしかし、Hoge.prototype.shout
を書き換えたことでfuga.shout
も書き換わってしまった。
この時の流れはこうだ。
fuga.shout
は無いfuga.__proto__.shout
を探すfuga.__proto__
はHoge.prototype
の参照Hoge.prototype.shout
が実行される
Hoge.prototype.shout
は「Hmm...」で上書きされているため、fuga.shoutの結果は「Hmm...」となる。
fuga.__proto__.shout を上書き
今度はfuga
, piyo
を作成してからfuga.__proto__.shout
を上書きしてみる。
これも実行すると「Hmm...」が出力される。
fuga.__proto__
がHoge.prototype
の参照なのだから、当然といえば当然だ。
class Hoge { shout() { console.log('Fooooooo!!!'); } } const fuga = new Hoge(); const piyo = new Hoge(); fuga.__proto__.shout = function() { console.log('Hmm...'); } piyo.shout(); // Hmm...
Number.prototype.toString
組み込みオブジェクトのメソッドでも同様のことが可能だ。
Number
のtoString
で試してみる。
(1).toString(); // "1" Number.prototype.toString = function () { console.log("polluted😉"); } (1).toString(); // polluted😉
悪用方法
すぐ思いつくのはisAdminみたいな関数・フィールドの上書き。
RCEとかやってるのも見るけど、よくわからない。結構条件厳しそう?
発生しがちなところ
オブジェクトにプロパティを設定する、オブジェクトをマージ・クローンする等
オブジェクトに自由なキーでデータを登録できるところで発生する。
obj[key] = value;
上記3パターン等のサンプルコードなどはこちら。
ちゃんと「ありそう」なパターンで書いてあって実践的。
再現
nodejsほぼ初めて書くけど、再現してみた。
function isObject(obj) { return obj !== null && typeof obj === 'object'; } function merge(a, b) { for (let key in b) { if (isObject(a[key]) && isObject(b[key])) { merge(a[key], b[key]); } else { a[key] = b[key]; } } return a; } const express = require('express'); const app = express(); app.use(express.json()); app.post('/', (req, res) => { const guest = {}; console.log(req.body); const input = req.body; merge(guest, input); const admin = {}; res.send(admin.adminFlg + ":" + guest.adminFlg); }); app.listen(3000);
guest
を作成した後にreq.body
をマージしてadmin
を作成する。
以下のcurl*1を実行すればObject.prototype.adminFlg
が1に上書きされるので、admin.adminFlg
もguest.adminFlg
も1になる。
curl -X POST -H "Content-Type: application/json" -d '{"__proto__":{"adminFlg": "1"}}' localhost:8080
再現してわかったことだが、一度プロトタイプ汚染をするとずっと汚染されたままになる。
例えば上記のままCTFで出題したとする。
最初の一人がadminFlg
を1に上書きしたら、後からアクセスする全員は何もしなくてもadminFlg
が1になってしまう。
問題として破綻してしまうのだ。
CTFのメタ読みに使えそうな知識が身についた。
この記事はIPFactory Advent Calendar 2021の12/04分です。
IPFactoryというサークルについてはこちらをご覧ください.
昨日はn01e0による「Detecting fileless execution in Linux」でした。
明日はDuGlaserによる「ただ移行するだけでいいのか?」です。
*1:dockerで8080に実行してるのでポートが異なります