Skip to content

Commit dac355d

Browse files
Land #16492, nfs_mount more intelligent mountability
2 parents b464f97 + c6936bd commit dac355d

File tree

4 files changed

+270
-104
lines changed

4 files changed

+270
-104
lines changed

documentation/modules/auxiliary/scanner/nfs/nfsmount.md

Lines changed: 105 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,102 @@
11
## Vulnerable Application
22

3-
NFS is very common, and this scanner searches for a mis-configuration, not a vulnerable software version. Installation instructions for NFS can be found for every operating system.
4-
The [Ubuntu 14.04](https://help.ubuntu.com/14.04/serverguide/network-file-system.html) instructions can be used as an example for installing and configuring NFS. The
3+
NFS is very common, and this scanner searches for a mis-configuration, not a vulnerable software version.
4+
Installation instructions for NFS can be found for every operating system.
5+
The [Ubuntu](https://ubuntu.com/server/docs/service-nfs)
6+
instructions can be used as an example for installing and configuring NFS. The
57
following was done on Kali linux:
6-
7-
1. `apt-get install nfs-kernel-server`
8-
2. Create 2 folders to share:
9-
```
10-
mkdir /tmp/open_share
11-
mkdir /tmp/closed_share
12-
```
13-
3. Add them to the list of shares:
14-
```
15-
echo "/tmp/closed_share 10.1.2.3(ro,sync,no_root_squash)" >> /etc/exports
16-
echo "/tmp/open_share *(rw,sync,no_root_squash)" >> /etc/exports
17-
```
18-
4. Restart the service: `service nfs-kernel-server restart`
19-
20-
In this scenario, `closed_share` is set to read only, and only mountable by the IP 10.1.2.3. `open_share` is mountable by anyone (`*`) in read/write mode.
8+
9+
1. `apt-get install nfs-kernel-server`
10+
2. Create folders to share and add them to exports (adjust 192.168.1.x as needed):
11+
```
12+
mkdir /tmp/star
13+
echo "/tmp/star *(rw,no_subtree_check)" >> /etc/exports
14+
mkdir /tmp/not_us_hostname
15+
echo "/tmp/not_us_hostname foo(rw,no_subtree_check)" >> /etc/exports
16+
mkdir /tmp/us_hostname
17+
echo "/tmp/us_hostname bar(rw,no_subtree_check)" >> /etc/exports
18+
mkdir /tmp/not_us_ip
19+
echo "/tmp/not_us_ip 1.1.1.1(rw,no_subtree_check)" >> /etc/exports
20+
mkdir /tmp/us_ip
21+
echo "/tmp/us_ip 192.168.1.111(rw,no_subtree_check)" >> /etc/exports
22+
mkdir /tmp/not_us_subnet
23+
echo "/tmp/not_us_subnet 1.1.1.1/24(rw,no_subtree_check)" >> /etc/exports
24+
mkdir /tmp/us_subnet
25+
echo "/tmp/us_subnet 192.168.1.1/24(rw,no_subtree_check)" >> /etc/exports
26+
mkdir /tmp/not_us_netmask
27+
echo "/tmp/not_us_netmask 1.1.1.1/255.255.255.0(rw,no_subtree_check)" >> /etc/exports
28+
mkdir /tmp/us_netmask
29+
echo "/tmp/us_netmask 192.168.1.1/255.255.255.0(rw,no_subtree_check)" >> /etc/exports
30+
mkdir /tmp/empty
31+
echo "/tmp/empty (rw,no_subtree_check)" >> /etc/exports
32+
```
33+
3. Restart the service: `service nfs-kernel-server restart`
34+
35+
## Options
36+
37+
### PROTOCOL
38+
Which networking protocol to use. Options are `udp` and `tcp`. Defaults to `udp`.
39+
40+
### LHOST
41+
IP to match shares against if `Mountable` is true. Defaults to the detected local IP address.
42+
43+
### HOSTNAME
44+
Hostname to match shares against if `Mountable` is true. Defaults to `` (empty string)
45+
46+
## Advanced Options
47+
48+
### Mountable
49+
50+
Determine if an export is mountable based on `LHOST` and `HOSTNAME`. Defaults to `true`. Pre 2022 behavior was `false`
2151

2252
## Verification Steps
2353

24-
1. Install and configure NFS
25-
2. Start msfconsole
26-
3. Do: `use auxiliary/scanner/nfs/nfsmount`
27-
4. Do: `run`
54+
1. Install and configure NFS
55+
2. Start msfconsole
56+
3. Do: `use auxiliary/scanner/nfs/nfsmount`
57+
4. Do: `run`
2858

2959
## Scenarios
3060

31-
A run against the configuration from these docs
32-
33-
```
34-
msf > use auxiliary/scanner/nfs/nfsmount
35-
msf auxiliary(nfsmount) > set rhosts 127.0.0.1
36-
rhosts => 127.0.0.1
37-
msf auxiliary(nfsmount) > run
38-
39-
[+] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/open_share [*]
40-
[+] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/closed_share [10.1.2.3]
41-
[*] Scanned 1 of 1 hosts (100% complete)
42-
[*] Auxiliary module execution completed
43-
```
44-
45-
Another example can be found at this [source](http://bitvijays.github.io/blog/2016/03/03/learning-from-the-field-basic-network-hygiene/):
46-
47-
```
48-
[*] Scanned 24 of 240 hosts (10% complete)
49-
[+] 10.10.xx.xx NFS Export: /data/iso [0.0.0.0/0.0.0.0]
50-
[*] Scanned 48 of 240 hosts (20% complete)
51-
[+] 10.10.xx.xx NFS Export: /DataVolume/Public [*]
52-
[+] 10.10.xx.xx NFS Export: /DataVolume/Download [*]
53-
[+] 10.10.xx.xx NFS Export: /DataVolume/Softshare [*]
54-
[*] Scanned 72 of 240 hosts (30% complete)
55-
[+] 10.10.xx.xx NFS Export: /var/ftp/pub [10.0.0.0/255.255.255.0]
56-
[*] Scanned 96 of 240 hosts (40% complete)
57-
[+] 10.10.xx.xx NFS Export: /common []
58-
```
61+
A run against the configuration from these docs
62+
63+
```
64+
msf > use auxiliary/scanner/nfs/nfsmount
65+
msf auxiliary(nfsmount) > set rhosts 127.0.0.1
66+
rhosts => 127.0.0.1
67+
msf auxiliary(nfsmount) > run
68+
69+
[+] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/empty [*]
70+
[+] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/star [*]
71+
[+] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/us_netmask [10.1.1.1/255.255.255.0]
72+
[*] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/not_us_netmask [1.1.1.1/255.255.255.0]
73+
[+] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/us_subnet [10.1.1.1/24]
74+
[*] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/not_us_subnet [1.1.1.1/24]
75+
[+] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/us_ip [192.168.1.111]
76+
[*] 127.0.0.1:111 - 127.0.0.1 NFS Export: /tmp/not_us_ip [1.1.1.1]
77+
[*] 127.0.0.1:111 - Scanned 1 of 1 hosts (100% complete)
78+
[*] Auxiliary module execution completed
79+
```
80+
81+
Another example can be found at this [source](http://bitvijays.github.io/blog/2016/03/03/learning-from-the-field-basic-network-hygiene/):
82+
83+
```
84+
[*] Scanned 24 of 240 hosts (10% complete)
85+
[+] 10.10.xx.xx NFS Export: /data/iso [0.0.0.0/0.0.0.0]
86+
[*] Scanned 48 of 240 hosts (20% complete)
87+
[+] 10.10.xx.xx NFS Export: /DataVolume/Public [*]
88+
[+] 10.10.xx.xx NFS Export: /DataVolume/Download [*]
89+
[+] 10.10.xx.xx NFS Export: /DataVolume/Softshare [*]
90+
[*] Scanned 72 of 240 hosts (30% complete)
91+
[+] 10.10.xx.xx NFS Export: /var/ftp/pub [10.0.0.0/255.255.255.0]
92+
[*] Scanned 96 of 240 hosts (40% complete)
93+
[+] 10.10.xx.xx NFS Export: /common []
94+
```
5995

6096
## Confirming
6197

62-
Since NFS has been around since 1989, with modern NFS(v4) being released in 2000, there are many tools which can also be used to verify this configuration issue.
98+
Since NFS has been around since 1989, with modern NFS(v4) being released in 2000, there are many tools which can also be used to
99+
verify this configuration issue.
63100
The following are other industry tools which can also be used.
64101

65102
### [nmap](https://nmap.org/nsedoc/scripts/nfs-showmount.html)
@@ -73,8 +110,14 @@ Host is up (0.000037s latency).
73110
PORT STATE SERVICE
74111
111/tcp open rpcbind
75112
| nfs-showmount:
76-
| /tmp/open_share *
77-
|_ /tmp/closed_share 10.1.2.3
113+
| /tmp/empty *
114+
| /tmp/star *
115+
| /tmp/us_netmask 10.1.1.1/255.255.255.0
116+
| /tmp/not_us_netmask 1.1.1.1/255.255.255.0
117+
| /tmp/us_subnet 10.1.1.1/24
118+
| /tmp/not_us_subnet 1.1.1.1/24
119+
| /tmp/us_ip 192.168.1.111
120+
|_ /tmp/not_us_ip 1.1.1.1
78121
79122
Nmap done: 1 IP address (1 host up) scanned in 0.32 seconds
80123
```
@@ -86,14 +129,21 @@ showmount is a part of the `nfs-common` package for debian.
86129
```
87130
showmount -e 127.0.0.1
88131
Export list for 127.0.0.1:
89-
/tmp/open_share *
90-
/tmp/closed_share 10.1.2.3
132+
/tmp/empty *
133+
/tmp/star *
134+
/tmp/us_netmask 10.1.1.1/255.255.255.0
135+
/tmp/not_us_netmask 1.1.1.1/255.255.255.0
136+
/tmp/us_subnet 10.1.1.1/24
137+
/tmp/not_us_subnet 1.1.1.1/24
138+
/tmp/us_ip 192.168.1.111
139+
/tmp/not_us_ip 1.1.1.1
91140
```
92141

93142
## Exploitation
94143

95144
Exploiting this mis-configuration is trivial, however exploitation doesn't necessarily give access (command execution) to the system.
96-
If a share is mountable, ie you either are the IP listed in the filter (or could assume it through a DoS), or it is open (*), mounting is trivial.
145+
If a share is mountable, ie you either are the IP listed in the filter (or could assume it through a DoS),
146+
or it is open (*), mounting is trivial.
97147
The following instructions were written for Kali linux.
98148

99149
1. Create a new directory to mount the remote volume to: `mkdir /mnt/remote`

lib/msf/core/auxiliary/nfs.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf
4+
###
5+
#
6+
# This module provides methods for working with NFS
7+
#
8+
###
9+
module Auxiliary::Nfs
10+
include Auxiliary::Scanner
11+
12+
def initialize(info = {})
13+
super
14+
register_options(
15+
[
16+
OptAddressLocal.new('LHOST', [false, 'IP to match shares against', Rex::Socket.source_address]),
17+
OptString.new('HOSTNAME', [false, 'Hostname to match shares against', ''])
18+
]
19+
)
20+
end
21+
22+
def can_mount?(locations, mountable = true, hostname = '', lhost = '')
23+
# attempts to validate if we'll be able to open it or not based on:
24+
# 1. its a wildcard, thus we can open it
25+
# 2. hostname isn't blank and its in the list
26+
# 3. our IP is explicitly listed
27+
# 4. theres a CIDR notation that we're included in.
28+
return true unless mountable
29+
return true if locations.include? '*'
30+
return true if !hostname.blank? && locations.include?(hostname)
31+
return true if !lhost.empty? && locations.include?(lhost)
32+
33+
locations.each do |location|
34+
# if it has a subnet mask, convert it to cidr
35+
if %r{(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})} =~ location
36+
location = "#{Regexp.last_match(1)}#{Rex::Socket.addr_atoc(Regexp.last_match(2))}"
37+
end
38+
return true if Rex::Socket::RangeWalker.new(location).include?(lhost)
39+
# at this point we assume its a hostname, so we use Ruby's File fnmatch so that it proceses the wildcards
40+
# as its a quick and easy way to use glob matching for wildcards and get a boolean response
41+
return true if File.fnmatch(location, hostname)
42+
end
43+
false
44+
end
45+
end
46+
end

modules/auxiliary/scanner/nfs/nfsmount.rb

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,77 +7,82 @@ class MetasploitModule < Msf::Auxiliary
77
include Msf::Exploit::Remote::SunRPC
88
include Msf::Auxiliary::Report
99
include Msf::Auxiliary::Scanner
10+
include Msf::Auxiliary::Nfs
1011

1112
def initialize
1213
super(
13-
'Name' => 'NFS Mount Scanner',
14-
'Description' => %q{
14+
'Name' => 'NFS Mount Scanner',
15+
'Description' => %q{
1516
This module scans NFS mounts and their permissions.
1617
},
17-
'Author' => ['<tebo[at]attackresearch.com>'],
18-
'References' =>
19-
[
20-
['CVE', '1999-0170'],
21-
['URL', 'https://www.ietf.org/rfc/rfc1094.txt']
22-
],
18+
'Author' => ['<tebo[at]attackresearch.com>'],
19+
'References' => [
20+
['CVE', '1999-0170'],
21+
['URL', 'https://www.ietf.org/rfc/rfc1094.txt']
22+
],
2323
'License' => MSF_LICENSE
2424
)
2525

2626
register_options([
2727
OptEnum.new('PROTOCOL', [ true, 'The protocol to use', 'udp', ['udp', 'tcp']])
2828
])
2929

30+
register_advanced_options(
31+
[
32+
OptBool.new('Mountable', [false, 'Determine if an export is mountable', true]),
33+
]
34+
)
3035
end
3136

3237
def run_host(ip)
38+
program = 100005
39+
progver = 1
40+
procedure = 5
3341

34-
begin
35-
program = 100005
36-
progver = 1
37-
procedure = 5
42+
sunrpc_create(datastore['PROTOCOL'], program, progver)
43+
sunrpc_authnull
44+
resp = sunrpc_call(procedure, '')
3845

39-
sunrpc_create(datastore['PROTOCOL'], program, progver)
40-
sunrpc_authnull()
41-
resp = sunrpc_call(procedure, "")
46+
# XXX: Assume that transport is udp and port is 2049
47+
# Technically we are talking to mountd not nfsd
4248

43-
# XXX: Assume that transport is udp and port is 2049
44-
# Technically we are talking to mountd not nfsd
49+
report_service(
50+
host: ip,
51+
proto: datastore['PROTOCOL'],
52+
port: 2049,
53+
name: 'nfsd',
54+
info: "NFS Daemon #{program} v#{progver}"
55+
)
4556

46-
report_service(
47-
:host => ip,
48-
:proto => datastore['PROTOCOL'],
49-
:port => 2049,
50-
:name => 'nfsd',
51-
:info => "NFS Daemon #{program} v#{progver}"
52-
)
57+
exports = resp[3, 1].unpack('C')[0]
58+
if (exports == 0x01)
59+
shares = []
60+
while Rex::Encoder::XDR.decode_int!(resp) == 1
61+
dir = Rex::Encoder::XDR.decode_string!(resp)
62+
grp = []
63+
grp << Rex::Encoder::XDR.decode_string!(resp) while Rex::Encoder::XDR.decode_int!(resp) == 1
5364

54-
exports = resp[3,1].unpack('C')[0]
55-
if (exports == 0x01)
56-
shares = []
57-
while Rex::Encoder::XDR.decode_int!(resp) == 1 do
58-
dir = Rex::Encoder::XDR.decode_string!(resp)
59-
grp = []
60-
while Rex::Encoder::XDR.decode_int!(resp) == 1 do
61-
grp << Rex::Encoder::XDR.decode_string!(resp)
62-
end
63-
print_good("#{ip} NFS Export: #{dir} [#{grp.join(", ")}]")
64-
shares << [dir, grp]
65+
if can_mount?(grp, datastore['Mountable'], datastore['HOSTNAME'], datastore['LHOST'] || '')
66+
print_good("#{ip} Mountable NFS Export: #{dir} [#{grp.join(', ')}]")
67+
else
68+
print_status("#{ip} NFS Export: #{dir} [#{grp.join(', ')}]")
6569
end
66-
report_note(
67-
:host => ip,
68-
:proto => datastore['PROTOCOL'],
69-
:port => 2049,
70-
:type => 'nfs.exports',
71-
:data => { :exports => shares },
72-
:update => :unique_data
73-
)
74-
elsif(exports == 0x00)
75-
vprint_status("#{ip} - No exported directories")
70+
shares << [dir, grp]
7671
end
77-
78-
sunrpc_destroy
79-
rescue ::Rex::Proto::SunRPC::RPCTimeout, ::Rex::Proto::SunRPC::RPCError => e
80-
vprint_error(e.to_s)
72+
report_note(
73+
host: ip,
74+
proto: datastore['PROTOCOL'],
75+
port: 2049,
76+
type: 'nfs.exports',
77+
data: { exports: shares },
78+
update: :unique_data
79+
)
80+
elsif (exports == 0x00)
81+
vprint_status("#{ip} - No exported directories")
8182
end
83+
84+
sunrpc_destroy
85+
rescue ::Rex::Proto::SunRPC::RPCTimeout, ::Rex::Proto::SunRPC::RPCError => e
86+
vprint_error(e.to_s)
8287
end
8388
end

0 commit comments

Comments
 (0)