【Blackhat】SSRF的新纪元:在编程语言中利用URL解析器

http://p7.qhimg.com/t01ac8cba31e1c50873.png

作者:Orange Tsai   译者:math1as

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


并非完全翻译,掺杂了一点自己的私货和见解。


什么是SSRF

[1] 服务器端请求伪造

[2] 穿透防火墙,直达内网

[3] 让如下的内网服务陷入危险当中

Structs2

Redis

Elastic


SSRF中的协议'走私'

[1] 让SSRF的利用更加有效

本质上说,是利用原本的协议夹带信息,攻击到目标的应用

[2] 用来'走私'的协议必须是适合的,比如

基于HTTP的各类协议=>Elastic,CouchDB,Mongodb,Docker

基于Text的各类协议=>FTP,SMTP,Redis,Memcached


一个有趣的例子

像这样的一个协议

http://p1.qhimg.com/t01f7b5019a1a025a3e.png

我们来分析一下,各个不同的python库,分别请求到的是哪个域名呢?

http://p6.qhimg.com/t0120d64a2ba28c59fc.png

可以看到,Python真是个矛盾的语言呢。


另一个有趣的例子

[1] HTTP协议中的CRLF(换行符)注入

[2] 使用HTTP协议'走私'信息来攻击SMTP协议

我们尝试构造CRLF注入,来进行如下的攻击

http://p9.qhimg.com/t01587ef0645b65a71c.png

STMP '讨厌' HTTP协议

这似乎是不可利用的,可是,真的如此么?

我们在传统的SSRF利用中都使用gopher协议来进行相关攻击

可是事实上,如果真实的利用场景中不支持gopher协议呢?

利用HTTPS协议:SSL握手中,什么信息不会被加密?

[1] HTTPS协议中的CRLF(换行符)注入

[2] 化腐朽为神奇 – 利用TLS SNI(Server Name Indication),它是用来改善SSL和TLS的一项特性

允许客户端在服务器端向其发送证书之前请求服务器的域名。

https://tools.ietf.org/html/rfc4366RFC文档 

简单的说,原本的访问,是将域名解析后,向目标ip直接发送client hello,不包含域名

而现在包含了域名,给我们的CRLF攻击提供了利用空间

我们尝试构造CRLF注入,来进行如下的攻击

http://p6.qhimg.com/t017f424499e156a819.png

监听25端口

http://p1.qhimg.com/t01938bd40d5af7e9f3.png

分析发现,127.0.0.1被作为域名信息附加在了client hello之后

http://p1.qhimg.com/t019a680975068ad7de.png

由此我们成功的向STMP'走私'了信息,实施了一次攻击

URL解析中的问题

[1] 所有的问题,几乎都是由URL解析器和请求函数的不一致造成的。

[2] 为什么验证一个URL的合法性是很困难的?

1.在 RFC2396/RFC3986 中进行了说明,但是也只是停留在说明书的层面。

2.WHATWG(网页超文本应用技术工作小组)定义了一个基于RFC的具体实现

但是不同的编程语言仍然使用他们自己的实现

RFC 3986中定义的URL组成部分

大致用这张图片来说明

http://p4.qhimg.com/t019185b8b7de66f3f0.png

其中的协议部分,在真实场景中一般都为http或https

而查询字符串和fragment,也就是#号后的部分,我们实际上是不关心的,因为这和我们的利用无关

所以,我们着重看的也就是authority和path部分

那么,在这几个部分中,能不能进行CRLF注入?

各个语言以及他们对应的库的情况如下图所示

http://p6.qhimg.com/t01b362af6787db29a7.png

可以看到支持CRLF注入的部分还是很多的,但是除了在实际的请求中能利用CRLF注入外

还要能通过URL解析器的检查,而这个图也列出来了对应的情况。

关于URL解析器

[1] 让我们思考如下的php代码

http://p8.qhimg.com/t0117aa13d0f2feea5a.png

在这段代码中,我们最后使用readfile函数来实施我们的SSRF攻击

但是,我们构造出的URL需要经过parse_url的相应检查

误用URL解析器的后果

当我们对上述的php脚本传入这样的一个URL

http://p6.qhimg.com/t017002738b92f73a5f.png

对于我们的请求函数readfile来说,它所请求的端口是11211

而相反的,对于parse_url来说,它则认为这个url的端口号是80,符合规定

http://p5.qhimg.com/t0165c1f2a38886bf45.png

这就产生了一个差异化的问题,从而造成了SSRF的成功利用

让我们来看看,在RFC3986中,相关的定义

http://p2.qhimg.com/t0138a1a99e37886cb3.png

那么,按照这个标准,当我们传入如下URL的时候,会发生什么呢

http://p9.qhimg.com/t01fa76a99ba74ec59f.png

对比我们的两个函数

http://p3.qhimg.com/t019f5fdf57e440d2f0.png

可以看到,parse_url最终得到的部分实际上是google.com

而readfile则忠实的执行了RFC的定义,将链接指向了evil.com

进行一下简单的分析

[1] 这样的问题同样影响了如下的编程语言

cURL,Python

[2] RFC3962的进一步分析

在3.2小节中有如下的定义:authority(基础认证)部分应该由//作为开始而由接下来的一个/号,或者问号

以及 #号作为一个结束,当然,如果都没有,这个部分将延续到URL的结尾。

cURL的利用

参照我们刚才所得到的结论

http://p1.qhimg.com/t01f41a43808713e6bd.png

对这样的一个URL进行分析和测试

http://p5.qhimg.com/t01c86a319174afd02f.png

可以发现,在cURL作为请求的实施者时,它最终将evil.com:80作为了目标

而其他的几种URL解析器则得到了不一样的结果,产生了不一致。

当他们被一起使用时,可以被利用的有如下的几种组合

http://p9.qhimg.com/t0174ddba94fb566ac4.png

于是我向cURL团队报告了这个问题,很快的我得到了一个补丁

但是这个补丁又可以被添加一个空格的方式绕过

http://p2.qhimg.com/t01d3ae788b0c783a51.png

但是,当我再次向cURL报告这个情况的时候,他们认为,cURL并不能100%的验证URL的合法性

它本来就是要让你来传给他正确的URL参数的

并且他们表示,这个漏洞不会修复,但是上一个补丁仍然在7.54.0版本中被使用了

NodeJS的Unicode解析问题

让我们来看如下的一段nodeJS代码

http://p6.qhimg.com/t01785b10fa37d0620f.png

可以看到,阻止了我们使用..来读取上层目录的内容

当对其传入如下的URL时,会发生什么呢

http://p1.qhimg.com/t01628d8c476b1d2f07.png

注意,这里的N是 U+FF2E,也就是拉丁文中的N,其unicode编码为 /xFF/x2E

http://p7.qhimg.com/t01396b82a5098fc1d3.png

最终,由于nodeJS的处理问题 xFF 被丢弃了,剩下的x2E被解析为.

于是我们得到了如下的结论

http://p8.qhimg.com/t0115ecb11f15dfa3ce.png

在NodeJS的http模块中, NN/ 可以起到../ 的作用,绕过特定的过滤

那么,nodeJS对于之前我们所研究的CRLF注入,又能不能够加以防御呢?

[1] HTTP模块可以避免直接的CRLF注入

[2] 那么,当我们将换行符编码时,会如何呢?

http://p2.qhimg.com/t011dc10b19355dc4f0.png

很明显,这时候它并不会进行自动的解码操作

如何打破这个僵局呢? 使用U+FF0D和U+FF0A

http://p6.qhimg.com/t010a0e82b6a17d78fc.png

我们成功的往请求中注入了新的一行

Glibc中的NSS特性

在Glibc的源代码文件 resolv/ns_name.c中,有一个叫ns_name_pton的函数

http://p4.qhimg.com/t01a224dc0ba2ddf80f.png

它遵循RFC1035标准,把一个ascii字符串转化成一个编码后的域名

这有什么可利用的呢?

让我们来看下面的代码

http://p4.qhimg.com/t01676277fda77d8a1e.png

通过gethostbyname函数来解析一个域名

在字符串中,代表转义符号,因此用\097来代表ascii码为97,也就是字母a

成功的解析到了orange.tw的ip地址

那么我们看看python的gethostbyname

http://p1.qhimg.com/t01dfc141d706a2b3ef.png

更让我们惊奇的是,它忽略了这些号 而解析到了orange.tw

同样的,一些类似的特性存在于linux的getaddrinfo()函数中,它会自动过滤掉空格后的垃圾信息

http://p4.qhimg.com/t01ff4a4b41e50823cb.png

python socket中的gethostbyname是依赖于getaddrinfo()函数的

因此出现了类似的问题,当传入CRLF时,后面的部分被丢弃了

http://p1.qhimg.com/t010d695197582a1eb7.png

说了这么多,这些特性有什么可以利用的地方呢?

让我们来看如下的几种payload

http://p7.qhimg.com/t01fd6c4be47d8a2dc8.png

可以想到的是,如果利用Glibc的NSS特性,当检查URL时,gethostbyname将其识别为127.0.0.1

为什么%2509能够生效呢?部分的函数实现可能会解码两次,甚至循环解码到不含URL编码

那么接下来,实际发起访问时,我们就可以使用CRLF注入了

http://p8.qhimg.com/t0102360c020fc21e6c.png

由此注入了一条redis语句

同样的,当HTTPS开启了之前我们提到的TLS SNI(Server Name Indication)时

它会把我们传入的域名放到握手包的client hello后面

http://p1.qhimg.com/t01e8de41244b1a438a.png

这样我们就成功的注入了一条语句

http://p8.qhimg.com/t0193bd0cdbea75d04f.png

而我们还可以进一步延伸,比如曾经的python CRLF注入漏洞,CVE-2016-5699

可以看到,这里其实允许CRLF后紧跟一个空格

http://p9.qhimg.com/t0175bfa7955dcfb681.png

由此绕过了_is_illegal_header_value()函数的正则表达式

但是,相应的应用会接受在行开头的空格么?

http://p8.qhimg.com/t016eacf58b654fb94d.png

可以看到,redis和memcached都是允许的,也就产生了利用。

利用IDNA标准

IDNA是,Internationalizing Domain Names in Applications的缩写,也就是'让域名变得国际化'

http://p2.qhimg.com/t016f10f1e7d31be273.png

上图是IDNA各个版本的标准,这个问题依赖于URL解析器和实际的请求器之间所用的IDNA标准不同

可以说,仍然是差异性的攻击。

http://p2.qhimg.com/t016935999cc956b2d4.png

比如,我们来看这个例子,将这个希腊字母转化为大写时,得到了SS

其实,这个技巧在之前的xss挑战赛 prompt 1 to win当中也有用到

这里我们面对的的是Wordpress

1.它其实很注重保护自己不被SSRF攻击

2.但是仍然被我们发现了3种不同的方法来绕过它的SSRF保护;

3.在2017年2月25日就已经向它报告了这几个漏洞,但是仍然没有被修复

4.为了遵守漏洞披露机制,我选择使用MyBB作为接下来的案例分析

实际上,我们仍然是追寻'差异性'来达到攻击的目的

这次要分析的,是URL解析器,dns检查器,以及URL请求器之间的差异性

http://p1.qhimg.com/t01c5c05444b3753e7a.png

上表列出了三种不同的web应用分别使用的URL解析器,dns检查器,以及URL请求器

[1] 第一种绕过方法

其实就是之前大家所普遍了解的dns-rebinding攻击

http://p5.qhimg.com/t011a60e56f4afa9fa2.png

在dns解析和最终请求之间有一个时间差,可以通过重新解析dns的方法进行绕过

http://p5.qhimg.com/t01ebc7f276d9181f3b.png

1.gethostbyname()函数得到了ip地址1.2.3.4

2.检查发现,1.2.3.4不在黑名单列表中

3.用curl_init()来获得一个ip地址,这时候cURL会再次发出一次DNS请求

4.最终我们重新解析foo.orange.tw到127.0.0.1 产生了一个dns攻击

[2] 第二种绕过方法

利用DNS解析器和URL请求器之间的差异性攻击

http://p5.qhimg.com/t01bbf77e54d1e493a8.png

对于gethostbynamel()这个DNS解析器所用的函数来说

它没有使用IDNA标准的转换,但是cURL却使用了

于是最终产生的后果是,gethostbynamel()解析不到对应的ip,返回了false

也就绕过了这里的检查。

[3] 第三种绕过方法

利用URL解析器和URL请求器之间的差异性攻击

这个漏洞已经在PHP 7.0.13中得到了修复

http://p5.qhimg.com/t0187d83268152d6e0f.png

有趣的是,这里最终请求到的是127.0.0.1:11211

而下一个payload则显示了curl的问题,最终也被解析到本地ip

http://p6.qhimg.com/t017fcd9538d8cc6e14.png

而这个漏洞也在cURL 7.54中被修复

可惜的是,ubuntu 17.04中自带的libcurl的版本仍然是7.52.1

但是,即使是这样进行了修复,参照之前的方法,添加空格仍然继续可以绕过

http://p7.qhimg.com/t01d83e0cf0869544e8.png

而且cURL明确表示不会修复

协议'走私' 案例分析

这次我们分析的是github企业版

它使用ruby on rails框架编写,而且代码已经被做了混淆处理

关于github企业版的远程代码执行漏洞

是github三周年报告的最好漏洞

它把4个漏洞结合为一个攻击链,实现了远程代码执行的攻击

[1] 第一个漏洞:在webhooks上的SSRF绕过

webhooks是什么?

http://p6.qhimg.com/t01c762c8ec15f0aefe.png

这就很明显了,它含有发送POST数据包的功能

而它是如何实现的呢?

请求器使用了rubygem-faraday是一个HTTP/REST 客户端库

而黑名单则由其内部的faraday-restrict-ip-addresses所定义

它过滤了localhost,127.0.0.1等地址

但是仅仅用一个简单的 0 就可以加以绕过,像这样

http://p5.qhimg.com/t01c44d3b13819a9bdc.png

但是,这个漏洞里有好几个限制,比如

不允许302重定向

不允许http,https之外的协议

不允许CRLF注入

只允许POST方式发送数据包

[2] 第二个漏洞:github企业版使用Graphite来绘制图标,它运行在本地的8000端口

http://p2.qhimg.com/t013524b29932058a4a.png

这里也是存在SSRF的

[3] 第三个漏洞 Graphite 中的CRLF注入

Graphite是由python编写的

于是,分析可知,这第二个SSRF的实现是httplib.HTTPConnection

很明显的,httplib是存在CRLF注入问题的

于是,我们可以构造下面的URL,产生一个'走私'漏洞

http://p6.qhimg.com/t01fae3b74fa7c35415.png

[4] 第四个漏洞 Memcached gem中不安全的编排问题

Github企业版使用Memcached gem来作为它的缓存客户端

所有缓存的ruby对象都会被编排

最终的攻击链如下:

http://p8.qhimg.com/t0165706f07a6289be6.png

这个漏洞最终获得了12500美金的奖励

在github企业版<2.8.7中可以使用

缓解措施

[1] 应用层

使用唯一的ip地址和URL,而不是对输入的URL进行复用

简单的说,拒绝对输入的URL进行二次解析,只使用第一次的结果

[2] 网络层

使用防火墙或者协议来阻断内网的通行

[3] 相关的项目

由 @fin1te 编写的SafeCurl

它也被 @JordanMilne 所提倡

总结

SSRF中的新攻击面

[1] URL解析器的问题

[2] 滥用IDNA标准

协议'走私'中的新攻击向量

[1] 利用linux Glibc中的新特性

[2] 利用NodeJS对Unicode字符的处理问题

以及相关的具体案例分析

未来展望

[1] OAuth中的URL解析器

[2] 现代浏览器中的URL解析器

[3] 代理服务器中的URL解析器

以及.. 更多

(完)