事例:CeleryのPython RCE脆弱性
Calum Hutton
2022年2月15日
0 分で読めます概要
既知のPython脆弱性をもとに調査を行い、それらに共通するソフトウェアのパターンを特定しました。Snyk Codeの基盤でもある社内静的解析エンジンを活用してカスタムルールを作成し、大規模なオープンソースコードのデータセットを検索することで、同じパターンを使用している他のプロジェクトを特定できました。その結果、Celeryに保存型コマンドインジェクションの脆弱性を発見しました。Snyk Codeのような静的アプリケーションセキュリティテスト(SAST)ツールを使えば、静的なソースコードを解析し、非効率または危険なコードのパターンを特定することで、開発者はソフトウェアのバグを検出できます。
調査の動機
個人的な経験と、オープンソースのPythonプロジェクトにおける脆弱性調査(CVE-2017-11610およびCVE-2021-32807)をきっかけに、この調査プロジェクトに取り組みました。私の仮説は、オブジェクトトラバーサル(あるオブジェクトから任意のオブジェクト属性への参照を取得できる機能)がPythonで広く使われているというものでした。この仮説を検証し、Pythonエコシステム全体でこのパターンがどの程度使われているかを明らかにするとともに、セキュリティ脆弱性につながり得る任意(またはほぼ任意)のオブジェクトトラバーサルの事例を特定したいと考えました。
背景
Pythonでは、ほぼすべての要素がオブジェクトであり、それぞれが明示的な属性や継承した属性、メソッドを持ちます(クラスのインスタンスやモジュールも含まれます)。そのため、Pythonアプリケーションでは、オブジェクトの名前空間をたどって、その属性やサブ属性への参照を取得できる場合があります。これを実現する方法の一つが、再帰的な属性検索です。
以下の簡単なコード例は、Python 3でオブジェクトの名前空間をたどる方法を示しています。無害なモジュール(random)をインポートすると、getattrを使ってインポート済みのosモジュール(_osという別名)への参照を取得し、危険な関数(os.system)にアクセスできます。
Pythonアプリケーションが、要求するオブジェクトの名前空間パスや属性をユーザーが操作できる形でオブジェクトトラバーサル機能を公開している場合、任意(またはほぼ任意)のオブジェクトトラバーサルにつながり、次のような問題が発生する可能性があります。
公開されたオブジェクトトラバーサル機能によって最も起こりやすい影響は、アクセス制御の不備や情報漏えいです。ユーザーが
obj._private_method() or obj._secretのような非公開メソッドや属性への参照を取得できる場合に発生します。任意のメソッド(
os.system()など)への参照を取得し、ユーザーが指定した引数で呼び出せる場合は、リモートコード実行(RCE)も可能です。ただし、メソッドへの参照を取得したうえで、引数を渡せる必要があるため、発生する可能性はそれほど高くありません。
つまり、Pythonアプリケーション内のオブジェクトトラバーサルを制御または操作できる攻撃者は、特定のモジュールやクラスインスタンスの属性およびサブ属性にアクセスし、その機能を制限なく、または想定外の方法で利用できる可能性があります。
脆弱性につながるパターン
上記のCVEに関連するパターンとして特定されたのは、通常、Pythonのドット区切りパスを使った再帰的な属性検索です。パスを分割し、ループ内で順に処理します。現在のパス要素を使ってgetattr()でコンテキストへの参照を取得します。前回のgetattr()呼び出しで取得した参照を次のコンテキストとして使い、パスの要素がなくなるまでこの処理を繰り返します。次のスクリプトがその例です。
ドット区切りのパスは、2つの文字列(_osとsystem)に分割されます。ループの最初の反復では、cls変数はrandomモジュールへの参照です。そのため、getattr()はrandomモジュールの_os属性を検索し、osモジュールを取得します。次にgetattr()のコンテキストがosモジュールとなり、次の反復でosモジュールのsystem属性、つまりos.system()が取得されます。
事例(Celery - CVE-2021-23727)
Snyk Codeエンジンを使い、他のオープンソースPythonコードにも同じパターンがないかを特定するルールを作成しました。ルールに一致したものの一つが、分散メッセージパッシングに基づくオープンソースの非同期タスクキュー、Celeryの内部にありました。ルールはcelery.backends.base.Backendクラス内のexception_to_python関数に一致しました。

関数内の354行目に、再帰的なgetattrパターンが見られます。関数をさらに調査し、Celery内での使われ方を確認したところ、excのdictオブジェクト全体が、Celeryバックエンドに保存されたJSONデータに由来することが分かりました。そのため、バックエンドサーバーにアクセスできるユーザーがこのデータを制御または操作できる可能性があり、dictのすべてのフィールドを汚染されている可能性のあるデータとして扱う必要があります。
コードをさらに分析した結果、excのdictオブジェクトのプロパティ(352行目)に基づいて、任意のモジュールへの参照が取得されることが分かりました。次に、再帰的な属性検索パターンを使って、そのモジュールの任意の属性を検索します。取得した属性への参照は、同じくexcのdictオブジェクトから取得した単一の文字列引数とともに呼び出されます(364行目)。
excのdictオブジェクト全体が汚染されている可能性があり、任意のモジュールや属性へのアクセスを防ぐ検証も行われていないことから、このコードは保存型コマンドインジェクションに対して脆弱である可能性が高いと判断しました。
Pythonコンソールで関連するクラスをインポートし、Celeryバックエンドのデータベースに攻撃者が悪意のあるJSONデータを作成した状況を再現するdictを用意して、この仮説を実証しました。
まず、脆弱な関数に渡すPythonのdictを作成します。このdictには、アクセスするモジュール(exc_module)、そのモジュールから参照を取得する属性(exc_type)、そして取得したメソッドに渡す引数(exc_message)を指定するプロパティが含まれています。
続く数行では、脆弱なコードをCeleryからPythonコンソールにインポートし、Backendオブジェクトを使ってCeleryクラスを初期化します。
最後に、dictを脆弱なメソッドexception_to_pythonに渡すことで、脆弱性が発動します。
上記コードスニペットの最後の行にはidコマンドの出力が表示されています。これにより、作成したdictがデシリアライズされ、Backendクラス内で任意のコマンドインジェクションが実行されたことが分かります。
Celeryを利用するシステムでこの脆弱性が悪用されると、プロデューサー内でコマンドインジェクションが起き、システム全体を乗っ取られる可能性があります。Celeryバックエンドがリモートにある場合は、バックエンドへのアクセス権を得た攻撃者が組織のネットワーク内を水平移動し、追加のネットワークインフラに足がかりを築くことも可能になります。
修正方法
この問題はCeleryに責任ある形で開示され、ソフトウェアのバージョン5.2.2で修正されました。対象モジュールを検証し、解決された属性の型を確認してから、汚染されている可能性のある入力を使って任意の関数を呼び出すように変更されています。一般に、オブジェクトをデシリアライズする際は、リモートデータソースやデータベースに由来する場合でも、汚染されている可能性のあるデータに十分注意する必要があります。可能であれば、デシリアライズの前、または少なくともデシリアライズされたオブジェクトのメソッドやプロパティを呼び出す前に、オブジェクトの型を確認・検証してください。
Celeryのコマンドインジェクション脆弱性の発見と、必要な汚染データ分析は、Snyk Codeによって効率化できました。革新的なSASTツールであるSnyk Codeは、開発者にとって使いやすく、高速で正確な、従来のセキュリティツールに代わる選択肢です。IDEプラグインにより、既存のワークフローにリアルタイムテストを組み込み、開発者はわずか5分で脆弱性を発見・修正できます。今すぐ無料トライアルを開始して、Snyk Codeで安全な開発をシンプルに実現しましょう。
Capture the Flagを始めよう
オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。
参考情報
Celery(CVE-2021-23727): https://security.snyk.io/vuln/SNYK-PYTHON-CELERY-2314953



