ペネトレーションしのべくん

さようなら、すべてのセキュリティエンジニア

AWS Lambdaの関数URLを使って、Prototype Pollution(プロトタイプ汚染)のサンプルを作ってみた

はじめに

kurenaifさんのプロトタイプ汚染解説動画の分かりやすさに感動して、復習がてら自分でも作ってみようと思い立ちました。

www.youtube.com

ついでに、AWS Lambdaが自前でURLを持てるようになったと聞いたので、それも試してみることにしました。

dev.classmethod.jp

リクエストに依存しない例

最初に想定していたのは以下のようなパターンだったのですが、うまくいきませんでした。

  • POSTリクエストのボディに入っているJSONを受け取る
  • そのJSONを既存のobjectにマージする(このタイミングでObjectが汚染される)
  • objectを新しく作り、汚染されたかどうか確認する

試しに以下のような、リクエスト抜きで完結するパターンを試したら、ちゃんと汚染されていました(これを汚染と呼ぶのかは微妙なところですが)。

exports.handler = async (event) => {
    const obj1 = {};
    obj1.__proto__.polluted = 1;
    console.log("obj1.polluted: " + obj1.polluted);

    const obj2 = {};
    console.log("obj2.polluted: "  + obj2.polluted);

    const response = {
        statusCode: 200,
        body: JSON.stringify(obj2.polluted),
    };

    return response;
};

リクエストによって汚染される例

どうしてもリクエストによって汚染されてほしいです。もろもろ頑張った結果、以下のコードで汚染されることを確認しました。

exports.handler = async (event) => {
    const obj1 = clone(JSON.parse(event.body));
    console.log("obj1.status: " + obj1.status);

    const obj2 = {};
    console.log("obj2.status: "  + obj2.status);

    const response = {
        statusCode: 200,
        body: JSON.stringify(obj2.status),
    };

    return response;
};

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;
  }
  
  function clone(obj) {
    return merge({}, obj);
  }

こちらの記事のコードを引用しました。おそらく merge()merge(a[key], b[key]) のところを、自前で実装できていなかったようです。{"__proto__":{"status":"polluted!"}}のようなJSONを読み込む時、このように再帰しないとプロトタイプのプロパティを適切に設定できないのだと考えています。

jovi0608.hatenablog.com

調査

merge() のところで何が起きているか知りたかったので、ログを出すようにしてみました。

  function merge(a, b) {
    for (let key in b) {
      console.log(isObject(a[key]) + ", " + isObject(b[key]))
      if (isObject(a[key]) && isObject(b[key])) {
        console.log("o: " + key + ", " + a[key] + ", " + b[key])
        merge(a[key], b[key]);
      } else {
        console.log("x: " + key + ", " + a[key] + ", " + b[key])
        a[key] = b[key];
      }
    }
    return a;
  }
  
  function clone(obj) {
    return merge({}, obj);
  }

結果、このようなログが出ました。

つまり、以下のようなことが起きている……?

  • merge({}, event.body)
    • 引数は {}event.body
    • forループに入る
      • {}.__proto__event.body.__proto__ いずれもobjectなので、merge() が実行される
        • merge({}.__proto__, event.body.__proto__)
          • forループに入る
            • {}.__proto__.statusevent.body.__proto__.status はいずれもobjectではないので、elseの処理が実行される
              • 前者はundefined、後者はstring("polluted!")
            • {}.__proto__.statusevent.body.__proto__.status が代入される
              • {}.__proto__ はこの時点で汚染されている?
            • forループが終了し、{}.__proto__{status:"polluted"})が元のプロセスへ返される
      • forループ終了が終了し、(汚染された?) {} が返される