🙌

JavascriptにおけるPrototype汚染: CVE-2022-46164

2022/12/19に公開

Javascriptにおける著名な脆弱性として、Prototype汚染が挙げられます。
今回prototype汚染を利用したCVEの内容を見て、どういった脆弱性なのかについて自分なりにまとめてみました。

Reference

https://www.cvedetails.com/cve/CVE-2022-46164/

Summary

NodeBB is an open source Node.js based forum software. Due to a plain object with a prototype being used in socket.io message handling a specially crafted payload can be used to impersonate other users and takeover accounts. This vulnerability has been patched in version 2.6.1. Users are advised to upgrade. Users unable to upgrade may cherry-pick commit 48d143921753914da45926cca6370a92ed0c46b8 into their codebase to patch the exploit.

NodeBB は、Node.js ベースのオープンソースのフォーラムソフトウェアです。socket.io のメッセージ処理に、プロトタイプを持つプレーンなオブジェクトが使用されているため、特別に細工されたペイロードを使用して、他のユーザーになりすまし、アカウントを乗っ取ることが可能です。この脆弱性は、バージョン 2.6.1 で修正されています。アップグレードすることをお勧めします。アップグレードできないユーザは 48d143921753914da45926cca6370a92ed0c46b8 を自分のコードベースにチェリーピックして、この脆弱性を修正することができます。

Affected Software

  • NodeBB up to v2.6.0 (< 2.6.1)

NodeBB はその名の通り Node.js ベースの掲示板のようですね。(BB はおそらく Bulletin Board ってコト...!?)
今回この NodeBB にPrototype Pollutionという javascript の prototype chain を利用した脆弱性が発見されたとのことです。

Enumeration

Prototype Pollution

ぼちぼち日記様が詳しく解説をしてくださってます。


javascript は prototype base の言語であり、メソッドや property の解決の際に prototype chain を辿ります。
https://developer.mozilla.org/ja/docs/Learn/JavaScript/Objects/Object_prototypes

プロトタイプチェーンの中では、メソッドやプロパティはあるオブジェクトから別のオブジェクトにコピーされないことを再確認しておきましょう。これらのメソッドやプロパティは、上で説明したようにチェーンを上っていくことでアクセスされます。


とあるように、一度 prototype chain の中に定義されたものは都度チェーンを辿って解決されます。
更に ECMAScript2015 で__proto__によって prototype chain へのアクセス(更に厳密に言うと[[prototype]]へのアクセス)が標準化されました。
なので以下のように全 Object に共通なメソッドやプロパティを定義できることになります。

const happy = {};
const grumpy = {};
happy.hello = "hello!";

// hello!
console.log(happy.hello);
// undefined
console.log(grumpy.hello);

happy.__proto__.hello = "You should say hello";

// You should say hello
console.log(grumpy.hello);

// a happy person has own property hello,so it will not be affected by the change of prototype
// hello!
console.log(happy.hello);

More Practical Example

上の例は不機嫌マンに挨拶を強要するだけで、つまらないです。(考えたのはお前だろ)
もう少し具体的な例でやってみます。
https://blog.sonarsource.com/nodebb-remote-code-execution-with-one-shot/

Auth.verifyToken = async function (token, done) {
  let tokens = [
    { token: "793a561", uid: 42 },
    { token: "1a444cf", uid: 1337 },
  ];

  // Array to Object: { "token" : "uid" }
  tokens = tokens.reduce((memo, cur) => {
    memo[cur.token] = cur.uid;
    return memo;
  }, {});

  // verify uid related to given token
  const uid = tokens[token];

  if (uid !== undefined) {
    if (parseInt(uid, 10) > 0) {
      done(null, {
        uid: uid,
      });
    } else {
      // if uid is less than 0, then it is a master token
      done(null, {
        master: true,
      });
    }
  } else {
    done(false);
  }
};

引数として受け取った token に紐づく uid の存在をチェックし、authenticated な User かを判定します。
nodeBB の仕様として、uid が 0 の場合は master token だとみなされます。
unix 系の特権ユーザが uid 0 なのを意識しているのだとすれば、master token さえ手に入れば全ての操作ができたりするのかな....?


さて、ここで問題になってくるのは、 reduce 関数の第 2 引数 {}です。
Object.create(null)等の方法で prototype を継承(簡単のため継承と言わせてください)しないようにしない限り、prototype は継承されます。
ここで示されている {}による初期化も例外ではありません。
つまりtokens は登録された token の他に,toString()constructorといった prototype に含まれるメソッド、プロパティにもアクセスできる事になります。
更に toString()等の関数は parseInt すると NaN となります。NaN と number の大小比較は常に false を返すので、else 文に突入し master token として扱われることに...
つまり我々は Object prototype に含まれる関数を token として渡すことで,master user として振る舞えることになります。

Reproduce

本題である CVE-2022-46164 に入ります。
早速 NodeBB の v2.6.0 時点でのsrc/socket.io/index.jsを見てみます。
以下のコードは簡単のため所々省略等編集をしています。

const Namespaces = {};

const eventName = payload.data[0];
const params = typeof payload.data[1] === "function" ? {} : payload.data[1];
const callback =
  typeof payload.data[payload.data.length - 1] === "function"
    ? payload.data[payload.data.length - 1]
    : function () {};

if (!eventName) {
  return winston.warn("[socket.io] Empty method name");
}

const parts = eventName.toString().split(".");

const methodToCall = parts.reduce((prev, cur) => {
  if (prev !== null && prev[cur]) {
    return prev[cur];
  }
  return null;
}, Namespaces);

if (!methodToCall || typeof methodToCall !== "function") {
  const escapedName = validator.escape(String(eventName));
  return callback({ message: `[[error:invalid-event, ${escapedName}]]` });

  methodToCall(socket, params, (err, result) => {
    callback(err ? { message: err.message } : null, result);
  });
}

初っ端の Namespaces が protytype を継承した Object なのが気になりますね...
methodToCall は reduce 関数で NameSpace を初期値として渡しているので、ここでtoString()などの prototype に含まれる関数を渡してやると
後続のチェックをすり抜けられそうです。
チェックをすり抜けられれば callback を実行させることができるので、任意の関数を自由に実行させることが出来そうですね...!

Remediation

https://github.com/NodeBB/NodeBB/commit/48d143921753914da45926cca6370a92ed0c46b8
こちらの PR で FIX となったようです。
Namespaces の prototype を削除して methodToCall のチェックが意図した動きになるよう修正したものと思われます。

一般的なprototype汚染の防ぎ方は調べれば素晴らしい先人の記事が沢山でてくるので、引用差せていただきます。

https://portswigger.net/web-security/prototype-pollution/preventing

感想

CVEから実際のコードを見ることはPrototype汚染の理解にかなり役に立ったなぁと思います。
javascript の忘れがちな prototype 回りの概念整理にもなってよかったです。

日頃の業務でも js,ts はよく使うので、これまで以上に User Input には気をつけたいですね。

また、rubyもprototype chainと似たような継承チェーンの概念が合ったと思います。
rubyの継承チェーンは汚染したりできるのでしょうか。いずれ調べてみたいと思います。

最後まで読んでいただきありがとうございました。

Discussion