Web 應用程式經常需要建立一個難以被猜解的令牌,例如,會話令牌、CSRF 令牌或者在忘記密碼功能中用於郵件重設密碼使用的令牌。這些令牌本應該被進行加密保護,但實際中卻經常被使用多次調用 rand函數並轉換輸出為字串來表示。本文將展示預測使用 rand產生的令牌其實並不那麼困難。
rand 是如何工作的
在 PHP 中,函數 rand 用於產生偽隨機數,而初始化隨機數的種子是由 srand 產生的。如果使用者不選擇調用 srand,那麼 PHP 會將一個非常難以猜測的數字作為隨機數產生器的種子。由 srand產生的種子將完全決定了 rand函數產生的隨機數。
隨機數產生器會保持由 srand初始化過後的狀態,並在每次調用 rand後改變。這個狀態與進程狀態相關,所以兩個進程通常不會返回相同的 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;}
如你所見,上述代碼首先調用 rand來決定是使用大寫字母、小寫字母還是數字,然後在選擇一個特定的字母或數字。每當我們請求 index.php 時都會產生一個新的 CSRF 令牌,所以我們可以隨意的發出請求以產生我們需要的令牌。我們的目標是預測分配給使用者的令牌,這樣我們就可以進行 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 來加快運行速度。這裡給出兩個版本的代碼,一個調用的是 glibc rand,另一個模仿的是 Windows rand。它是基於 token.php中的 PHP 代碼,使用 PHP 中的宏 ext/standard/rand.c來迴圈嘗試找出可能的種子。在 Windows 上大約只需要十來分鐘,Linux 上大約需要幾個小時。
一旦攻擊完成,就擁有了跟伺服器處在相同狀態下的隨機數產生器,從而可以同伺服器產生相同的令牌。通過將自己產生的令牌與伺服器返回的令牌進行比對,就可以知道哪些令牌已經被分配給了使用者,進而可以開始攻擊。
Linux 上的狀態攻擊
在 Windows 上,猜解 srand參數和隨機數產生器的狀態是一回事,但在 Linux 上則不同。glibc 的 rand()保持一系列的數字,並決定下一個狀態像下面這樣:
state[i] = state[i-3] + state[i-31]return state[i] >> 1
所以每一個輸出近似是之前 3 和 31 的結果之和。考慮如下令牌:
- 6ZF5kNgonV
- 9h3byovpGR
- gGt0A94U92
現在,該決定下一個隨機數是否為大寫字母、小寫字母還是數字。這將由之前第 3 和 31 次的結果決定, gGt0A94U92中的 9 和 9h3byovpGR中的 y。所以,我們預計下一個 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駭客與極客(FreeBuf.COM)