NFS AUTH_SYS identity spoofing, still the default in NFSv4

malcolmfrazier1 pts0 comments

NFSv3: Open Says Me, No Sesame Required | Opscode - Linux & Infrastructure EngineeringThis is not a new problem. There is nothing to patch, because none of it is a defect in the usual sense. It is the documented, intended behavior of a protocol that a surprising amount of infrastructure still runs today, very often underneath home directories.<br>I first ran this audit a few years ago against a large Linux infrastructure running CentOS 7, and recently set out to document it. Reproducing it on a modern Ubuntu 22.04 client took one detour, which I will explain shortly. The result is the same as it has always been: NFSv3 with AUTH_SYS does not authenticate anyone. It takes the client at its word.<br>This post walks the whole chain: reading another user&rsquo;s files, writing as them, spawning a shell as them, escalating to root, and then covering how to mitigate it.<br>How NFSv3 Authenticates#<br>It doesn&rsquo;t.<br>By default, NFSv3 uses AUTH_SYS (historically AUTH_UNIX). Although Kerberos authentication is supported via RPCSEC_GSS, the default out of the box is AUTH_SYS, and that is what historically the overwhelming majority of deployments run. Every RPC call carries a credential containing a UID, a primary GID, and a list of supplementary GIDs. The client fills those values in, and the server believes them.<br>There is no password, no ticket, no challenge, and no key. The only thing standing between an attacker and a given UID is the ability to write that number into the RPC credential. A normal kernel mount requires root on the client, and from then on the kernel fills each request&rsquo;s credential with the calling process&rsquo;s own UID and GID. That requirement does a lot of quiet, load-bearing work, and people mistake it for security. It is not security, and it is not even much of a barrier.<br>Two facts remove even that:<br>A userspace NFS client needs no kernel mount, so it needs no root, and nothing forces it to send a real UID. It builds the credential by hand.<br>root_squash, the one protection most admins can name, only squashes the root account (UID 0). It does nothing to UID 2000, or to any other non-root UID.<br>Put those together and any user who can reach the NFS server can read and write files as any non-root UID on the export. On a plain data share that is bad enough. When the export holds home directories, reading and writing as a UID becomes being that user, on every host that mounts it.<br>Lab setup#<br>Two hosts, flat lab network.<br>172.22.22.32 nfs.lab.opscode.io # NFS server, exports /export<br>172.22.22.31 target.lab.opscode.io # client, mounts /export/home<br>target mounts home directories from nfs, the familiar &ldquo;log in anywhere, your home follows you&rdquo; pattern. Two users:<br>bob:x:2000:2000::/export/home/bob:/bin/bash<br>mallory:x:2001:2001::/export/home/mallory:/bin/bash<br>mallory is the attacker: a plain, unprivileged user, with no sudo and nothing special.<br>mallory@target:~$ id<br>uid=2001(mallory) gid=2001(mallory) groups=2001(mallory)<br>bob&rsquo;s home directory is locked down exactly the way you would expect:<br>mallory@target:~$ ls -lad /export/home/bob<br>drwx------ 3 bob bob 4096 Jun 16 19:00 /export/home/bob<br>mallory@target:~$ ls -la /export/home/bob<br>ls: cannot open directory '/export/home/bob': Permission denied<br>Local POSIX permissions work exactly as expected. The directory is 0700, owned by bob, and mallory is denied. This is the protection everyone trusts. Watch it evaporate.<br>The tool#<br>The attack tool is libnfs and its ld_nfs.so shared library, loaded via LD_PRELOAD. It hooks standard glibc functions — open, read, write, chmod, chown, fchmod, fchown, the __xstat family of stat variants, and related functions — by resolving the real glibc implementations via dlsym(RTLD_NEXT, ...) at startup and replacing them with wrappers. When a wrapper detects a path beginning with nfs://, it bypasses the kernel&rsquo;s NFS client entirely and opens a direct userspace TCP connection to the NFS server via libnfs, without any local kernel NFS mount permission check.<br>Before issuing any RPC, the library calls nfs_set_uid() and nfs_set_gid() on the libnfs context with the values from LD_NFS_UID and LD_NFS_GID. Internally, those calls flow through to libnfs_authunix_create(), which constructs an AUTH_UNIX (AUTH_SYS) credential and packs the UID and GID values into its opaque body. That credential is attached to the RPC context and sent with all subsequent NFS protocol operations made through that context. The server reads the UID and GID out of the credential and makes all access control decisions based on them.<br>sudo apt update && sudo apt install -y autoconf automake libtool libkrb5-dev build-essential

git clone https://github.com/sahlberg/libnfs.git<br>cd libnfs/<br>./bootstrap<br>./configure --prefix=$HOME/libnfs-install<br>make -j$(nproc)<br>make install<br>gcc -fPIC -shared -o ld_nfs.so examples/ld_nfs.c -ldl -lnfs -I./include -L$HOME/libnfs-install/lib

export LD_LIBRARY_PATH=$HOME/libnfs-install/lib PATH=$HOME/libnfs-install/bin:$PATH

A note on...

home export mallory libnfs client credential

Related Articles