Redis 是一個 in-memory 的開源專案,使用 key-value 的形式來儲存資料,常被用來當作資料庫或是 cache 使用,並透過 rdb 與 aof 機制來作到 persistence,rdb 用來存儲資料庫中的資料,而 aof 則是 log 檔,當 appendonly 為 yes 時,會將對 Redis 的操作 log 到 appendonly.aof 檔中。
預設的 service port 是 6379,版本 >= 3.2.7 之後偵測到指令裡面有 HOST 或是 POST 關鍵字會 abort connection,並且在版本 >= 3.2.0 預設開啟 protected mode,代表當沒有 bind 到任何 IP 位址且沒有密碼要求時,只有 IPv4、IPv6 的 loopback 或 Unix socket 可以訪問該 Redis。
Exploit
除了 memory corruption 相關的 CVE 弱點外,redis open port 還可以透過以下方式來 exploit
- SLAVEOF
- 可以對資料庫中的資料做任意的改動藉此來改變程式邏輯,但需要知道資料結構
- MIGRATE
- 可以從目標資料庫取得任意資料,但需要知道資料結構
- CONFIG SET
- 可以設定資料庫的除存路徑與名稱,可能可以寫 webshell,但需要知道路目標徑且權限為可寫
- 當 Redis 是由 root 運行且伺服器上有 SSH 服務時可以考慮將公鑰寫入
authorized_keys
中就可以直接以 root 身份連上 - 或是寫入 crontab 檔案來執行任意指令,但對平台有依賴性
- debian 系列可執行的 crontab 權限需要是
0600
但 Redis 寫入會是0644
且寫入的指令無法解析 - centos 可以成功寫入 crontab 亦可正常解析,其對 crontab 格式要求疑似較為鬆散
- debian 系列可執行的 crontab 權限需要是
Redis 目前支援兩種傳輸方式
- Plaintext
- 使用空格分開指令並用換行符號結尾
SET keyname value\n
- Custom
- 在傳輸的資料中,使用星號表示指令與參數數量,錢字號表示字串長度並以
\r\n
區隔與結尾 *3\r\n$3\r\nSET\r\n$7\r\nkeyname\r\n$5\r\nvalue\r\n
- 在傳輸的資料中,使用星號表示指令與參數數量,錢字號表示字串長度並以
Redis 中支援不同的 instance 之間互相同步資料,用的是傳統的 Master/Slave 模式,Master 與 Slave 之間的傳輸使用的就是 Redis 本身支援的協定與指令,並沒有特殊傳輸方式。
Rogue server
在找相關資料的時候發現 zeronights.ru 在 2018 時有一篇名為 redis-post-exploitation 的 talk 講述了一種架設 rogue server 來 RCE 的技巧。
Redis 要求的同步流程如下
PING
, 測試目標是否存活+PONG
回應表示連線正常
REPLCONF
, 用以傳輸 master 與 slave 之間的複製訊息+OK
PSYNC/SYNC <replid>
, slave 會開始嘗試與 master 進行 full sync 或 partial sync+CONTINUE <replid> 0
之後就可以透過 rogue server 對 slave 發送任意 Redis 指令,但這中間有一個問題,Redis 之間的同步並不會有指令執行結果,不過可以透過 Redis 中的 SCRIPT DEBUG
模式來解決。
首先使用 SLAVEOF 指令讓目標 Redis 成為自己 rogue server 的 slave,如此一來便可以對目標 Redis 下任意指令,但是兩個 Redis 同步時並不會顯示指令結果,但 Redis 在 SCRIPT DEBUG
模式時是可以執行 Redis 指令且會有結果顯示的,利用這個 feature 就可以取得 Redis 中的任意資料了。
解決取得回應問題後,為了要 RCE 還需要用到另外一個 feature,Redis 有開放 RedisModulesSDK供使用者撰寫自己的 Redis module,在PSYNC/SYNC
請求階段透過回應 FULLRESYNC 將惡意 SO 檔當成 PAYLOAD 傳給 slave,slave 就會把檔案存成 dump.rdb
(這是預設檔名,可能因設定而異),之後可以透過指令 MODLUE LOAD
來載入該 module,基本上編譯成 SO 又得以載入後就可以為所欲為了。
但在 Redis 5.0 之後,SCRIPT DEBUG
模式下無法使用 CONFIG 指令,這將導致無法透過 CONFIG GET/SET
取得或更動目標資料庫名稱,從而不知道要去哪裡載入 module。
這裡有另外一個解法,當 PAYLOAD 還沒完全送達時,Redis 會在目錄下存放一個暫存檔,檔名格式為 temp-<time>.<pid>.rdb
,這樣一來只要透過前面取得回應的方式用 TIME 與 INFO SERVER 指令,一樣可以取得 Redis 目前的相關資訊,這樣一來便可以載入該檔案。
需要注意的是可能因為網路或是傳送指令執行有時間差,載入時可能需要嘗試後面一秒的檔案名稱,另外 pid 是對應 process_id 欄位沒錯,但 SCRIPT DEBUG
模式下的 pid 值與非 SCRIPT DEBUG
模式下的會相差一個整數,可能是因為 fork 出另外一個 process 執行的關係或是跟 OS 相關,純屬猜測尚未釐清。
exploit 步驟如下
- 使用
SLAVEOF
使對方成為 rogue server 的 slave - 透過
SCRIPT DEBUG
取得 time 與 info server 中的 pid - 使用新的連線發起 FULLRESYNC,回應惡意 module 與不正確的長度使對方進入等待
- 使用
MODULE LOAD ./temp-<time>.<pid>.rdb
載入惡意 module
Conclusion
slide 後面還有提到 Redis cluster 與 Redis sentinel 方面的 exploit 方式,基本上大同小異,自己在寫 PoC 的時候遇到一些坑,像是 pid 問題或是 Redis 4 與 Redis 5 之間使用的協定不同,不過基本上都可以透過 socat 來看兩者間的傳輸資料來釐清問題,然後再讀一下 RedisModuelsSDK 怎麼寫就差不多可以弄出來了。
slide 最後有提到說有向 Redis 官方回報這些問題,但官方表示 document 寫得很清楚了,如果將 Redis service port 開放出來又沒有任何密碼驗證要求,那本身就已經是危險至極的事情了,所以這不是他們的問題。
- 可以考慮的安全設定
- 修改 redis.conf
- 開啟保護模式
protected-mode yes
- 限定本機訪問
bind 127.0.0.1
- 登入時要求密碼
requirepass XXXX
- 更改預設埠號
port XXXX
- 將危險 Redis 指令重新命名為空字串,將使得該指令變為不可用
rename-command FLUSHALL ""
- 開啟保護模式
- 使用 iptables 設定訪問白名單
- 修改 redis.conf
最後,在相關的參考資料中都有講到,可以使用 flushall
來提高寫入檔案的成功率(避免其他資料干擾寫入的檔案),但這將會對目標造成影響,不過 slide 中都沒有相關警告,而且在同步 DB 的過程中亂送東西的話一樣可能導致 redis crash 退出,或是同步之後有其他應用程式對 redis 執行查詢的動作,這一樣會導致出錯而 crash,雖然不是 100% 不會爆炸的 exploit 方式,但提供了另外一種攻擊的思維。