Web アプリケーションでは、セッション トークン、CSRF トークン、パスワードを忘れた場合の電子メール パスワードのリセットに使用されるトークンなど、推測が困難なトークンを作成する必要があることがよくあります。これらのトークンは暗号化して保護する必要がありますが、実際には rand 関数を複数回呼び出し、出力を文字列に変換することで表現されることがよくあります。この記事では、rand を使用して生成されるトークンを予測するのはそれほど難しくないことを示します。
PHP では、関数 rand を使用して疑似乱数を生成し、乱数を初期化するためのシードは srand によって生成されます。ユーザーが srand を呼び出すことを選択しなかった場合、PHP は非常に推測しにくい数値を乱数ジェネレーターにシードします。 srand によって生成されたシードは、rand 関数によって生成された乱数を完全に決定します。
乱数生成器は、srand によって初期化された状態を維持し、rand を呼び出すたびに変更されます。この状態はプロセスの状態に関連しているため、通常、2 つのプロセスが同じ rand 乱数を返すことはありません。
使用するサンプル プログラムは EZChatter です。これは CSRF トークンを生成するための小さなプログラムですが、作成時にセキュリティが十分に考慮されていませんでした。
public static function gen($len = 5){ $token = ''; while($len--){ $choose = rand(0, 2); if ($choose === 0) $token .= chr(rand(ord('A'), ord('Z'))); else if($choose === 1) $token .= chr(rand(ord('a'), ord('z'))); else $token .= chr(rand(ord('0'), ord('9'))); } return $token;}
Asご覧のとおり、上記のコードは最初に rand を呼び出して、大文字、小文字、数字のいずれを使用するかを決定し、次に特定の文字または数字を選択します。新しい CSRF トークンは、index.php をリクエストするたびに生成されるため、必要なトークンを生成するためにランダムなリクエストを行うことができます。私たちの目標は、CSRF 攻撃を実行できるように、ユーザーに割り当てられたトークンを予測することです。
前に述べたように、乱数シーケンスはシードによって完全に決定されるため、正しいものを推測するために、srand のパラメーターとして考えられるすべての数値を単純に試すことができます。発電機の状態。ただし、これはサーバー プロセスが新しく作成された場合にのみ Linux で機能することに注意してください。サーバーがすでに多くの rand 呼び出しを生成している場合、同じ状態を取得するにはクラッカーで同じ回数の呼び出しを繰り返す必要があります。 Windows では、乱数生成器の状態は srand のパラメータにのみ関係するため、繰り返し処理を実行する必要はありません。
新しく作成したプロセスからトークンを取得したい場合は、次の PHP スクリプトを使用してクラックできます:
for ($i = 0; $i < PHP_INT_MAX; $i++) { srand($i); if (Token::gen(10) == "2118Jx9w3e") { die("Found: $i \n"); }}
srand を検索するには、合計 4294967295 個の可能なパラメータがありますそれだけの価値はありますが、所要時間は約 12 時間です。ただし、PHP は glibc の rand 関数のみを呼び出すため、PHP コードを C に再変換して実行を高速化できます。ここでは 2 つのバージョンのコードを示します。1 つは glibc rand を呼び出し、もう 1 つは Windows rand を模倣します。これは token.php の PHP コードに基づいており、PHP のマクロ ext/standard/rand.c を使用して、考えられるシードの検索をループします。 Windows では約 10 分、Linux では数時間しかかかりません。
攻撃が完了すると、乱数生成器がサーバーと同じ状態になるため、サーバーと同じトークンを生成できます。自分で生成したトークンとサーバーから返されたトークンを比較することで、どのトークンがユーザーに割り当てられているかを知ることができ、攻撃を開始できます。
Windows では、srand パラメーターと乱数ジェネレーターの状態を推測することは別のことですが、Linux では異なります。 glibc の rand() は一連の数値を保持し、次のように次の状態を決定します。
state[i] = state[i-3] + state[i-31]return state[i] >> 1
したがって、各出力は前の 3 件と 31 件の結果のほぼ合計になります。次のトークンを考えてみましょう:
ここで、次の乱数が大文字、小文字、または数字を使用してください。これは、以前の実行 3 と 31、gGt0A94U92 の 9 および y の 9h3byovpGR の結果によって決定されます。したがって、次の rand(0, 2) の出力はおよそ ⌊10/10 + 25/26 × 3⌋ = 2 mod 3 になると予想されます。これは、数値が得られることを意味します。以下では、この数値が予測できると仮定すると、次回の rand の呼び出しで取得される数値は、前回の 3 回目の rand の呼び出しで取得された数値と、前回の 31 回目の rand の呼び出しで取得された小文字によって決まります。この数値は、 ⌊2/3 + 1/3 × 10⌋ = 0 mod 10 と ⌊3/3 + 2/3 × 10⌋ = 6 mod 10 の間になります。したがって、値は 0 から 6 の間であると予想され、最終的には 4 になります。
ご覧のとおり、これでは正確に予測できません。メソッド次の乱数ですが、おおよその範囲を予測できることも明らかであり、もはや乱数とは言えません。同じ方法を使用して glibc の乱数発生器の状態を推測することもできますが、ここでは試みません。
暗号的に安全な乱数生成器を使用する必要があります。 rand を使用して乱数を生成すると、多くの場合、乱数発生器を推測できるため、トークンが予測可能になります。 Linux ではトークンの予測が少し難しくなりますが、それでも安全ではありません。 Windows 上の乱数ジェネレーターは、乱数ジェネレーターの状態が数分以内に推測できるため、悪用が比較的簡単です。
*原文: sjoerdlangkemper.nl、FB 編集者 xiaix が編集、FreeBuf Hackers and Geeks (FreeBuf.COM) から転載