Pillowのマルチフレーム画像読み込み機能は、モードの切り替え後に、以前のフレームで使用されていたネイティブの画像ストレージを再利用できる。
### 概要
Pillowのマルチフレーム画像読み込み機能は、あるフレームのために割り当てられたネイティブの`ImagingCore`ストレージを保持しながら、後続のフレームでPython側の画像`mode`やデコーダのタイルを変更することができる。
もし最初のフレームが、`L`のような狭いモードでストレージを割り当て、同じサイズの後のフレームが`RGB`や`CMYK`のような広いモードに切り替わった場合、Pillowは広いピクセルデータを、以前の狭いモードのネイティブバッファにデコードしてしまう可能性がある。これにより、ネイティブな範囲外への書き込みや、プロセスのクラッシュが発生する。また、MPO/JPEGの処理において、Pillowのドキュメントに記載されている`ImageSequence.Iterator`と`frame.load()`のパスを通じて、Python/Pillowの実行環境を一致させ、コマンド実行をデモンストレーションした。
### 詳細
根本的な原因は、`ImageFile.load_prepare()`が、`_im`が`None`の場合にのみネイティブストレージを再割り当てすることにある。この関数は、保持されているネイティブの`ImagingCore`ストレージが、`self.mode`や`self.size`と一致しているかどうかを確認していない。
いくつかのマルチフレームの`seek()`/`_open()`の処理において、後続のフレームのために`self._mode`やデコーダのタイルを更新するが、`_im`を無効化しない。その結果、デコーダは新しいフレームのモードに従って書き込みまたは読み込みを行うが、ネイティブストレージは依然として古いフレームのモードに関連付けられている。
影響を受ける領域として、以下のものが確認された。
1. **PSD**
`PsdImageFile.seek()`は、グレースケール/ネイティブの`L`ストレージから、RGBレイヤーのデコードに切り替わる際に、`_im`を保持する。これにより、攻撃者が制御できるネイティブな範囲外への書き込みや、確実なクラッシュが発生する。
2. **MPO/JPEG**
巧妙に作成された2フレームのMPOファイルを使用すると、最初のフレームが`L`ストレージを割り当て、その後、2番目のフレームが`CMYK`/`RGB`データを、保持されている`L`ストレージにデコードする。ドキュメントに記載されている`ImageSequence.Iterator`のパスを使用すると、`frame.load()`によってこの状態に到達できる。
Pillow 12.3.0において、ネイティブメモリの破壊、クラッシュ、範囲外読み込み、および限定的な無害なコマンド実行が確認された。
3. **DCX**
`DcxImageFile.seek()` が `PcxImageFile._open()` を呼び出し、これにより、ネイティブストレージモードの保護なしにモード/タイル状態が変更されます。 `L` の後に `RGB` の PCX フレームを含む悪意のある DCX ファイルを作成すると、ストレージの不整合が発生し、クラッシュします。
4. **MIC**
MIC は `TiffImageFile._open()` を直接呼び出すことができ、TIFF の通常の保護された `seek()` の動作を回避できます。 `NextIFD` に基づく到達可能なフレーム遷移により、ストレージの不整合が発生し、クラッシュします。
TIFF は、`seek()` の処理において、ストレージを再利用する前にサイズとモードの両方をチェックするため、有用な検証用として機能します。
### 影響
- プロセスのクラッシュ / サービス拒否
- ネイティブストレージ領域外への書き込み
- MPO パスにおけるネイティブストレージ領域外からの読み込み
より明確な影響:
- MPO/JPEG パスを介して、一致する Python/Pillow ランタイム環境でコマンドの実行が可能。
サーバーサイドでの影響:
- 認証されていない MPO ファイルのアップロードを受け入れ、Pillow を使用して自動的に処理する、脆弱な Web アップロードサービスを作成します。
- 攻撃は 1 つの HTTP リクエストで実行され、アップロード後のユーザー操作は不要です。
- 修正された無害なコマンド出力マーカーは、10 回中 10 回で成功しました。
- `_im=None` の制御は、10 回中 10 回で問題ありませんでした。
## 再現の証拠
テストバージョン:
```text
Pillow 12.3.0
```
MPO の検証環境:
```text
Python 3.12.3 + Pillow 12.3.0、クリーンな PyPI 環境:
マーカー 10/10、制御 0/10
Python 3.13.14 + Pillow 12.3.0、準備されたプロファイル:
マーカー 10/10、制御 0/10
```
サーバーサイドの Web アップロードの検証:
```text
制御 1~10: MARKER_MISSING
制御の概要:OK 0、欠落 10 /10
脆弱 1~10: MARKER_OK、マーカー出力 <サーバーユーザー>
脆弱の概要:OK 10、欠落 0 /10
```
ターミナルでの検証は、固定された無害なマーカーコマンドのみを使用します。 リバースシェル、永続化、データ窃取、または任意の攻撃者によって選択されたコマンドは使用しません。
私は、メンテナの方々に対して、以下の非公開データを提供できます。
- PSD、MPO、DCX、およびMIC形式のファイルに対する脆弱性対策の最小限のサンプル。
- 武器化されていないMPO形式のPoC(概念実証)。
- ローカル環境で脆弱なWebアップロードのデモ。
- gdb/GEFを使用した幾何学的データの解析。
- テスト実行のログ。
- 提案された回帰テスト用のデータ形式。
## 脆弱性を再現するための最小限のデータ形式
MPO形式の場合、脆弱性のあるデータ形式は以下の通りです。
```python
from io import BytesIO
from PIL import Image, ImageSequence
source = BytesIO(open(path, "rb").read())
image = Image.open(source)
iterator = ImageSequence.Iterator(image)
next(iterator).load()
next(iterator).load()
```
作成された画像データ形式は以下の通りです。
```text
フレーム0: モードL、幅W x 高さH
フレーム1: 同じ幅W x 高さH、CMYKなどのより広いモード
```
フレーム0は、ネイティブの`L`形式のストレージを割り当てます。フレーム1は、論理的なモードとデコーダの状態を変更しますが、既存のネイティブストレージを再利用できる可能性があります。フレーム1のデコーダは、より広いモードでデータを書き込み、狭いストレージに保存します。
## 修正の提案
ネイティブの`ImagingCore`ストレージを再利用する前に、Pillowは、保持されているストレージが現在の論理的な画像モードとサイズと互換性があるかどうかを確認する必要があります。
`ImageFile.load_prepare()`に中央のガードを設け、ネイティブストレージのモード/サイズを`self.mode` / `self.size`と比較し、それらが異なる場合は新しいストレージを割り当てるようにすることができます。あるいは、モードを変更するすべての`seek()` / `_open()`の処理において、デコードの前に`_im`を明示的に無効化する必要があります。
注意点:TIFF形式は、特殊なタイルサイズに関する動作があり、すでにモード/サイズのチェック機能があります。したがって、中央の修正は、TIFF形式のタイルバッファの処理を悪化させないようにする必要があります。
## 貢献者
```text
Rotem Kama
```
## 手法に関する注意
AIを活用した分析が、脆弱性の発見と検証のサポートに使用されました。このレポートに含まれる内容は、上記のローカルのデータ、コントロール、およびコマンド出力に限定されます。
影響を受けるパッケージ:
- pip (Python) Pillow: 影響を受けるバージョンは11.0.0以上、13.0.0未満。13.0.0で修正済み。