在網路安全領域,攻擊面偵察至關重要。本文將探討如何使用 Rust 語言開發多執行緒網路掃描器,以提升偵察效率。主動式偵察技術藉由直接與目標互動收集資訊,例如子網域名稱列舉和埠掃描。透過 crt.sh API,我們可以快速取得目標的子網域名稱資訊,並利用多執行緒技術同時掃描多個埠,縮短偵察時間。Rust 的強型別系統和所有權機制,則確保了多執行緒環境下的程式碼安全性和穩定性,避免資料競爭問題。
多執行緒攻擊面偵測
「要了解你的敵人,你必須成為你的敵人」,孫子曰。
如同我們所見,每一次攻擊的第一步都是偵察。這個階段的目標是盡可能收集有關目標的資訊,以找出即將到來的攻擊的入口點。
在本章中,我們將瞭解偵察的基本知識,如何在Rust中實作我們自己的掃描器,以及如何透過利用多執行緒來加速它。
進行偵察有兩種方式:被動式和主動式。
被動式偵察
被動式偵察是在不直接與目標互動的情況下收集有關目標的資訊,例如在不同的社交網路和搜尋引擎上搜尋目標。
使用公開可用的來源稱為OSINT(Open Source Intelligence,開源智力)。
被動式偵察收集的資料型別
通常,透過被動式偵察收集到的資訊包括公司員工的姓名、電子郵件地址、電話號碼,以及原始碼儲存函式庫、洩漏的權杖等。藉助像Shodan這樣的搜尋引擎,我們還可以尋找對外開放的服務和機器。
由於被動式偵察是第5章的主題,本章將重點關注主動式偵察。
主動式偵察
主動式偵察是透過直接與目標互動來收集有關目標的資訊。
主動式偵察更為嘈雜,可能會被防火牆和蜜罐檢測到,因此您必須小心保持隱匿,例如透過在較長時間內分散掃描。
蜜罐是一個外部端點,按照預期永遠不會被某家公司的「普通」人員使用,因此唯一存取這個端點的人是攻擊者。它可以是郵件伺服器、HTTP伺服器,甚至是嵌入遠端內容的檔案。
一旦蜜罐被掃描或存取,它將回報給佈署它的安全團隊。
資產發現
傳統上,資產僅由技術元素定義:IP位址、伺服器、網域名稱、網路等。
如今,範圍更廣,包括社交網路帳戶、公開的原始碼儲存函式庫、物聯網裝置等。現在,一切都連線到網際網路。從攻擊者的角度來看,這非常有趣。
列出和對映目標的所有資產的目的是找到即將到來的攻擊的入口點和漏洞。
子網域名稱列舉
對於公共資產發現來說,產生最佳結果且最省力的方法是子網域名稱列舉。
事實上,隨著雲端服務的興起,越來越多的公司不再需要VPN來存取其私有服務。這些服務透過HTTPS公開可用。
子網域名稱最容易獲得的來源是憑證透明度日誌。當憑證授權單位(CA)發出網頁憑證(例如,用於HTTPS流量)時,憑證會被儲存在公開、透明的日誌中。
這些日誌的合法用途是檢測流氓憑證授權單位,它們可能會將憑證交付給錯誤的實體(想像一下,*.google.com的憑證被交付給惡意的駭客團隊,這意味著他們將能夠在未被檢測到的情況下對所有Google網域名稱進行中間人攻擊)。
另一方面,這種透明度使我們能夠自動化我們的工作的一大部分。
例如,要搜尋針對kerkour.com及其子網域名稱發出的所有憑證,請存取https://crt.sh並搜尋%.kerkour.com(%是萬用字元): https://crt.sh/?q=%25.kerkour.com。
這種技術的一個限制是無法找到非HTTP(S)服務(例如電子郵件或VPN伺服器),以及可能混淆實際使用的子網域名稱的萬用字元子網域名稱(例如*.kerkour.com)。
子網域名稱列舉能發現什麼
以下是透過爬取子網域名稱可以發現的一些內容的非詳盡清單:
- 原始碼儲存函式庫
- 容易被接管的遺忘子網域名稱
- 管理面板
- 分享檔案
- 儲存桶
- 電子郵件/聊天伺服器
用Rust編寫我們的第一個掃描器
用於對映攻擊面的軟體稱為掃描器。埠掃描器、漏洞掃描器、子網域名稱掃描器、SQL注入掃描器等。它們自動化了偵察這一冗長乏味的任務,並防止了人類錯誤(如遺忘子網域名稱或伺服器)。
但是,您必須牢記,掃描器並非萬能藥:它們可能非常嘈雜,從而可能暴露您的意圖,被反垃圾郵件系統阻止,或報告不完整的資料。
我們將從一個簡單的掃描器開始,其目的是找到目標的子網域名稱,然後掃描每個子網域名稱的最常見埠。然後,我們將逐步新增越來越多的功能,以自動化的方式找到更多有趣的東西。
隨著我們的程式變得越來越複雜,我們首先需要加深對Rust中錯誤處理的理解。
錯誤處理
無論是對於函式庫還是應用程式,Rust中的錯誤都是強型別的,大多數時候以列舉的形式表示,其中一個變體對應於我們的函式庫或程式可能遇到的每一種錯誤。
對於函式庫,目前的最佳實踐是使用thiserror crate。
對於程式,anyhow crate是推薦的。它將美化由main函式傳回的錯誤。
我們將在掃描器中使用這兩者來瞭解它們如何協同工作。
讓我們定義程式的所有錯誤情況。這裡很簡單,因為唯一的致命錯誤是命令列引數的使用不當。
// ch_02/tricoder/src/error.rs
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum Error {
#[error("Usage: tricoder <kerkour.com>")]
CliUsage,
}
// ch_02/tricoder/src/main.rs
fn main() -> Result<(), anyhow::Error> {
// ...
}
內容解密:
上述程式碼定義了一個自定義錯誤型別Error
,該型別使用thiserror
crate來派生Error
特性。Error
列舉只有一個變體CliUsage
,用於表示命令列引數使用不當的錯誤。在main.rs
中,main
函式傳回一個Result
型別,該型別使用了anyhow::Error
來處理錯誤。這種方式可以提供更好的錯誤訊息和處理機制,使得錯誤處理更加靈活和強大。透過這種方式,我們可以統一處理來自不同部分的錯誤,無論是來自標準函式庫還是第三方crate。
多執行緒實作
為了提高掃描器的效率,我們可以利用Rust的多執行緒功能。Rust提供了強大的平行處理能力,可以讓我們同時掃描多個子網域名稱或埠,從而大大加快掃描速度。
使用多執行緒的好處
- 提高掃描效率:透過平行處理,可以同時對多個目標進行掃描,大大縮短了總體掃描時間。
- 更好的資源利用:現代電腦通常具備多核心處理器,多執行緒可以充分利用這些資源,提高程式的整體效能。
如何實作多執行緒
在Rust中,可以使用std::thread
模組來建立和管理執行緒。對於更進階的使用場景,可以考慮使用像rayon
這樣的crate,它提供了更高層次的平行處理API,使得平行化任務變得更加簡單和直觀。
use std::thread;
fn main() {
let handles: Vec<_> = (0..10).map(|_| {
thread::spawn(|| {
// 在這裡執行掃描任務
println!("正在執行掃描任務");
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
}
內容解密:
這段程式碼展示瞭如何使用Rust的標準函式庫來建立10個執行緒,並在每個執行緒中執行掃描任務。thread::spawn
函式用於建立新的執行緒,並傳回一個JoinHandle
,可以用來等待執行緒完成。透過收集這些JoinHandle
到一個向量中,我們可以在之後遍歷這個向量,並呼叫join
方法等待所有執行緒完成。這種方式使得主執行緒能夠等待所有掃描任務完成,從而確保程式在所有任務完成之前不會離開。
結語
在本章中,我們介紹了偵察的基本概念,包括被動式和主動式偵察,並探討瞭如何使用Rust編寫一個簡單的子網域名稱掃描器。同時,我們也討論了錯誤處理和多執行緒實作的重要性,以及如何在Rust中實作這些功能。透過結合這些技術,我們可以建立一個高效且強大的掃描器,用於發現目標的攻擊面。在下一章中,我們將進一步擴充套件掃描器的功能,包括漏洞識別和更進階的偵察技術。
2.6 子網域列舉
我們將使用crt.sh提供的API,透過呼叫以下端點進行查詢:https://crt.sh/?q=%25.[domain.com]&output=json。
程式碼實作
// ch_02/tricoder/src/subdomains.rs
pub fn enumerate(http_client: &Client, target: &str) -> Result<Vec<Subdomain>, Error> {
let entries: Vec<CrtShEntry> = http_client
.get(&format!("https://crt.sh/?q=%25.{}&output=json", target))
.send()?
.json()?;
// 清理和去重結果
let mut subdomains: HashSet<String> = entries
.into_iter()
.map(|entry| {
entry
.name_value
.split("\n")
.map(|subdomain| subdomain.trim().to_string())
.collect::<Vec<String>>()
})
.flatten()
.filter(|subdomain: &String| subdomain != target)
.filter(|subdomain: &String| !subdomain.contains("*"))
.collect();
subdomains.insert(target.to_string());
let subdomains: Vec<Subdomain> = subdomains
.into_iter()
.map(|domain| Subdomain {
domain,
open_ports: Vec::new(),
})
.filter(resolves)
.collect();
Ok(subdomains)
}
內容解密:
- 錯誤處理:程式碼中使用了
?
運算子,當被呼叫的函式傳回錯誤時,會立即中止當前函式並傳回錯誤。 - 資料清理:從API取得的資料經過多層處理,包括分割、過濾和去重,最終得到一個
Subdomain
的向量。 - 過濾邏輯:過濾掉了與目標網域名稱相同的子網域,以及包含
*
字元的子網域。 resolves
函式:用於過濾能夠成功解析的子網域。
2.7 掃描埠
子網域和IP地址列舉只是資產發現的一部分。下一步是埠掃描:一旦發現哪些伺服器是公開可用的,就需要找出這些伺服器上有哪些服務是公開可用的。
TCP 連線掃描
最簡單的技術是嘗試開啟一個TCP socket。如果伺服器接受連線,則表示該埠是開放的。
程式碼實作
// ch_02/tricoder/src/ports.rs
use crate::{
common_ports::MOST_COMMON_PORTS_100,
model::{Port, Subdomain},
};
use std::net::{SocketAddr, ToSocketAddrs};
use std::{net::TcpStream, time::Duration};
use rayon::prelude::*;
pub fn scan_ports(mut subdomain: Subdomain) -> Subdomain {
let socket_addresses: Vec<SocketAddr> = format!("{}:1024", subdomain.domain)
.to_socket_addrs()
.expect("port scanner: Creating socket address")
.collect();
if socket_addresses.len() == 0 {
return subdomain;
}
subdomain.open_ports = MOST_COMMON_PORTS_100
.into_iter()
.map(|port| scan_port(socket_addresses[0], *port))
.filter(|port| port.is_open) // 過濾關閉的埠
.collect();
subdomain
}
fn scan_port(mut socket_address: SocketAddr, port: u16) -> Port {
let timeout = Duration::from_secs(3);
socket_address.set_port(port);
let is_open = if let Ok(_) = TcpStream::connect_timeout(&socket_address, timeout) {
true
} else {
false
};
Port {
port: port,
is_open,
}
}
內容解密:
- 埠掃描邏輯:嘗試與指定埠建立TCP連線,如果成功則認為埠是開放的。
- 超時處理:設定了3秒的超時時間,避免掃描器被阻塞。
- 平行處理:雖然這裡沒有直接展示,但通常會使用多執行緒技術來平行掃描多個埠,以提高效率。
2.8 多執行緒處理
單執行緒順序掃描埠非常慢。多執行緒可以顯著提高掃描效率。
多執行緒的優勢
- 現代CPU難以透過提高單核效能來提升整體效能,而是透過增加核心數量。
- 多執行緒允許將工作負載分配到多個核心上。
圖表說明
graph LR; A[單執行緒] -->|順序執行|> B[慢]; C[多執行緒] -->|平行執行|> D[快];
圖表翻譯: 此圖表比較了單執行緒與多執行緒的執行方式。單執行緒是順序執行的,因此速度較慢;多執行緒可以平行執行任務,因此速度更快。
2.9 Rust 中的無畏併發
Rust 的所有權系統保證了程式的資料競爭安全性。
程式碼範例
// ch_02/snippets/thread_error/src/main.rs
use std::thread;
fn main() {
let mut my_vec: Vec<i64> = Vec::new();
thread::spawn(|| {
add_to_vec(&mut my_vec);
});
my_vec.push(34)
}
fn add_to_vec(vec: &mut Vec<i64>) {
vec.push(42);
}
內容解密:
- 編譯錯誤:嘗試在多個執行緒中修改同一個向量時,Rust 編譯器會報錯,因為這可能導致資料競爭。
- 所有權問題:Rust 強制執行嚴格的所有權規則,防止了潛在的併發問題。
透過使用 Rust 的多執行緒功能,可以有效地提高階口掃描的效率,同時保證程式的安全性。接下來的章節將進一步探討如何在實際應用中使用這些技術。