Skip to content

Instantly share code, notes, and snippets.

@mala
Last active December 29, 2016 18:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mala/e219cd7dcdf8f1d1382d to your computer and use it in GitHub Desktop.
Save mala/e219cd7dcdf8f1d1382d to your computer and use it in GitHub Desktop.
Amon2::Util の random_string の実装に起因する脆弱性

(まだ影響範囲調査 + いくつかアップデート予定なので公開ブクマなしで、社内共有はOKです)

Amon2::Util の random_string の実装に起因する脆弱性

Amon2::Util の random_string は暗号論的に安全ではありません。

Amon2::Utilのrandom_stringの無作為性はsrandのみに依存しています。引数無しのsrandを呼び出したときに使われるseedは32bit intです。

# 大体43億ぐらいの値が出てきます 
List::Util::max map { srand } 1..10000

# 2**32より大きい値を入れたら戻ります
do { srand(1); rand } == do { srand(2**32+1); rand } # => 1

# 小数点以下は切り捨てられます
do { srand(1); rand } == do { srand(1.1); rand } # => 1

srandは初回のrand呼び出しの際に自動で呼ばれます。また使える場合には/dev/urandomが使われます。urandomが使われますが、srandに与えられるseedは32bitなので、Perlにおいて、randの出力パターンは、2 ** 32 通りしかありません。

暗黙のうちに呼ばれるsrandの箇所

Amon2::Util::random_string は、ランダムな英数字の生成にrandのみ使っていました。そのため「生成していく文字列のパターン」は、2 ** 32 通りです。複数回呼ばれた時のrandom_stringの出力結果は232ではなくもっと多いです(62n文字)。seedが確定すれば、次に出力されるrandom_stringの値も決まっているので、そのパターンが 2**32通りということです。


amon2-setup.pl で自動生成される HTTP::Session2 のsecret の取得が可能になる問題

amon2-setup.pl で自動生成されるアプリケーションの雛形で使われるHTTP::Session2のsecretの生成にrandom_stringが使われていました。このsecretはセッションデータをcookie自体にシリアライズして保持するClientStoreの改変防止の署名に使われていました。

ClientStoreが選択されている場合

  • 自分宛てに発行されたsession cookieをオフラインで解析することで、secretを求めることが出来ます。
  • 一番最初に生成される random_stringの出力は 2 ** 32 しかないので、約43億回のブルートフォースアタックでsecretを取得することができます。

secretが漏れた場合の影響

以下のような影響があります。ClientStoreを使っている場合は、必ず、早急に、アップデートし、その上でsecretを再度生成してください。

セッション情報の改変による他のユーザーへの成りすまし

  • セッション情報に { user_id: 公開されているユーザーid } といったハッシュが格納されていて、それを元にログイン処理を行っている場合、他のユーザーに成りすますことが出来ます。
  • ユーザーの識別に、本人以外から取得や推測が困難な、内部id(連番でないUUIDなど) or アクセストークン等が使われていた場合、この攻撃は成立しません。

最悪の場合、任意のコード実行

  • 細工されたStorableオブジェクトを渡すことで、Storable::thawが呼び出された際に、DESTROYメソッドを通じて、最悪の場合、任意のコード実行が起こる可能性があります。
  • 典型的には一時ファイルを削除するための処理が呼ばれて、任意のファイルの削除が可能になる
  • 任意コード実行には、アプリケーションが、任意コードを実行しうる性質のあるDESTROYメソッドを備えたモジュールをロード済みである必要があります。

追加情報

  • 大規模なサイトでは、そもそもセッションストレージにClientStoreを選択すべきではありません。
  • サーバーサイドのストレージを使った場合と同じ感覚で「セッションの中身がユーザーからは見られない」前提でシリアライズされたオブジェクトをセッションに格納した場合、ユーザー本人からでも見れてはマズイようなデータが含まれてしまう可能性があります。

参考文献

信頼出来ないStorableをthawしてはいけない

Railsの事例

Rails4で、署名されたCookie(ユーザーが中身見れる、改変はsecretが無いと不可) から、暗号化されたCookie(ユーザーからも中身が見れない、改変は同じくsecretが無いと不可) に変更された。

Cookie自体にセッション情報を格納する方式の問題点としてサーバー側から強制的にログアウト出来ない問題が存在している。(時刻と署名とセットで指定時刻来たら無効にすることは出来る、有効期限が最初にセッション設定したタイミングに依存する)


Amon2::Plugin::Web::CSRFDefender が生成するCSRF tokenの推測が可能になる問題

影響

  • アプリケーションを動かしているサーバー実行環境によっては、現実的な試行回数でCSRF攻撃に成功する可能性があります

実際に攻撃が成立するかどうかはサーバー実行環境によりけりです。アップデートが推奨されますが、多くの場合緊急ではありません。workerで共通のseedが使われてしまう問題があるサーバーを使っている場合は、そちらも同時にアップデートしたほうが良いです。

攻撃パターン1: 初期シードを確定させて同一プロセスで生成されるrandom_stringを予測する

Amon2::Plugin::Web::CSRFDefender が生成するCSRF tokenにrandom_stringが使われており、生成される文字列のパターンは、大体43億通りということになります。

43億通りのパターンを予め作っておけば、攻撃者は自分宛に生成されたCSRF tokenを観測することで、srandに与えられたseedの予測が可能になります。 2 ** 32 * 32bytes だと単純計算で137GBになります。いまどき現実的なサイズですね。実際にはもっと効率的に保存出来るでしょう。

初回に発行されたCSRF tokenからseedを予測するなら43億通り用意しておけばいいですが、「n回目のリクエストで生成されたCSRF token」から初期seedを予測するには、用意しなければいけないデータがn倍になっていくので、予測が大変になります。 最初の数回分のデータを作っておくケースと、取りうる値すべてを予め計算しておくケースの両方が考えられます。max-requests-per-childの数が少ないのであれば、生成されうる全てのCSRF tokenを(現実的に用意可能なストレージのサイズで)全て事前に計算しておくことが可能でしょう。

攻撃パターン2: 子プロセスが同じseedを使っちゃう問題があるサーバー上で動かしている

prefork型のサーバーで、子プロセスが全て同じseedを使ってしまう問題が知られています

fork前にsrandが呼ばれてて、fork後にsrandが呼ばれていないと、全てのworkerが同じseedを使います。つまり、同じタイミングで兄弟workerが生成したCSRF tokenは同一になる可能性があります。自分宛に発行されたCSRF tokenを別の誰かが使っているかも知れません。

このタイプの攻撃の場合、seedの予測に成功しなかった場合でも成立します。ただし、アプリケーションが別の箇所でrandを呼び出していれば(randの呼び出し回数が安定してなければ)、random_stringの出力が一文字ずつズレていくので、一致しなくなります。(リクエストによってrandが使われたり使われなかったりすれば、ズレる)

実際に攻撃できんの?

  • 標的が決まってる場合:「ターゲットに対してCSRF tokenが発行されたタイミング」と「CSRF tokenが生成されたプロセスで使われていたseed」を両方予測して、取りうるCSRF tokenの値全てに対してCSRF攻撃を試行する
  • 誰でも良い場合: 自分宛に発行されたCSRF tokenをコピーして、パターン2の問題があるアプリのユーザーを無差別に狙う

攻撃が成立しやすくなる条件

  • 「アプリがrandを他の箇所で呼び出していない or 呼び出し回数が予測できる」
  • 「workerの数が少ない、あるいは、子プロセスが同じseed使っちゃう問題のあるサーバー構成で動かしている」
  • 「任意タイミングでサーバー再起動できる + max-requests-per-childが大きい」or「ターゲットに任意タイミングでセッション発行できる + max-requests-per-childが少ない」

max-requests-per-childの数の大小

  • 小さい場合、生成されるrandom_stringのパターンが少なくなります
  • 全パターン用意するタイプのseed予測に必要なデータサイズが小さくて済みます、また、取りうるCSRF tokenの数が少なくなるので攻撃の試行回数が少なくて済みます(危険度アップ)
  • ターゲットが既に死んでるworkerで作られたCSRF tokenを持ってる場合、初期seedを遡って予測することが出来ないので、CSRF tokenの予測もできなくなります(危険度ダウン)
  • 大きい場合、seed予測に成功した場合、ターゲットも同じseedで生成されたCSRF tokenを使ってる確率が高くなります
  • 一度予測したseedを長期間使い続けられるという点で攻撃をうけやすくなります

攻撃が困難になる条件

  • workerの数が多い(攻撃者からはターゲットがどのworkerに当たるのか制御できないので、試行回数が増える)
  • アプリ内でrand呼び出しがあったりなかったりする(生成されるrandom_stringがズレていくので、試行回数が増える)

セッションIDが推測可能な脆弱性との対比

セッションハイジャックの場合

  • セッションIDが予測可能だった場合、乗っ取り対象が誰でもいいのであれば予測したセッションIDで淡々とリクエストするだけで攻撃が成立します。
  • 攻撃者は自分一人で黙々と攻撃を試行することが出来ます。

CSRF攻撃の場合

  • 予測したCSRF tokenを使っている人を対象に攻撃サイトに誘導しなくてはいけません。
  • 予測したCSRF tokenが実際に過去に生成したことがあるCSRF tokenであっても、そのCSRF tokenが発行されたユーザーと一致しなければ攻撃が成立しません

CWE-330: Use of Insufficiently Random Values

ではありますが、少なくとも「ターゲットを絞ったセッションハイジャック」相当には攻撃が困難です。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment