Solaris NFS Server XDR handling vulnerability
Release date: 27-06-2009
1. Summary
There is vulnerability in Solaris 10 kernel NFS implementation resulting in an
attacker being able to perform a denial of service attack on an NFS server.
One crafted UDP packet is sufficient to send to port 2049 to trigger the kernel
panic and core dump. Remote code execution is unlikely though.
This vulnerability was only tested on Solaris 10 and 11 x86, but it's likely
that it also affects other platforms/versions. The source code references come
from the Nevada project cvs web page dated 11/03/2007 (http://cvs.opensolaris.org).
2. Details
There is a bug in the Solaris kernel NFS ACL code executed when client calls
setfacl() (possibly also getfacl() and some others if there are any), that may
be exploited to perform a denial of service attack on the server.
When calling setfacl(), the ACL structure is passed as an argument following
the file handle. When the packet gets opened in Ethereal/Wireshark, two fields
describing the number of ACL entries can be seen:
- ACL count (nfsacl.aclcnt, the structure member)
- Total ACL entries (the XDR's protocol array length).
Under normal condition, they are equal. But if one of them gets modified, the kernel will panic while trying to free memory chunk using wrong size paramater passed to kmem_free() (size that doesn't match the corresponding kmem_alloc()). When allocating kernel memory with kmem_alloc(chunk_addr, size1), and freeing it with kmem_free(chunk_addr, size2), assuming size1 != size2, the system will panic with the crash message similar to the below:
NOTICE: Failed to decode arguments for ACL version 3 procedure ACL3_SETACL client clientname.domain panic[cpu0]/thread=d57a8e00: vmem_hash_delete(dac04690, d5430600, 786492): bad free d456a970 genunix:vmem_hash_delete+d0 (dac04690, d5430600,) d456a9ac genunix:vmem_xfree+2b (dac04690, d5430600,) d456a9c0 genunix:vmem_free+1e (dac04690, d5430600,) d456a9f4 genunix:kmem_free+36 (d5430600, c003c) d456aa34 genunix:xdr_array+f6 (d49cd484, d456ab20,) d456aa7c nfs:xdr_secattr+69 (d49cd484, d456ab18) d456aa98 nfs:xdr_SETACL3args+4f (d49cd484, d456aad0) d456aab0 rpcmod:svc_clts_kfreeargs+29 (d49cd400, fa19438c,) d456ad10 nfssrv:common_dispatch+6ce (d456ad8c, d49cd400,) d456ad34 nfssrv:acl_dispatch+1f (d456ad8c, d49cd400) d456adc4 rpcmod:svc_getreq+158 (d49cd400, dad9e2c0) d456ae0c rpcmod:svc_run+146 (d57a9960) d456ae2c rpcmod:svc_do_run+6e (1) d456af84 nfs:nfssys+3fb (e, d2940fc8, d08e, )
3. Code analysis
Below is the fragment of xdr_secattr() function code:
bool_t xdr_secattr(XDR *xdrs, vsecattr_t *objp) { uint_t count; if (!xdruint(xdrs, &objp->vsa_mask)) return (FALSE); [1] if (!xdr_int(xdrs, &objp->vsa_aclcnt)) return (FALSE); [2] if (objp->vsa_aclentp != NULL) [3] count = (uint_t)objp->vsa_aclcnt; else [4] count = 0; [5] if (!xdr_array(xdrs, (char **)&objp->vsa_aclentp, &count, NFS_ACL_MAX_ENTRIES, sizeof (aclent_t), (xdrproc_t)xdr_aclent)) return (FALSE); [6] if (count != 0 && count != (uint_t)objp->vsa_aclcnt) return (FALSE);
That function is executed two times when the SETACL request has come in.
First, common_dispatch() function calls it via the SVC_GETARGS macro with the
request to decode the arguments wrapped by XDR. At [1], the vsa_aclcnt field
is filled with the integer value corresponding to ACL count (see section 2).
But at [2], the vsa_aclentp structure is still NULL, so the freshly filled
vsa_aclcnt will not be used as the ACL count as it would have been at [3],
but the count variable becomes 0. Then, the count variable is used at
xdr_array() call at [5], but is ignored anyway, because xdr_array() function
reads the array using the array length (see Total ACL entries at in section 2).
Then it fills up the vsa_aclentp pointer. This value should normally be the
same as ACL count, but is represented by the different four octets in the
packet.
Assume the first value has been modified to be different than the second,
let's say legitimate value of ACL Count was 5, but has been modified to 65541.
In this case, the function will return at [6], causing the control to be given
back to the common_dispatch() function. As result was FALSE, the
common_dispatch() function will send "NOTICE: Failed to decode arguments..."
message that can be seen above the crash dump in section 2, and will call
SVC_FREEARGS macro to free the memory allocated by the garbled arguments.
The control gets again to the xdr_secattr() function. But the structure
at [2] has already been filled up, so the count becomes 65541 instead of 5.
The xdr_array() is called again, and it panics at the mem_free() below, because
the nodesize is now different for tha same chunk:
xdr_array.c#115 ::
/* * the array may need freeing */ if (xdrs->x_op == XDR_FREE) { mem_free(*addrp, nodesize);
4. Exploit
The malicious UDP packet makes the system call SETACL with the five ACLs. The ACL
count value (see section 2) was modified from 5 to 65541. This will cause the system
to crash. The exploit is not attached in this advisory.
5. Fix
There doesn't seem to be a fix available for this issue for now. The possible
way o fixing would probably be to assign (uint_t)objp->vsa_aclcnt a real array
length somewhere between [5] and [6] in xdr_secattr().
UPDATE: the issue has been fixed after reporting to the vendor: http://sunsolve.sun.com/search/document.do?assetkey=1-26-102965-1