某cms任意賬戶密碼重置漏洞分析和利用
上次我po過某cms的郵箱找回密碼功能的驗證模塊不太嚴謹,這次又找到一個信息泄漏漏洞,導致的後果是攻擊者可以重置任意郵箱賬號的密碼。攻擊者可以通過某cms的郵箱找回密碼功能從自己的郵箱中獲取關鍵參數key,拿到key之後在本地利用演算法本身的問題可以算出配置文件中的私鑰pwdhash的md5值。之後再利用找回密碼邏輯上的問題與pwdhash的md5值可以自行構造密碼重置鏈接通過服務端的檢查重置任意郵箱賬戶的密碼。
漏洞詳情
生成重置密碼鏈接中的key參數加密過程:
/upload/Application/Home/Controller/MembersController.class.php
$str 字元串:『e=$email&k=$key&t=$time』
這裡的 $time 是第一個不可控點,但是通過在發送請求前和成功收到響應後記錄記錄時間可以將 $time 控制在一個很小的範圍,即使考慮本地和伺服器獲取的時間區別前後各加上5秒的延遲補償,時間範圍最差也在20秒以內。
C(『PWDHASH』) 是網站程序加密過程的私鑰,
在安裝時隨機生成 /upload/Application/Common/Common/function.php
$encrypt_key 是加密過程中的第2個不可控點,簡記為en,共32000種可能。for循環加密過程很簡單,將txt字元串和en字元串逐位異或的結果與en字元串逐位拼接,當下標走到32時,en的指針歸0(因為en字元串只有32位,而通過簡單判斷txt字元串長度肯定超過32位),最後生成的tmp,記作tmp1字元串如下:
/upload/Application/Common/Common/function.php
passport_key的加密過程和encrypt類似,逐位異或,不拼接,for循環結束後生成的tmp,記作tmp2字元串如下:
passport_key 將tmp2 返回給encrypt 後再經過base64編碼就是重置鏈接中的key參數。
現在回顧一下加密過程用到的參數
$email :輸入郵箱,可控
$time :代碼執行時間,通過監測發包和收包的時間,可控在一個範圍內,比如10秒
md5(rand(0, 32000)) :32000種遍歷結果
pwdhash :未知
即 en_func($email, $time, md5(rand(0, 32000)), md5(pwdhash)) = key
由於異或操作
$a ^ $ = $c => $a^$c = $b
所以 md5(pwdhash) = de_func($email, $time, md5(rand(0, 32000)), key)
此外,對於time(以10秒為例) * 32000這32萬種輸入,怎麼過濾出正確的結果。
通過用郵箱中的key參數與其他參數異或,可以得到32萬種md5(pwdhash),但是md5輸出的字元串只可能包含數字和小寫字母a到f,用這一點就能將正確結果過濾出來(實際測試過程中,不正確的輸入除了會導致結果包含其他字母,因為異或實際上產生了大量非列印字元)
至此算出了pwdhash的md5值
再看key的校驗過程
/upload/Application/Home/Controller/MembersController.class.php
獲取重置密碼鏈接中的key參數後用decrypt方法對其進行解密,結果放在$data中
然後驗證了4個條件:
data[『e』] 是否符合郵箱格式(甚至不判斷是不是剛才的郵箱!)
data[』t』] 代表的時間有沒有過期
data[『k』] 是否等於substr(md5($data[『e』].$data["t"]),8,16)
前綴_members表中是否有與該郵箱關聯的賬號
再看解密函數怎麼做的
這裡就不再po密鑰處理函數的過程了,簡單說一下,首先base64解碼key參數,然後交給passport_key函數按位異或,之前tmp2的內容
passport_key的處理過程以字元串第1位為例
(en[0]^pwd[0]). ^. pwd[0] = en[0]
(txt[0]^en[0]^pwd[1]) ^. pwd[1] = en[0] ^ txt[0]
……
……
回到decrypt函數這兒for循環的解密過程就是
en[0] ^ (en[0] ^ txt[0]) = txt[0]
en[1] ^ (en[1] ^ txt[1]) = txt[1]
……
……
最後得到的就是txt字元串
『e=$email&k=$key&t=$time』
而未知的那個0到32000的md5值在解密的異或過程中就被抵消了。
POC
測試環境需要在某cms後台開啟另外smtp,另外,為了省去激活測試賬號的步驟,我直接將email_audit列置1,對漏洞沒有影響。
首先用一個可用郵箱註冊賬號,發包申請郵箱找回密碼,記錄收發包時間範圍
payload_send_request.py
記錄的時間
用郵箱中的key參數和剛才記錄的時間開始破解
32000*11種可能1秒就出貨了
看一下算出來的pwdhash是否正確
再註冊一個測試賬號test22
請求重置密碼
然後用剛才的pwdhash構造重置密碼鏈接
點擊console中生成的鏈接
我們可以進一步驗證資料庫是否改變,隨便輸入一個新密碼888888
看看資料庫,顯然密碼發生了變化
利用方式
從之前的分析過程可以看出這個漏洞的利用方式比較暴力,正常流程
1、通過自己的測試賬號利用郵箱找回密碼的過程本地算出pwdhash
2、利用郵箱找回密碼功能提交目標郵箱賬戶
3、本地利用步驟1中的pwdhash構造重置密碼鏈接重置目標郵箱賬戶的密碼
這樣做的結果是無法恢複目標賬戶的密碼,並且目標的郵箱里留下了一封重置密碼鏈接。
能否做到完美的利用,即既不往目標郵箱發送郵件還能在重置後還原。如果在生成重置鏈接的時候沒有以session、資料庫或者文件等可存儲等形式存儲賬戶標誌(email、username、uid等)或者即使存儲在校驗時不校驗賬戶信息即可滿足第一點;第二點比較困難,需要密碼找回過程中用到原密碼,並且我們能通過無論是輸出或是其他方法去暴力驗證。
user_getpass方法:
生成密碼重置鏈接的過程中沒有以任何可存儲介質存儲賬戶標誌,但是在最後可以看到賬戶的uid被放入到了session中
校驗的user_setpass方法:
完全沒有對uid的校驗,還在最後把session中的原uid直接幹掉,令人窒息的操作。。。
所以顯然,可以在不向目標賬戶郵箱發送重置鏈接的情況下重置密碼,大概過程就是
1、獲取pwdhash
2、重置密碼頁面輸入自己的郵箱
3、利用pwdhash和目標郵箱構造重置密碼鏈接重置目標賬號的密碼
過程截圖就不貼了。
至於第二點密碼恢復,從這個過程可以看出顯然做不到。如果碰到類似的漏洞,有沒有可能恢復密碼呢?我想到了Skype一類的app在重置密碼時如果新密碼是該賬號用過的密碼,那麼它會提示你不要設置曾經用過的密碼,順著這個思路有可能給它暴力破解出來。但是這種方法也有個問題,這種方法不能在本地測試,校驗一般是在服務端,一下子發這麼多數據包大概率會被防火牆幹掉。
※Kerberos域用戶名枚舉
※使用圖像鏈接的UNC路徑注入
TAG:SecPulse安全脈搏 |