現今網站應用程式潛藏許多安全風險,從常見的跨站請求偽造(CSRF)到進階的記憶體漏洞,都可能導致嚴重後果。理解這些漏洞的原理、攻擊手法及防護策略,對於維護網站安全至關重要。本文除了剖析各種漏洞型別外,也探討了模糊測試等自動化安全稽核技術,並以 Rust 語言為例,示範如何應用模糊測試來發現程式碼中的潛在問題。透過整合這些知識,開發者可以更有效地防範攻擊,提升網站應用程式的安全性。

網站安全漏洞解析與防護策略

在現代網路環境中,各類別網站安全漏洞對企業和個人使用者都構成嚴重威脅。本文將探討多種常見的安全漏洞及其防護措施。

6.11 跨站請求偽造(CSRF)

漏洞原理

跨站請求偽造(CSRF)是一種攻擊方式,允許攻擊者以其他使用者的名義執行未經授權的操作。這種攻擊利用了網站對使用者瀏覽器的信任。

攻擊示例

  1. 表單提交型CSRF:

    <form action="https://kerkour.com/admin" method="POST">
        <input type="hidden" name="username" value="skerkour" />
    </form>
    <script>
        document.forms[0].submit();
    </script>
    

    當管理員存取惡意網站時,瀏覽器會自動提交表單,將攻擊者設定為管理員。

  2. URL型CSRF: GET https://kerkour.com/admin?set_role=admin&user=skerkour 誘騙管理員存取該URL,即可將攻擊者提升為管理員。

風險評估

CSRF漏洞可能導致攻擊者獲得管理員許可權,從而完全控制應用系統。

防護措施

  1. 使用CSRF Token驗證
  2. 採用SameSite Cookie屬性
  3. 避免使用GET請求執行敏感操作

6.12 開放重定向漏洞

漏洞原理

開放重定向漏洞允許攻擊者將使用者重定向到惡意網站,利用合法網站的評價進行網路釣魚攻擊。

漏洞示例

function do_something(req, res) {
    let redirect_url = req.body.redirect;
    res.redirect(redirect_url);
}

攻擊者可建構URL:https://kerkour.com/login?redirect=malicious.com

風險評估

開放重定向漏洞主要用於網路釣魚攻擊,欺騙使用者存取惡意網站。

防護措施

  1. 對重定向目標進行白名單驗證
  2. 避免使用使用者可控的重定向引數

6.13 子網域名稱接管

漏洞原理

當DNS記錄指向的雲端資源不再受公司控制時,攻擊者可接管該子網域名稱。

風險評估

  1. 可能導致Cookie洩露
  2. 用於建立網路釣魚頁面
  3. 傳播虛假資訊

防護措施

  1. 定期審查DNS記錄
  2. 刪除不再使用的DNS記錄

6.14 任意檔案讀取

漏洞原理

攻擊者可利用該漏洞讀取系統中本應保密的檔案。

漏洞示例

function get_asset(req, res) {
    let asset_id = req.query.id;
    let asset_content = file.read('/assets/' + asset_id);
    res(asset_content);
}

攻擊者可建構請求:https://example.com/assets?id=../etc/passwd

風險評估

可能導致敏感資訊洩露,如資料函式庫憑證、加密金鑰等。

防護措施

  1. 對輸入進行嚴格驗證和過濾
  2. 限制檔案讀取許可權

6.15 拒絕服務攻擊(DoS)

攻擊原理

透過使服務不可用,影響正常使用者的使用。

風險評估

可能導致業務中斷,造成經濟損失。

防護措施

  1. 實施資源限制和隔離
  2. 使用負載平衡和流量清洗技術

分散式阻斷服務攻擊(DDoS)與其他安全漏洞

分散式阻斷服務攻擊(DDoS)

分散式阻斷服務攻擊(DDoS)是一種惡意的網路攻擊,旨在使目標伺服器或網路資源無法正常運作,從而拒絕合法使用者的服務請求。攻擊者通常利用多台受控電腦(殭屍網路)同時傳送大量請求,耗盡目標系統的資源。

案例研究

  • 對 Twitter 網站(twitter.com 和 mobile.twitter.com)進行的拒絕服務攻擊
  • 利用 Mermaid 圖表漏洞對議題頁面進行的 DoS 攻擊

任意檔案寫入漏洞

任意檔案寫入漏洞允許攻擊者覆寫本應保持不變的檔案內容,從而可能導致系統被入侵或敏感資訊洩露。

程式碼範例

function upload_file(req, res) {
    let file = req.body.file;
    let file_name = req.body.file_name;
    fs.write('/uploads/' + file_name, file);
    res(ok);
}

內容解密:

此段程式碼存在任意檔案寫入漏洞,因為它允許使用者上傳檔案並指設定檔案名稱。攻擊者可以透過上傳一個名為 ../root/.ssh/authorized_keys 的檔案來覆寫 root 使用者的 SSH 授權金鑰,從而獲得對系統的控制權。

記憶體漏洞

記憶體漏洞主要影響低階程式語言,如 C 和 C++,因為這些語言需要手動管理記憶體。然而,即使是依賴大量 C 或 C++ 套件的動態語言,如 Ruby 和 Python,也可能間接受到記憶體漏洞的影響。

為何 Rust 不易受記憶體漏洞影響?

Rust 透過內建的記憶體安全機制,如緩衝區邊界檢查,避免了許多記憶體相關的漏洞。

緩衝區溢位

緩衝區溢位發生在程式嘗試向緩衝區寫入超出其容量的資料時。

程式碼範例

function copy_string(input []char) []char {
    // 如果 len(input) > 32,將導致緩衝區溢位
    let copy = [32]char;
    for (i, c) in input {
        copy[i] = c;
    }
    return copy;
}

內容解密:

此段程式碼存在緩衝區溢位漏洞,因為它沒有檢查輸入資料的長度,直接將其複製到一個固定大小的緩衝區中。Rust 可以透過其內建的緩衝區邊界檢查機制避免這種問題。

使用已釋放記憶體(Use After Free)

使用已釋放記憶體漏洞發生在程式嘗試使用已經被釋放的記憶體時。

程式碼範例

function allocate_foobar() []char {
    let foobar = malloc([]char, 1000);
}

function use_foobar(foobar []char) {
    // 做一些事情
    free(foobar);
}

function also_use_foobar(foobar []char) {
    // 做一些事情
}

function main() {
    let foobar = allocate_foobar();
    use_foobar(foobar);
    // 做其他事情
    // ...
    // !! 在 use_foobar 中釋放 foobar 後再次使用
    also_use_foobar(foobar);
}

內容解密:

此段程式碼存在使用已釋放記憶體漏洞,因為它在 use_foobar 函式中釋放了 foobar 所指向的記憶體後,又在 also_use_foobar 函式中嘗試使用它。這可能導致資料損壞或遠端程式碼執行。

重複釋放記憶體(Double Free)

重複釋放記憶體漏洞發生在程式嘗試多次釋放同一塊記憶體時。

程式碼範例

function allocate_foobar() []char {
    let foobar = malloc([]char, 1000);
}

function use_foobar(foobar []char) {
    // 做一些事情
    free(foobar);
}

function main() {
    let foobar = allocate_foobar();
    use_foobar(foobar);
    // !! foobar 已經在 use_foobar 中被釋放
    free(foobar);
}

內容解密:

此段程式碼存在重複釋放記憶體漏洞,因為它在 use_foobar 函式中釋放了 foobar 所指向的記憶體後,又在 main 函式中再次釋放它。這可能導致未定義行為,包括程式當機或資料損壞。

遠端程式碼執行(RCE)

遠端程式碼執行漏洞允許攻擊者在目標機器上遠端執行任意程式碼。

為何 RCE 如此危險?

RCE 允許攻擊者完全控制受影響的機器,從而可能導致資料洩露、網站篡改等嚴重後果。

案例研究

  • 利用 ExifTool 移除元資料時的 RCE 漏洞
  • 透過不安全的內聯 Kramdown 選項渲染特定 Wiki 頁面時的 RCE 漏洞

整數溢位(Integer Overflow)

整數溢位發生在算術運算嘗試建立一個超出變數表示範圍的數值時。

程式碼範例

function withdraw(user_id, amount int32) {
    let balance: int32 = find_balance(user);
    if (balance - amount > 0) {
        return ok();
    } else {
        return error();
    }
}

內容解密:

此段程式碼存在整數溢位漏洞,因為 balanceamount 是 32 位整數,當 amount 為一個很大的負數時,balance - amount 可能會溢位,導致錯誤的結果。這可能被利用來繞過餘額檢查,進行非法操作。

6.23 整數溢位漏洞

整數溢位漏洞發生在程式設計中,當一個整數變數的值超出其資料型別所能表示的範圍時,可能導致意外的行為或安全漏洞。

程式碼範例分析

// n 由攻擊者控制
function do_something(n uint32) {
    let buffer = malloc(sizeof(*char) * n);
    for (i = 0; i < n; i++)
        buffer[i] = do_something();
}

內容解密:

  1. n 由攻擊者控制:這意味著攻擊者可以輸入任意值給 n,可能導致安全問題。
  2. malloc(sizeof(*char) * n):這行程式碼根據 n 的值分配記憶體。如果 n 是一個非常大的數字,並且 sizeof(*char) * n 超出了 uint32 的最大值,就會發生整數溢位。
  3. 整數溢位:當 n 的值足夠大(如 1073741824),乘以指標大小(在 32 位系統上為 4 位元組)後發生溢位,結果變為 0。於是 malloc 分配了一個大小為 0 的緩衝區。
  4. for 迴圈導致緩衝區溢位:接著的 for 迴圈會向這個大小為 0 的緩衝區寫入資料,導致緩衝區溢位,可能引發程式當機或被利用進行攻擊。

Rust 中的整數溢位行為

在 Rust 中,整數溢位的行為取決於編譯模式:

  • 除錯模式(debug mode):Rust 會在發生整數溢位時 panic。
  • 發布模式(release mode):Rust 會執行「二補數包裝(two’s complement wrapping)」,即變數會持有一個無效的值,而程式不會當機。
let x: u8 = 255;
// x + 1 = 0 (而不是 256)
// x + 2 = 1 (而不是 257)
// x + 3 = 2 (而不是 258)

內容解密:

  1. u8 資料型別:這是一個無符號的 8 位元整數,最大值為 255。
  2. 整數溢位包裝:當 x 為 255 時,再加 1 結果變為 0,這是因為超出了 u8 能表示的最大值,發生了包裝。

為什麼整數溢位是壞的

整數溢位漏洞曾經在智慧合約中造成大量資金被盜,因為錯誤的合約程式碼允許攻擊者利用整數溢位來控制程式的執行流程或觸發其他漏洞,如緩衝區溢位。

真實案例研究

  • /lib/urlapi.c 中的整數溢位
  • libssh2 中的整數溢位和越界讀取(CVE-2019-13115)
  • 另一個 libssh2 中的整數溢位(CVE-2019-17498)

6.24 邏輯錯誤

邏輯錯誤是指任何允許攻擊者操控應用程式業務邏輯的錯誤。例如,攻擊者可能能夠以零價格訂購多個商品,或取得通常只有管理員才能存取的敏感資料。

為什麼邏輯錯誤難以避免

邏輯錯誤往往是由於開發者對業務邏輯的理解或實作不當引起的。即使用像 Rust 這樣注重安全的語言,也無法完全避免邏輯錯誤,因為編譯器無法捕捉到這些錯誤。撰寫測試是發現邏輯錯誤的重要手段。

真實案例研究

  • 使用隨機第三方錢包 ID 取得 Zomato 金幣
  • 透過使用負數量操縱 OLO 總價

6.25 競態條件

競態條件發生在程式依賴多個平行操作,並且程式的正確輸出依賴於這些操作的順序或時序。如果由於缺乏同步機制等原因導致操作的順序或時序被改變,就可能發生錯誤。

為什麼競態條件是壞的

大多數可利用的競態條件發生在驗證操作與更新(或建立、刪除)操作平行執行的情況下。

真實案例研究

  • 轉賬資料信用的競態條件導致組織獲得額外免費資料信用
  • 競態條件允許多次兌換禮品卡,導致免費「金錢」
  • 繞過合作夥伴電子郵件確認以接管任何商店(給定員工電子郵件)

6.26 其他資源

有許多 GitHub 倉函式庫提供了豐富的範例和 payload,有助於發現這些漏洞,例如 swisskyrepo/PayloadsAllTheThings 和 EdOverflow/bugbounty-cheatsheet。

6.27 Bug Hunting

現在我們瞭解了漏洞是什麼樣的,讓我們看看如何在現實世界中找到它們。尋找漏洞時,有一些常見的模式值得關注。

常見漏洞模式

  1. 富文字編輯器:WYSIWYG 或 Markdown 編輯器經常是 XSS 的易受攻擊目標。
  2. 檔案上傳:檔案上傳表單可能導致任意檔案寫入或 XSS(使用 SVG 檔案)。
  3. 輸入欄位:未經清理的輸入欄位可能導致注入攻擊。瞭解輸入如何被處理和輸出至關重要。
  4. HTTP 標頭:HTTP 標頭有時被應用程式使用並回顯在回應中,是被忽視的攻擊向量。
  5. 危險/已棄用的演算法:仍在使用的危險演算法(如 md5)可能導致安全問題。
  6. 具有危險引數的方法:密碼學函式或記憶體不安全的語言中的資料操作函式(如 C 語言中的 memsetstrcpy)可能導致嚴重錯誤。
  7. 認證系統:認證和授權系統複雜且分散,容易出現漏洞。瞭解需要提升許可權的操作,並嘗試在沒有這些許可權的情況下執行它們。
  8. 多人遊戲:遊戲開發者往往不是安全工程師,可能忽略安全性。一些遊戲的網路堆積疊使用記憶體不安全的語言,如 C 或 C++,容易出現記憶體相關漏洞。
  9. 複雜格式解析:解析 YAML 等複雜格式很困難,容易出現 bug,有時這些 bug 是實際的安全漏洞。

圖表說明

  graph LR;
    A[輸入欄位] -->|未經清理|> B[注入攻擊];
    C[檔案上傳] -->|惡意檔案|> D[XSS];
    E[HTTP標頭] -->|未驗證|> F[攻擊向量];

圖表翻譯: 此圖表展示了常見的漏洞來源,包括未經清理的輸入欄位、惡意檔案上傳和未驗證的 HTTP 標頭。這些都是攻擊者常用的攻擊途徑。

網路安全中的自動化稽核與模糊測試技術

在網路安全領域,自動化稽核扮演著越來越重要的角色。隨著軟體專案的日益複雜,單純依靠人工檢測已經無法滿足高效、準確的安全需求。因此,各種自動化工具和技術應運而生,其中模糊測試(Fuzzing)技術尤其受到重視。

模糊測試技術原理

模糊測試是一種自動化測試方法,主要用於發現軟體專案中的錯誤和漏洞。與傳統的手工編寫測試案例不同,模糊測試透過向目標軟體輸入大量隨機資料來檢測其穩定性和安全性。這種方法尤其適用於大型程式碼函式庫或閉源程式的測試。

模糊測試的優勢

  1. 高度自動化:模糊測試可以全自動執行,大大減少了人工測試的工作量。
  2. 廣泛的覆寫範圍:透過輸入大量隨機資料,模糊測試能夠覆寫更多的程式碼路徑。
  3. 適用於閉源程式:無需原始碼即可進行測試。

開始使用模糊測試

安裝必要工具

對於Rust專案,推薦使用cargo-fuzz工具進行模糊測試。首先,需要安裝該工具:

$ cargo install -f cargo-fuzz
$ rustup install nightly

注意:cargo-fuzz依賴於libFuzzer,後者需要LLVM sanitizer支援,因此目前僅支援x86-64 Linux和x86-64 macOS。此外,還需要nightly版本的Rust工具鏈以及支援C++11的C++編譯器。

初始化模糊測試

  1. 準備待測程式碼

    使用一個有漏洞的memcpy函式作為示例:

    pub fn vulnerable_memcopy(dest: &mut [u8], src: &[u8], n: usize) {
        let mut i = 0;
        while i < n {
            dest[i] = src[i];
            i += 1;
        }
    }
    
  2. 初始化cargo-fuzz

    $ cargo fuzz init
    $ cargo fuzz list
    fuzz_target_1
    

    這將建立一個fuzz資料夾,其中包含一個Cargo.toml檔案。

  3. 組態Cargo.toml

    fuzz/Cargo.toml中新增arbitrary依賴:

    [dependencies]
    libfuzzer-sys = "0.4"
    arbitrary = { version = "1", features = ["derive"] }
    [dependencies.fuzzing]
    path = ".."
    
  4. 實作模糊測試目標

    編寫第一個模糊測試目標:

    #![no_main]
    use libfuzzer_sys::fuzz_target;
    
    #[derive(Clone, Debug, arbitrary::Arbitrary)]
    struct MemcopyInput {
        dest: Vec<u8>,
        src: Vec<u8>,
        n: usize,
    }
    
    fuzz_target!(|input: MemcopyInput| {
        // 執行待測函式
        vulnerable_memcopy(&mut input.dest, &input.src, input.n);
    });
    

內容解密:

此段程式碼定義了一個名為MemcopyInput的結構體,用於表示模糊測試的輸入資料。該結構體派生了Arbitrary特性,使其能夠被隨機生成。接著,使用fuzz_target!宏定義了一個模糊測試目標,該目標會呼叫vulnerable_memcopy函式,並傳入隨機生成的輸入資料。

模糊測試的實際應用

透過上述步驟,我們可以開始對目標函式進行模糊測試。模糊測試能夠自動生成大量隨機輸入,並檢查程式在這些輸入下的行為,從而發現潛在的安全漏洞。

執行模糊測試

使用以下命令執行模糊測試:

$ cargo fuzz run fuzz_target_1

內容解密:

此命令啟動了對fuzz_target_1目標的模糊測試。測試過程中,cargo-fuzz會不斷生成新的輸入資料並執行目標函式,同時監控是否有錯誤或當機發生。

模糊測試流程圖示
  graph LR;
    A[開始模糊測試] --> B[初始化cargo-fuzz];
    B --> C[組態Cargo.toml];
    C --> D[實作模糊測試目標];
    D --> E[執行模糊測試];
    E --> F[分析測試結果];

圖表翻譯: 此圖示呈現了模糊測試的主要流程。首先,開發者需要初始化cargo-fuzz並組態相關設定。接著,實作具體的模糊測試目標。最後,執行模糊測試並分析結果,以發現潛在的安全問題。

隨著軟體安全性的日益重要,模糊測試技術將繼續發展和完善。未來,我們可以期待更多高效的模糊測試工具和方法的出現,以幫助開發者更好地保護軟體安全。

參考資料

透過結合上述技術和方法,我們能夠更全面地保障軟體的安全性,為使用者提供更加可靠的產品。