🥳

バグギルド第1回! CVE-2024-4367 – Arbitrary JavaScript execution in PDF.js

2024/05/29に公開

茶番

来たな新米バグハンター!ようこそバグギルドへ!

俺たちの業界(バグハント業界)では、世界中の脆弱性に飢えたハンター達が日夜脆弱性を見つけてはレポートとして報告し、その脆弱性にかけられた懸賞金をかっさらっていっている...

腕のたつハンターともなれば稼ぎまくりだ!
そんな夢のようなハンターという職業だが、伝説級のハンター、すなわちレジェンドハンターになるのはもちろん楽じゃあねえ...

このバグギルドは、お前ら新米ハンターが将来伝説級のハンターになれるように、腕利きのハンターたちが報告した脆弱性レポートを解説&読み込んでいきながらレジェンドハンターを目指すことを目的としたギルドだ!

とはいえこのギルドも立ち上げたばかり、、
このギルドのハンター(俺含め)から、将来レジェンドハンターが出ることを祈っている!!

レジェンドハンターになるにはそりゃあ血の滲むようなトレーニングが必要になるが、レポートを正しく読める能力は今後必ず必要になる!

コードが光って見えるようになるまで、俺たちの闘いは終わらねえ!!


ということで今回は、PDF.jsというJavaScriptのライブラリに存在する脆弱性であるCVE-2024-4367 – Arbitrary JavaScript execution in PDF.jsのレポートを眺めましょう🎊

本日のお題

Codean Labsなる会社のリサーチャーが発見したこれ。
まずはこのレポートのリンクにとんで、理解できなくても大丈夫なのでチラッとレポートを見てみることをおすすめします。
理解できた人は.... はい、ここでタブを閉じましょう。

Summaryとか

この脆弱性のざっくりまとめはこんな具合です。
Impact: High!
影響範囲: 主にWebアプリケーションにPDF.jsを使っている場合&Firefoxをブラウザとして使っているユーザ。

ここからは、上述のレポートを理解するのに必要となる周辺知識を解説してから、PDF.jsのレポートを再度精読してみます。自分も1周目レポート読んだときはマジで意味が分からなかったので、くじけずに繰り返し読み込みましょう。
まだ怪しいですが段々なんとなくわかってきます🥸

上述のCodean Labsのレポートをチラッと見るとわかりますが、この脆弱性を理解するためにはまあまあ事前知識が必要になります。
自分はわからない単語とか結構興味本位で調べて寄り道もしてしまうので、ここいらねえなと思った部分は飛ばしていただいて大丈夫です。

自分が書いてもらったこの記事を一通り見てもらってからもう一回Codean Labsのレポートを読んでみると、理解が深まると思います。

まずCodean Labs誰だヨ

あんただれよ的な。
オランダの若い会社らしい。
Home - Codean Labs


ホワイトボックスでコードレビューをしてくれたりするらしい。


アナリストは全員OSCPを持っているらしい。

Mozillaのことちゃんと知ってる?

Mozillaの伝説は1998年に始まった.....
Internet for people, not profit - Mozilla

非営利団体のMozilla Foundationと、会社組織のMozilla Corporationがあるそうな。
利益 << 理念らしい。

考えてみたらホームページに飛んだことすらなかった。。

もっじーらの歴史に残る偉大なプロダクトたち。

もっじーらはFirefoxだけじゃねーぞ!!

もっじーらのギッハブ

まずPDF.jsってなんや。

そろぼち本題に入ります。

PDF.jsは、JavaScriptで書かれたOSSのライブラリです。
特に我々バグハンターとして知っておくべきことは以下です。

  1. PDF.jsは、2011年にアンドレアス・ガルっていう人が始めたプロジェクト。はじめはFirefoxの拡張機能として作成され、その後2012年からはFirefoxに内蔵されて2013年からFirefoxでデフォルト有効になっている。
  2. PDF.jsはライブラリなので、Webアプリケーション開発者が自身のWebアプリケーション上でPDFをレンダリングするためにPDF.jsの機能を使用することがある。
  3. PDF.jsにはPDFビューアとしての機能が内蔵されていて、FirefoxブラウザでPDFを開いたユーザは、このPDF.jsライブラリの機能を使ってFirefox上でPDFファイルを見ることになる。

要するにPDF.jsはJavaScriptライブラリで、PDFを人間が読めるようにするためにFirefoxやWebアプリケーション上で使われてるってことみたいです。

PDFビューアってサラっと言うなヨ

先ほどチラッと出てきましたが、レポートの中でもPDFビューアっていう言葉が出てきます。
PDFビューアちゅーのは、名前の通りPDFを表示できるツールのことです。

具体例をあげると、多分皆さんよく使うAdobe Acrobat ReaderもPDFビューアの一つです。

普段ChromeやEdgeを使ってPDFを開くとブラウザでそのままPDFが見れると思いますが、あれはブラウザに内蔵されたデフォルトのPDFビューアがPDFを読めるように表示してくれています。

なので、別にAdobe Acrobat Readerを使おうと意識していなくても、ブラウザでPDFを見るときはPDFビューアを使っているということになります。

重要なのは、ブラウザごとにデフォルトのPDFビューアが違うんだということを意識&理解しておくことです。ここが分かっていると、この後の脆弱性の理解がしやすくなります。

PDF.jsはChromeやEdgeでは使われていないようなのでそれらのブラウザを使っている人たちは今回の脆弱性の影響はありませんが、Firefoxを使っている人は影響が出るというのはそういうことです。(ChromeとEdgeを使ってても、PDF.jsを使ってるWebアプリケーションの機能でPDFを見ると影響をうけます。)

PDFの核心に迫る...!

これがムズかったです😑
全部理解するのは大変なので今回のレポートの理解に必要な要点だけをディグりましょう。
↓の記事がかなりディグってました。
僕「PDFとは何か知りたい」 - Qiita

とりまPDF(Portable Document Format)というのは、1993年にAdobeによって開発されたファイルの形式、のことぽいです。
アプリケーション、ハードウェア、OSに依存しないでテキストやら画像やらを表示できるようなファイル形式、という点で革新的な技術だったらしいです。

まずはPDFをテキストエディタで開こう。話はそれからや。

開くと、なんかところどころ文字化けしてますが以下のようになっています。

もう閉じたいいいいい🫠

PDFはまず、マジックナンバーとPDF-1.7のようなバージョンの記述から始まります。(マジックナンバーはここでは見えてないです。)
また、PDFはオブジェクトという構成要素の集まりからできています。
このオブジェクトを参照することで、PDFの中でページ番号を指定したときにすぐにそのページに飛ぶ、みたいなことが実現できます。

更にこのオブジェクトというのも色んなタイプのオブジェクトがあるようで、例えば文字列を持つオブジェクトであったり、その文字列にフォントを適用するための、ゴシック体みたいなことを定義しているオブジェクトがあったりします。
まあとりま、オブジェクトっていうのがあるんだあと思っておいてください。

オブジェクト単位で見てみる

オブジェクトはざっくり以下のような形です。

1 0 obj
<< /Pages 2 0 R /Type /Catalog >>
endobj

オブジェクトにはオブジェクト番号なるものが割り当てられており、ここでは1 0 objという部分でオブジェクトに番号をあてています。

/Pages 2 0 Rは、「オブジェクト番号2のバージョン0を参照する」という意味で、 /Type /Catalog >>はカタログオブジェクトであることを示しています。
で、オブジェクトの〆としてendobjが来ています。

とりあえず以下だけ分かれば大丈夫です。

  1. PDFはオブジェクトという単位で定義されてる。
  2. 各オブジェクトは参照しあっているものがあり、それによってフォントを適用したりしてる。

PDFのフォントとかGlyph(グリフ)とか。

さあ、段々今回の脆弱性のミソに近づいてきています。特にグリフの理解が重要です。

まず、PDFで文字を表示、つまりレンダリングするのは結構大変です。

僕らがPDFでグラフとかフォントが適用された文字とかスクショとか色々なものが混じった文書を可視化できているっていうのは案外すごいことなんです。

以下を見ると、なんか色々やってんなあっていうのが分かると思います。


https://www.antenna.co.jp/pdf/reference/FontEmbedding.htmlから拝借しました

で、グリフっていうのは要するに、PDF上で実際に表示される文字の形のことを言うみたいです。厳密には違うかもですが、とりまPDFで見えてる文字がグリフか!と思って一旦OKだと思います。

このグリフをPDFビューアで見えるように可視化するには以下のステップを踏みます。

  1. 可視化したいグリフのデータを取り出す。
  2. データをもとにグリフのアウトラインを描く。アウトラインはグリフの輪郭のことです。
  3. このあとベジエ曲線やらの数学っぽい処理があってPDFビューアでレンダリングする。

ムズいですね。
まあとりあえず脆弱性を理解する上では、

  • グリフはPDFで表示される文字のこと。
  • PDFビューアによってレンダリングされるまでに、フォントメトリックスっていうのが適用されたり曲線の計算やらなにやら色々ある。フォントメトリックスっていうのは文字フォントに関する情報を保持したもので、特定のフォントがどういう高さ、どういう幅で表示される、とかを保持したもの。

上記くらいの理解で多分大丈夫です。
まだムズいですね。とにかくグリフにフォントメトリックスとかを適用して、表示されるグリフをいじくってPDFビューアでレンダリングするんですね。

そしてとうとうVulnerability Reportの精読へ。。


いざ尋常に!
改めてこいつを読みましょう。
最初は読めなかったこのレポートも、今の俺らなら読めるはずだ!!
※引用は全部Chromeで日本語翻訳したやつをのっけてます。

要約から。

要約
この投稿では、Codean Labs が発見した PDF.js の脆弱性CVE-2024-4367について詳しく説明します。PDF.js は、Mozilla が管理する JavaScript ベースの PDF ビューアです。このバグにより、悪意のある PDF ファイルが開かれるとすぐに、攻撃者が任意の JavaScript コードを実行できるようになります。PDF.js は Firefox で PDF ファイルを表示するために使用されているため、これはすべての Firefox ユーザー(<126) に影響しますが、プレビュー機能に PDF.js を (間接的に) 使用する多くの Web ベースおよび Electron ベースのアプリケーションにも深刻な影響を及ぼします。

何らかの方法で PDF ファイルを処理する JavaScript/Typescript ベースのアプリケーションの開発者である場合は、PDF.js の脆弱なバージョンを (間接的に) を使用していないことを確認することをお勧めします。緩和策の詳細については、この投稿の最後を参照してください。

改めて、CVE-2024-4367はMozillaがOSSとして開発・管理しているJavaScriptで書かれたライブラリであり、PDFビューアとしての機能を提供しています。
今回のCVE-2024-4367の脆弱性によって、悪意のあるJSコードを埋め込んだPDFファイルをPDF.jsによって開くと、攻撃者が任意のJSコードをターゲットユーザのブラウザ上で実行できることになります。
前述の通りPDF.jsはFirefoxではデフォルトで使われているPDFビューアなので、FirefoxユーザがPDFファイルを開くときに影響を受けることになります。

また、PDF.jsはライブラリとしてPDFビューアの機能を提供しているので、WebアプリケーションでPDFを開く機能を提供したいときにWebアプリケーション内に組み込まれていることがあります。その場合はFirefoxを使っていないユーザでもこの脆弱性の影響を受ける可能性があります。
ここでは詳しく触れませんが、Electronを使っているアプリケーションでも影響があるらしいです。

グリフレンダリング

これも先述の通り、この脆弱性の理解にはグリフというものをある程度知らなくてはいけません。
グリフはPDFビューアで表示される「文字」のことです。

以下はPDF.jsライブラリ内にあったコードです。

isEvalSupportが有効になっている場合、cmdsという変数をコンパイルして、最終的にはjsBufというところに入ります。
で、最終的にはreturn (this.compiledGlyphs[character]のところで、compiledGlyphs[character]なる関数を作り出してreturnで戻り値になります。

// If we can, compile cmds into JS for MAXIMUM SPEED...
if (this.isEvalSupported && FeatureTest.isEvalSupported) {
    const jsBuf = []; // 空のjsBuf配列を作成、ここにcmdsをぶっこんでいく
    for (const current of cmds) { // ここでcmdsがcurrentに入っていく。 pythonでいうfor i inてきなやつ。
      const args = current.args !== undefined ? current.args.join(",") : "";
      jsBuf.push("c.", current.cmd, "(", args, ");\n"); // さっきつくったjsBufに "c.", cmdsの値, "(", args, ");"がひとつずつ突っ込まれていく。
    }
    // eslint-disable-next-line no-new-func
    console.log(jsBuf.join("")); // 多分デバッグ用
    return (this.compiledGlyphs[character] = new Function( // 新しく関数を作る。compiledGlyphsが関数名
      "c", // 引数1
      "size", // 引数2
      jsBuf.join("") // compiledGlyphs関数の処理内容
    ));
  }

上記のコードを見た時に、バグハンターとしての視点で考えられるとレジェンドハンターに近づけます。
cmdsがjsBufに入って新しく関数が作られるので、このcmdsに入る値を好きにいじれれば、わんちゃん任意のコンパイルされたグリフをPDF.jsにレンダリングさせることができるはずです。

とりあえず、cmdsに入る値が大事。です。

なので次に、cmdsが作られる場所を見ていきましょう。

  compileGlyph(code, glyphId) {
    if (!code || code.length === 0 || code[0] === 14) {
      return NOOP;
    }

    let fontMatrix = this.fontMatrix;
    ...

    const cmds = [ // <====ここ!
      { cmd: "save" },
      { cmd: "transform", args: fontMatrix.slice() },
      { cmd: "scale", args: ["size", "-size"] },
    ];
    this.compileGlyphImpl(code, cmds, glyphId);

    cmds.push({ cmd: "restore" });

    return cmds;
  }

ここにcompileGlyphメソッドがあり、このメソッドがreturn でcmdsを返してくれます。なのでこのメソッドの動きを紐解く必要があります。

cmdsはsaveやらtransformやらscaleやらのキーに対応する値を取るように見えます。

ここで恐らく僕らがいじれるのはcmd: "transform", args: fontMatrix.sllice()の部分ですね。それ以外のsaveとscaleは固定文字っぽいです。

つまりどっかでtransformっていうパラメータなのかキーなのかわからないですが、そこのバリューに値をセットしてそれがそのままcmdに渡されれば、その時点でさっきのコンパイルされたグリフ、の値をいじることができます。

で、cmd達はthis.compileGlyphImpl(code, cmds, glyphId);でcompileGlyphImpleメソッドに渡されます。
transformのバリューがcmdにわたって、それがcompileGlyphImplメソッドに渡されるわけです。
ただ、transformのキーに設定されたバリューもそのままcmdに入るわけではなく、fontMatrix.sllice()とかいうのがあります。なのでここで何されているのかも見る必要があります。

以下は、compileGlyphImplメソッドから生成された値ををCodean Labsの人がデバッグしてくれてるぽいやつです。

c.save();
c.transform(0.001,0,0,0.001,0,0);
c.scale(size,-size);
c.moveTo(0,0);
c.restore();

c.transformにはデフォルトで0.001,0,0,0.001,0,0が入っているのがポイントです。他はまあいったんどうでもいいです。


ここまで難しくて僕の頭がダメになってきたので、一旦内容を要約します。

  1. ユーザインプットによってcmdsに入る値を操作でき、このグリフをPDFビューアがレンダリングする時にJSコードを実行できる脆弱性がありそう。 これがthis.compiledGlyphs[character] = new Functionのやつ。
  2. グリフはcompileGlyphメソッドでコンパイルされ、cmdsという値をreturnする。 このcmdsは配列で、{ cmd: "transform", args: fontMatrix.slice() },の部分がユーザインプットの影響を受ける可能性がありそう。
  3. transformにはfontMatrix.slice()で返る値が入るはず。
  4. 脆弱性があるかないかをチェックするには、fontMatrixの動きを調べる必要がある。

FontMatrixのディグり

FontMatrixの値はデフォルトで[0.001, 0, 0, 0.001, 0, 0]らしく、フォントを変えることによってこの数値配列が変わるらしいです。
で、このFontMatrixは数値を想定した配列らしいです。

しかしこのFontMatrix配列に入る値はデフォルトの数値以外にも値のソースとなる場所があります。
それがここのコード。

    const properties = {
      type,
      name: fontName.name,
      subtype,
      file: fontFile,
      ...
      fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX, // <====これ! 
      ...
      bbox: descriptor.getArray("FontBBox") || dict.getArray("FontBBox"),
      ascent: descriptor.get("Ascent"),
      descent: descriptor.get("Descent"),
      xHeight: descriptor.get("XHeight") || 0,
      capHeight: descriptor.get("CapHeight") || 0,
      flags: descriptor.get("Flags"),
      italicAngle: descriptor.get("ItalicAngle") || 0,
      ...
    };

getArray("FontMatrix")で、FontMatrixキーの値をとるようです。
この値、つまりFontMatrix配列に入る値が、今回の脆弱性におけるsourceとなります。transformがsourceじゃなかったんですね。

このsourceに任意の値を入れるには、PDFで以下のように書きます。

1 0 obj
<<
  /Type /Font
  /Subtype /Type1
  /FontDescriptor 2 0 R
  /BaseFont /FooBarFont
  /FontMatrix [1 2 3 4 5 (0\); alert\('foobar')]   % <----- これ!!
>>
endobj

上記の文字列を埋め込んだPDFをFirefoxで開くと....

動いた、、動いたぞ、、!!

POCはCodean Labsのレポートから手に入るので試してみてください。

まとめ

FontMatrix配列の値をいじる(source) => cmd: "transform", args: fontMatrix.slice()で、cmdにFontMatrixの配列たちが入る => compileGlyphメソッドによって、cmdがcmdsに入れられた状態でcmdsが返る => new Functionのところで作られる関数、compiledGlyphsによってcmdsがPDFにレンダリングされてJSが発火(ここがsink)

この脆弱性によるインパクトとしては、大きく2パターン考えられます。

  1. WebアプリケーションにPDF.jsライブラリによるPDF描画機能を使っている場合
    => JSによるDOMアクセスができた場合、Cookie情報や開いているファイル名、ログイン情報といった機密情報を外部に送信できる可能性があります。
  2. Webアプリケーション埋め込みではなく、単にFirefoxを使ってるユーザがこの脆弱性を狙ったPDFファイルを開いた場合
    => PDFファイルが開かれたパスを攻撃者が知ったり、不正なファイルをダウンロードさせたりといったことができる可能性があります。

WebアプリケーションにPDF.jsが組み込まれている場合は、JSの実行によってユーザ情報や機密情報にアクセスできる可能性が上がるので、ケース1の方がインパクトが大きくなる可能性が高そうです。

いやはや、初回にしてはハードルが高かった🙃。

最強のレジェンドハンターになってチャリンチャリンするまで、俺たちの歩みは止まらない!

Discussion