よーでんのブログ

One for All,All for わんわんお!

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 を上書き

Hogefugaを作成したあと、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も書き換わってしまった。

この時の流れはこうだ。

  1. fuga.shoutは無い
  2. fuga.__proto__.shoutを探す
  3. fuga.__proto__Hoge.prototypeの参照
  4. 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

組み込みオブジェクトのメソッドでも同様のことが可能だ。

NumbertoStringで試してみる。

(1).toString(); // "1"

Number.prototype.toString = function () {
    console.log("polluted😉");
}

(1).toString(); // polluted😉

悪用方法

すぐ思いつくのはisAdminみたいな関数・フィールドの上書き。
RCEとかやってるのも見るけど、よくわからない。結構条件厳しそう?

発生しがちなところ

オブジェクトにプロパティを設定する、オブジェクトをマージ・クローンする等
オブジェクトに自由なキーでデータを登録できるところで発生する。

obj[key] = value;

上記3パターン等のサンプルコードなどはこちら。
ちゃんと「ありそう」なパターンで書いてあって実践的。

jovi0608.hatenablog.com

再現

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.adminFlgguest.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」でした。

feneshi.co

明日はDuGlaserによる「ただ移行するだけでいいのか?」です。

duglaser.dev

*1:dockerで8080に実行してるのでポートが異なります