Python dnslib阅读小记

  python

0x01 DNS解析类型

DNS服务器的解析大致分为几类,AAAAACNAMEMXNSTXTSRVSOAPTR等,大致为

A记录: 将域名指向一个IPV4的ip地址

CNAME记录: 将该域名指向另外一个域名

MX记录: 建立电子邮箱服务,将指向邮件服务地址

NS记录: 域名解析记录,如果要将子域名指定某个域名服务器来解析,需要设置NS记录

TXT记录:  可任意填写,一般为某主机或域名设置使用,用于做一些备注

AAAA记录: 将域名指向一个IPV6记录

PTR记录: 记录A记录的逆向记录,又称做IP反查记录或指针记录,负责将IP反向解析为域名

SOA记录: 表示开始权限记录,记录用于在众多NS记录中那一台是主服务器

 

0x02 dnslib

dnslib为python的一个用于编码/解码的DNS有线格式数据包的库。dnslib最主要的就是用于一个dns解析器。其主要的类为DNSServer()。我们可以使用如下的命令来构造我们的第一个dns服务器

resolver = BaseResolver()
logger = DNSLogger(prefix=False)
server = DNSServer(resolver,port=8053,address="localhost",logger=logger)
server.start_thread()
q = DNSRecord.question("example.com")
a = q.send("localhost",8053)

print(DNSRecord.parse(a))
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;example.com.                     IN      A

同时我们可以自定义我们dns服务器需要解析域名的地址,可以使用如下代码

class TestResolver:
    def resolve(self,request,handler):
        reply = request.reply()
        reply.add_answer(*RR.fromZone("example IN A 1.2.3.4"))
        return reply
resolver = TestResolver()
server = DNSServer(resolver,port=8053,address="localhost",logger=logger,tcp=True)
server.start_thread()
a = q.send("localhost",8053,tcp=True)

因此我们可以将example.com的域名指向1.2.3.4

 

首先跟一下DNSServer类,其为自定义DNS解析的主要类,其调用了socketserver模块来进行发包

class DNSServer(object):

    def __init__(self,resolver,
                      address="",
                      port=53,
                      tcp=False,
                      logger=None,
                      handler=DNSHandler,
                      server=None):
        if not server:
            if tcp:
                server = TCPServer
            else:
                server = UDPServer
        self.server = server((address,port),handler)
        self.server.resolver = resolver
        self.server.logger = logger or DNSLogger()

class UDPServer(socketserver.ThreadingMixIn,socketserver.UDPServer):
    allow_reuse_address = True

class TCPServer(socketserver.ThreadingMixIn,socketserver.TCPServer):
    allow_reuse_address = True

resolver则为解析器或者转发的作用,同时继承了socketserver中UPDP和TCP的发包.

dnslib.client

client脚本为dnslib中用来查询DNS信息文件,其使用方法为

python -m dnslib.client example.com

跟一下cilent查询的整个过程,从client.py文件中,代码首先执行到了

q = DNSRecord(q=DNSQuestion("christa.top",getattr(QTYPE,args.qtype)))

这一行来,DNSQuestion类中对域名和域名的了行做了初始化

class DNSQuestion(object):
    
    def __init__(self,qname=None,qtype=1,qclass=1):
        self.qname = qname
        self.qtype = qtype
        self.qclass = qclass

紧接着到DNSRecord里面,进行了认证等一系列的初始化

然后,设置解析地址和端口

之后,进入到send阶段,send函数大致如下

  def send(self,dest,port=53,tcp=False,timeout=None,ipv6=False):
        """
            Send packet to nameserver and return response
        """
        data = self.pack()
        if ipv6:
            inet = socket.AF_INET6
        else:
            inet = socket.AF_INET
        if tcp:
            if len(data) > 65535:
                raise ValueError("Packet length too long: %d" % len(data))
            data = struct.pack("!H",len(data)) + data
            sock = socket.socket(inet,socket.SOCK_STREAM)
            if timeout is not None:
                sock.settimeout(timeout)
            sock.connect((dest,port))
            sock.sendall(data)
            response = sock.recv(8192)
            length = struct.unpack("!H",bytes(response[:2]))[0]
            while len(response) - 2 < length:
                response += sock.recv(8192)
            sock.close()
            response = response[2:]
        else:
            sock = socket.socket(inet,socket.SOCK_DGRAM)
            if timeout is not None:
                sock.settimeout(timeout)
            sock.sendto(self.pack(),(dest,port))
            response,server = sock.recvfrom(8192)
            sock.close()
        return response
        

将打包好的数据发送到执行的DNS解析器上,那么跟踪DNS查询数据的打包过程,继续跟进data = self.pack()代码

def pack(self):
    self.set_header_qa()
    buffer = DNSBuffer()
    self.header.pack(buffer)
    for q in self.questions:
        q.pack(buffer)
    for rr in self.rr:
        rr.pack(buffer)
    for auth in self.auth:
        auth.pack(buffer)
    for ar in self.ar:
        ar.pack(buffer)
    return buffer.data

set_header_qa中将去除查询的数组的长度

def set_header_qa(self):
    self.header.q = len(self.questions)
    self.header.a = len(self.rr)
    self.header.auth = len(self.auth)
    self.header.ar = len(self.ar)

之后进入DNSBuffer的构造,在DNSBuffer类中继承了Buffer类,主要将字符转化为bytes类型

然后进入到pack打包阶段

def pack(self,buffer):
    buffer.pack("!HHHHHH",self.id,self.bitmap,
                          self.q,self.a,self.auth,self.ar)

其中每一个参数的调用为

改函数为struct !HHHHHH打包

之后将需要解析的域名进行struct !B打包

最后将需要查询的type,qclass进行打包,也就是查询域名类型,类似A,TXT,AAAA记录等进行!HH打包

最后得出查询数据bytearray(b'\xd8\xff\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07christa\x03top\x00\x00\x01\x00\x01'),最后发送给8.8.8.8的53端口上

因此dnslib查询的过程为此

 

dnslib.proxy

该模块为一个代理模式,用于查看DNS的解析记录,用法为

python -m dnslib.proxy

dnslib.intercept

改模块为自定义一个DNS的返回模式,可用于DNS重绑定漏洞,其原理为当网站查询一个网址的时候其原来指向一个外网地址,之后对网络资源进行操作的时候,重新将网址的地址指向到127.0.0.1,这样就读取了服务器自身的文件,也就造成了SSRF漏洞,只需要我们将需要的域名设置一个NS记录,设置它的域名解析器为我们另一个网址,用阿里云域名举例来说

这样将dns.christa.top的域名指名到另一个域名来接解析,因此设置执行dns服务器返回的DNS的解析结果即可,可以使用命令

python3 -m dnslib.intercept -i "dns.christa.top IN A 127.0.0.2"

之后进行DNS查询

可以看到dns.christa.top域名被成功指向到127.0.0.2地址

DNS存储IP的形式是五元组,dnslib的例子为

[(<DNSLabel: 'dns.christa.top.'>, 'A', <DNS RR: 'dns.christa.top.' rtype=A rclass=IN ttl=60 rdata='127.0.0.2'>)]

 

 

0xFF Reference