Skip to content

Commit ba1f7c3

Browse files
author
HD Moore
committed
Land rapid7#3687, reworks the nat-pmp portscanner
2 parents 3b8bbdf + ed9bb3e commit ba1f7c3

File tree

7 files changed

+138
-86
lines changed

7 files changed

+138
-86
lines changed

lib/msf/core/auxiliary/mixins.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
require 'msf/core/auxiliary/rservices'
2121
require 'msf/core/auxiliary/cisco'
2222
require 'msf/core/auxiliary/nmap'
23+
require 'msf/core/auxiliary/natpmp'
2324
require 'msf/core/auxiliary/iax2'
2425
require 'msf/core/auxiliary/ntp'
2526
require 'msf/core/auxiliary/pii'

lib/msf/core/auxiliary/natpmp.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: binary -*-
2+
require 'rex/proto/natpmp'
3+
4+
module Msf
5+
6+
###
7+
#
8+
# This module provides methods for working with NAT-PMP
9+
#
10+
###
11+
module Auxiliary::NATPMP
12+
13+
include Auxiliary::Scanner
14+
include Rex::Proto::NATPMP
15+
16+
def initialize(info = {})
17+
super
18+
register_options(
19+
[
20+
Opt::RPORT(Rex::Proto::NATPMP::DefaultPort),
21+
Opt::CHOST
22+
],
23+
self.class
24+
)
25+
end
26+
end
27+
end

lib/rex/proto/natpmp/packet.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@ module Proto
1212
module NATPMP
1313

1414
# Return a NAT-PMP request to get the external address.
15-
def self.external_address_request
15+
def external_address_request
1616
[ 0, 0 ].pack('nn')
1717
end
1818

1919
# Parse a NAT-PMP external address response +resp+.
2020
# Returns the decoded parts of the response as an array.
21-
def self.parse_external_address_response(resp)
22-
(ver, op, result, epoch, addr) = resp.unpack("CCvVN")
21+
def parse_external_address_response(resp)
22+
(ver, op, result, epoch, addr) = resp.unpack("CCnNN")
2323
[ ver, op, result, epoch, Rex::Socket::addr_itoa(addr) ]
2424
end
2525

2626
# Return a NAT-PMP request to map remote port +rport+/+protocol+ to local port +lport+ for +lifetime+ ms
27-
def self.map_port_request(lport, rport, protocol, lifetime)
28-
[ Rex::Proto::NATPMP::Version, # version
27+
def map_port_request(lport, rport, protocol, lifetime)
28+
[ Rex::Proto::NATPMP::Version, # version
2929
protocol, # opcode, which is now the protocol we are asking to forward
3030
0, # reserved
3131
lport,
@@ -36,8 +36,8 @@ def self.map_port_request(lport, rport, protocol, lifetime)
3636

3737
# Parse a NAT-PMP mapping response +resp+.
3838
# Returns the decoded parts as an array.
39-
def self.parse_map_port_response(resp)
40-
resp.unpack("CCvVnnN")
39+
def parse_map_port_response(resp)
40+
resp.unpack("CCnNnnN")
4141
end
4242
end
4343

modules/auxiliary/admin/natpmp/natpmp_map.rb

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
##
55

66
require 'msf/core'
7-
require 'rex/proto/natpmp'
87

98
class Metasploit3 < Msf::Auxiliary
109

1110
include Msf::Auxiliary::Report
1211
include Msf::Auxiliary::Scanner
12+
include Msf::Auxiliary::NATPMP
13+
include Rex::Proto::NATPMP
1314

1415
def initialize
1516
super(
@@ -21,12 +22,10 @@ def initialize
2122

2223
register_options(
2324
[
24-
Opt::LPORT,
25-
Opt::RPORT,
26-
OptInt.new('NATPMPPORT', [true, "NAT-PMP port to use", Rex::Proto::NATPMP::DefaultPort]),
25+
OptPort.new('EXTERNAL_PORT', [true, 'The external port to foward from']),
26+
OptPort.new('INTERNAL_PORT', [true, 'The internal port to forward to']),
2727
OptInt.new('LIFETIME', [true, "Time in ms to keep this port forwarded", 3600000]),
2828
OptEnum.new('PROTOCOL', [true, "Protocol to forward", 'TCP', %w(TCP UDP)]),
29-
Opt::CHOST
3029
],
3130
self.class
3231
)
@@ -43,21 +42,20 @@ def run_host(host)
4342

4443
# get the external address first
4544
vprint_status "#{host} - NATPMP - Probing for external address"
46-
req = Rex::Proto::NATPMP.external_address_request
47-
udp_sock.sendto(req, host, datastore['NATPMPPORT'], 0)
45+
udp_sock.sendto(external_address_request, host, datastore['RPORT'], 0)
4846
external_address = nil
4947
while (r = udp_sock.recvfrom(12, 1) and r[1])
50-
(ver, op, result, epoch, external_address) = Rex::Proto::NATPMP.parse_external_address_response(r[0])
48+
(ver, op, result, epoch, external_address) = parse_external_address_response(r[0])
5149
end
5250

5351
vprint_status "#{host} - NATPMP - Sending mapping request"
5452
# build the mapping request
55-
req = Rex::Proto::NATPMP.map_port_request(
56-
datastore['LPORT'].to_i, datastore['RPORT'].to_i,
53+
req = map_port_request(
54+
datastore['INTERNAL_PORT'], datastore['EXTERNAL_PORT'],
5755
Rex::Proto::NATPMP.const_get(datastore['PROTOCOL']), datastore['LIFETIME']
5856
)
5957
# send it
60-
udp_sock.sendto(req, host, datastore['NATPMPPORT'], 0)
58+
udp_sock.sendto(req, host, datastore['RPORT'], 0)
6159
# handle the reply
6260
while (r = udp_sock.recvfrom(16, 1) and r[1])
6361
handle_reply(Rex::Socket.source_address(host), host, external_address, r)
@@ -78,12 +76,12 @@ def handle_reply(map_target, host, external_address, pkt)
7876
pkt[1] = pkt[1].sub(/^::ffff:/, '')
7977
end
8078

81-
(ver, op, result, epoch, internal_port, external_port, lifetime) = Rex::Proto::NATPMP.parse_map_port_response(pkt[0])
79+
(ver, op, result, epoch, internal_port, external_port, lifetime) = parse_map_port_response(pkt[0])
8280

8381
if (result == 0)
84-
if (datastore['RPORT'].to_i != external_port)
82+
if (datastore['EXTERNAL_PORT'] != external_port)
8583
print_status( "#{external_address} " +
86-
"#{datastore['RPORT']}/#{datastore['PROTOCOL']} -> #{map_target} " +
84+
"#{datastore['EXTERNAL_PORT']}/#{datastore['PROTOCOL']} -> #{map_target} " +
8785
"#{internal_port}/#{datastore['PROTOCOL']} couldn't be forwarded")
8886
end
8987
print_status( "#{external_address} " +

modules/auxiliary/gather/natpmp_external_address.rb

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
##
55

66
require 'msf/core'
7-
require 'rex/proto/natpmp'
87

98
class Metasploit3 < Msf::Auxiliary
109

1110
include Msf::Auxiliary::Report
12-
include Msf::Auxiliary::Scanner
11+
include Msf::Exploit::Remote::Udp
12+
include Msf::Auxiliary::UDPScanner
13+
include Msf::Auxiliary::NATPMP
14+
include Rex::Proto::NATPMP
1315

1416
def initialize
1517
super(
@@ -19,68 +21,43 @@ def initialize
1921
'License' => MSF_LICENSE
2022
)
2123

22-
register_options(
23-
[
24-
Opt::RPORT(Rex::Proto::NATPMP::DefaultPort),
25-
Opt::CHOST
26-
],
27-
self.class
28-
)
2924
end
3025

31-
def run_host(host)
32-
begin
33-
udp_sock = Rex::Socket::Udp.create({
34-
'LocalHost' => datastore['CHOST'] || nil,
35-
'Context' => {'Msf' => framework, 'MsfExploit' => self}
36-
})
37-
add_socket(udp_sock)
38-
vprint_status "#{host}:#{datastore['RPORT']} - NATPMP - Probing for external address"
39-
40-
udp_sock.sendto(Rex::Proto::NATPMP.external_address_request, host, datastore['RPORT'].to_i, 0)
41-
while (r = udp_sock.recvfrom(12, 1.0) and r[1])
42-
handle_reply(host, r)
43-
end
44-
rescue ::Interrupt
45-
raise $!
46-
rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused
47-
nil
48-
rescue ::Exception => e
49-
print_error("#{host}:#{datastore['RPORT']} Unknown error: #{e.class} #{e}")
50-
end
26+
def scan_host(ip)
27+
scanner_send(@probe, ip, datastore['RPORT'])
5128
end
5229

53-
def handle_reply(host, pkt)
54-
return if not pkt[1]
55-
56-
if(pkt[1] =~ /^::ffff:/)
57-
pkt[1] = pkt[1].sub(/^::ffff:/, '')
58-
end
30+
def scanner_prescan(batch)
31+
@probe = external_address_request
32+
end
5933

60-
(ver, op, result, epoch, external_address) = Rex::Proto::NATPMP.parse_external_address_response(pkt[0])
34+
def scanner_process(data, shost, sport)
35+
(ver, op, result, epoch, external_address) = parse_external_address_response(data)
6136

62-
if (result == 0)
63-
print_status("#{host} -- external address #{external_address}")
37+
peer = "#{shost}:#{sport}"
38+
if (ver == 0 && op == 128 && result == 0)
39+
print_good("#{peer} -- external address #{external_address}")
40+
# report its external address as alive
41+
if inside_workspace_boundary?(external_address)
42+
report_host(
43+
:host => external_address,
44+
:state => Msf::HostState::Alive
45+
)
46+
end
47+
else
48+
print_error("#{peer} -- unexpected version/opcode/result/address: #{ver}/#{op}/#{result}/#{external_address}")
6449
end
6550

6651
# report the host we scanned as alive
6752
report_host(
68-
:host => host,
53+
:host => shost,
6954
:state => Msf::HostState::Alive
7055
)
7156

72-
# also report its external address as alive
73-
if inside_workspace_boundary?(external_address)
74-
report_host(
75-
:host => external_address,
76-
:state => Msf::HostState::Alive
77-
)
78-
end
79-
8057
# report NAT-PMP as being open
8158
report_service(
82-
:host => host,
83-
:port => pkt[2],
59+
:host => shost,
60+
:port => sport,
8461
:proto => 'udp',
8562
:name => 'natpmp',
8663
:state => Msf::ServiceState::Open

modules/auxiliary/scanner/natpmp/natpmp_portscan.rb

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55

66

77
require 'msf/core'
8-
require 'rex/proto/natpmp'
98

109
class Metasploit3 < Msf::Auxiliary
1110

1211
include Msf::Auxiliary::Report
1312
include Msf::Auxiliary::Scanner
13+
include Msf::Auxiliary::NATPMP
14+
include Rex::Proto::NATPMP
1415

1516
def initialize
1617
super(
@@ -22,10 +23,8 @@ def initialize
2223

2324
register_options(
2425
[
25-
Opt::RPORT(Rex::Proto::NATPMP::DefaultPort),
2626
OptString.new('PORTS', [true, "Ports to scan (e.g. 22-25,80,110-900)", "1-1000"]),
2727
OptEnum.new('PROTOCOL', [true, "Protocol to scan", 'TCP', %w(TCP UDP)]),
28-
Opt::CHOST
2928
], self.class)
3029
end
3130

@@ -36,32 +35,33 @@ def run_host(host)
3635
'Context' => {'Msf' => framework, 'MsfExploit' => self} }
3736
)
3837
add_socket(udp_sock)
39-
vprint_status "Scanning #{datastore['PROTOCOL']} ports #{datastore['PORTS']} on #{host} using NATPMP"
38+
peer = "#{host}:#{datastore['RPORT']}"
39+
vprint_status("#{peer} Scanning #{datastore['PROTOCOL']} ports #{datastore['PORTS']} using NATPMP")
4040

4141
# first, send a request to get the external address
42-
udp_sock.sendto(Rex::Proto::NATPMP.external_address_request, host, datastore['RPORT'].to_i, 0)
42+
udp_sock.sendto(external_address_request, host, datastore['RPORT'], 0)
4343
external_address = nil
4444
while (r = udp_sock.recvfrom(12, 0.25) and r[1])
45-
(ver,op,result,epoch,external_address) = Rex::Proto::NATPMP.parse_external_address_response(r[0])
45+
(ver,op,result,epoch,external_address) = parse_external_address_response(r[0])
4646
end
4747

4848
if (external_address)
49-
print_good("External address of #{host} is #{external_address}")
49+
print_good("#{peer} responded with external address of #{external_address}")
5050
else
51-
print_error("Didn't get a response for #{host}'s external address")
51+
vprint_status("#{peer} didn't respond with an external address")
5252
return
5353
end
5454

5555
Rex::Socket.portspec_crack(datastore['PORTS']).each do |port|
5656
# send one request to clear the mapping if *we've* created it before
57-
clear_req = Rex::Proto::NATPMP.map_port_request(port, port, Rex::Proto::NATPMP.const_get(datastore['PROTOCOL']), 0)
58-
udp_sock.sendto(clear_req, host, datastore['RPORT'].to_i, 0)
57+
clear_req = map_port_request(port, port, Rex::Proto::NATPMP.const_get(datastore['PROTOCOL']), 0)
58+
udp_sock.sendto(clear_req, host, datastore['RPORT'], 0)
5959
while (r = udp_sock.recvfrom(16, 1.0) and r[1])
6060
end
6161

6262
# now try the real mapping
63-
map_req = Rex::Proto::NATPMP.map_port_request(port, port, Rex::Proto::NATPMP.const_get(datastore['PROTOCOL']), 1)
64-
udp_sock.sendto(map_req, host, datastore['RPORT'].to_i, 0)
63+
map_req = map_port_request(port, port, Rex::Proto::NATPMP.const_get(datastore['PROTOCOL']), 1)
64+
udp_sock.sendto(map_req, host, datastore['RPORT'], 0)
6565
while (r = udp_sock.recvfrom(16, 1.0) and r[1])
6666
handle_reply(host, external_address, r)
6767
end
@@ -85,21 +85,22 @@ def handle_reply(host, external_addr, pkt)
8585
host = pkt[1]
8686
protocol = datastore['PROTOCOL'].to_s.downcase
8787

88-
(ver, op, result, epoch, int, ext, lifetime) = Rex::Proto::NATPMP.parse_map_port_response(pkt[0])
88+
(ver, op, result, epoch, int, ext, lifetime) = parse_map_port_response(pkt[0])
89+
peer = "#{host}:#{datastore['RPORT']}"
8990
if (result == 0)
9091
# we always ask to map an external port to the same port on us. If
9192
# we get a successful reponse back but the port we requested be forwarded
9293
# is different, that means that someone else already has it open
9394
if (int != ext)
9495
state = Msf::ServiceState::Open
95-
print_status("#{external_addr} - #{int}/#{protocol} #{state} because of successful mapping with unmatched ports")
96+
print_good("#{peer} #{external_addr} - #{int}/#{protocol} #{state} because of successful mapping with unmatched ports")
9697
else
9798
state = Msf::ServiceState::Closed
98-
print_status("#{external_addr} - #{int}/#{protocol} #{state} because of successful mapping with matched ports") if (datastore['DEBUG'])
99+
print_status("#{peer} #{external_addr} - #{int}/#{protocol} #{state} because of successful mapping with matched ports") if (datastore['DEBUG'])
99100
end
100101
else
101102
state = Msf::ServiceState::Closed
102-
print_status("#{external_addr} - #{int}/#{protocol} #{state} because of code #{result} response") if (datastore['DEBUG'])
103+
print_status("#{peer} #{external_addr} - #{int}/#{protocol} #{state} because of code #{result} response") if (datastore['DEBUG'])
103104
end
104105

105106
if inside_workspace_boundary?(external_addr)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# -*- coding: binary -*-
2+
require 'spec_helper'
3+
4+
require 'rex/proto/natpmp/packet'
5+
describe Rex::Proto::NATPMP do
6+
subject do
7+
mod = Module.new
8+
mod.extend described_class
9+
mod
10+
end
11+
12+
describe '#parse_external_address_response' do
13+
it 'should properly parse non-error responses' do
14+
data = "\x00\x80\x00\x00\x00\x33\x50\x53\xc0\xa8\x01\x02"
15+
subject.parse_external_address_response(data)
16+
ver, opcode, result, epoch, addr = subject.parse_external_address_response(data)
17+
expect(ver).to eq(0)
18+
expect(opcode).to eq(128)
19+
expect(result).to eq(0)
20+
expect(epoch).to eq(3362899)
21+
expect(addr).to eq('192.168.1.2')
22+
end
23+
it 'should properly parse error responses' do
24+
data = "\x00\x80\x00\x03\x00\x00\x70\x90\x00\x00\x00\x00"
25+
subject.parse_external_address_response(data)
26+
ver, opcode, result, epoch, addr = subject.parse_external_address_response(data)
27+
expect(ver).to eq(0)
28+
expect(opcode).to eq(128)
29+
expect(result).to eq(3)
30+
expect(epoch).to eq(28816)
31+
expect(addr).to eq('0.0.0.0')
32+
end
33+
end
34+
35+
describe '#parse_map_port_response' do
36+
it 'should properly parse responses' do
37+
data = "\x00\x82\x00\x00\x00\x33\x6f\xd8\x11\x5c\x15\xb3\x00\x36\xee\x80"
38+
ver, opcode, result, epoch, internal, external, lifetime = subject.parse_map_port_response(data)
39+
expect(ver).to eq(0)
40+
expect(opcode).to eq(130)
41+
expect(result).to eq(0)
42+
expect(epoch).to eq(3370968)
43+
expect(internal).to eq(4444)
44+
expect(external).to eq(5555)
45+
expect(lifetime).to eq(3600000)
46+
end
47+
end
48+
end

0 commit comments

Comments
 (0)