和往常一样,我今天早早到了公司, 也就提前了两个小时吧(现在你们知道我为什么有时间写文章了吧)
想着既然离上班还早,于是我逛起了推特,看看今天是哪位幸运的大佬被我膜拜,不一会,我就发现很多人都转了一条推特,这条推特是关于CVE-2020-13379这个漏洞的
看到这么多人都在转,那一定是很牛逼的漏洞,于是我点了进去…
这是一个Grafana的未授权ssrf漏洞,涉及3.0.1-7.0.1版本。下面是漏洞详情
漏洞的入口点是/avatar/:hash
接口,在Grafana api.go 文件中我们可以看到如下的路由
r.Get("/avatar/:hash", avatarCacheServer.Handler)
这个路由会获取到接口/avatar/:hash
中的hash值,并把它路由到secure.grafana.com
,代码实现大体如下:
const (
gravatarSource = "https://secure.gravatar.com/avatar/"
)
...
case err = <-thunder.GoFetch(gravatarSource+this.hash+"?"+this.reqParams, this):
代码中的gravatarSource的值就是secure.grafana.com
,this.hash
的值就是经过URL解码过后的/avatar/:hash
中的:hash
的值,因为:hash
的值可控,所以我们可以注入参数到url中。
而这个secure.gravartar.com
有一个特点,如下:
secure.gravartar.com
这个网站是国外的一个著名的提供头像等图片的站点,而它还实现了从其它主机加载图片的功能,只要用参数d=
指定你要加载的图片地址就行,例如你要加载的图片地址是http://axin.com/1.png
,你就可以这么写https://www.gravatar.com/avatar/test?d=http%3a%2f%2faxin.com%2f1.png
,注意,冒号斜杠等特殊等字符需要url编码
而这个功能是怎么实现的呢?
其实就是一个重定向,当我们请求https://www.gravatar.com/avatar/test?d=http%3a%2f%2faxin.com%2f1.png
会被重定向到
http://i0.wp.com/axin.com/1.png
我们可以用curl做个试验,如下
那你可能会疑惑了,我指定的地址不是http://axin.com/1.png
吗,怎么给我跳到了i0.wp.com
这个主机上呀
而且,这个主机怎么拿到我指定地址的图片的?
没错,你可能已经猜到了,这里又有一次重定向
当我们请求i0.wp.com/imageDomain/pathofImage
时,会被重定向到imageDoamin/pathofImage
这个地址,但是这里的imageDoamin可不是随便什么域名都可以,而是要*.bp.blogspot.com
子域下的域名
这是一个白名单的配置,而我们需要做的就是绕过这个白名单,只要绕过了这个白名单,我们就大功告成了
我又探索了一番,发现了如下绕过手法
http://i0.wp.com/axin.com%3f/1.bp.blogspot.com/
通过如上技巧就可以轻松绕过白名单限制,把请求重定向到axin.com
,至此就完成了整个ssrf,由于我在复现的过程中发现i0.wp.com
已经修复了这种绕过方式,所以我贴一张原作者的图
从图中可以看到,确实是重定向到了我们指定的域名,但是修复过后,发送上述请求不会再重定向,而是返回400
这真是一个悲伤的故事
最后的payload类似这样:
https://grafanaHost/avatar/test%3fd%3daxin.com%25253f%253b%252fbp.blogspot.com
再来梳理一下整个流程:
Grafana把字符串test%3fd%3daxin.com%25253f%253b%252fbp.blogspot.com
当做:hash
,此时,目标服务器会请求到
https://secure.gravatar.com/avatar/anything?d=axin.com%253f%3b/1.bp.blogspot.com/
然后,因为我们使用了d=
参数,请求又被重定向到了
http://i0.wp.com/google.com%3f%;/1.bp.blogspot.com/
最后,由于白名单的正则缺陷,我们重定向到我们指定的主机地址axin.com
http://axin.com?;/1.bp.blogspot.com
最后,目标服务器会请求上述地址,然后给我们返回一个Content-Type为image/jpeg的响应,相关代码如下:
...
if avatar.Expired() {
// The cache item is either expired or newly created, update it from the server
if err := avatar.Update(); err != nil {
log.Trace("avatar update error: %v", err)
avatar = this.notFound
}
}
if avatar.notFound {
avatar = this.notFound
} else if !exists {
if err := this.cache.Add(hash, avatar, gocache.DefaultExpiration); err != nil {
log.Trace("Error adding avatar to cache: %s", err)
}
}
ctx.Resp.Header().Add("Content-Type", "image/jpeg")
if !setting.EnableGzip {
ctx.Resp.Header().Add("Content-Length", strconv.Itoa(len(avatar.data.Bytes())))
}
ctx.Resp.Header().Add("Cache-Control", "private, max-age=3600")
if err := avatar.Encode(ctx.Resp); err != nil {
log.Warn("avatar encode error: %v", err)
ctx.WriteHeader(500)
}
最后,漏洞作者给出了他自己的一个payload:
https://grafanaHost/avatar/test%3fd%3dredirect.rhynorater.com%25253f%253b%252fbp.blogspot.com%252fYOURHOSTHERE
但是,由于i0.wp.com
修复了正则缺陷,这个payload好像也就失效了…
原文地址: https://rhynorater.github.io/CVE-2020-13379-Write-Up
PPT: https://docs.google.com/presentation/d/1He_zFFXCuft3LsZTXbHKoDxQHNoSveZg2c2uF1HKuaw/edit#slide=id.g8dc2d55207_8_578
原文很精彩,我只挑了最最最最基础的漏洞原理讲了讲,漏洞作者可是用这个漏洞横扫了bugbounty呢!
ok,文章写完了,我也该下班了!我们周末见~