Hi Ajay,
Ajay S.K via GNU coreutils Bug Reports <[email protected]> writes:
> Dear Coreutils Maintainers,
> I am a penetration tester assessing a desktop endpoint management
> application that runs with root privileges. One of its features
> replaces the existing nmap binary with its own version. The
> implementation performs the following steps:
>
> 1.
> cp -f /usr/bin/nmap /tmp/nmap
> 2.
> mv -f /tmp/nmap /usr/bin/nmap
>
> Both commands are executed through popen.
> While testing this behavior as an unprivileged user, I attempted to
> exploit a race condition by monitoring /tmp/nmap and obtaining a write
> handle using open() when the file size exceeded a certain threshold.
> After several attempts, I was able to overwrite the beginning of the
> file with the following payload (29 bytes):
> #!/bin/bash
> id > /tmp/abcdef
> When the endpoint agent later invoked nmap, the payload executed and wrote
> the output of id to /tmp/abcdef.
> During multiple race attempts, I observed several different outcomes.
> In some cases the file size was truncated to the payload size and
> owned by the unprivileged user. In other cases the owner was root but
> the payload remained. Sometimes the final file had the full nmap size
> but with the first 29 bytes overwritten by user-controlled data.
> Ownership also alternated between root and the racing user in
> different runs.
> I also noticed the file permissions changing unpredictably during different
> race attempts (e.g., 0644, 0640, 0400).
> In another scenario, if /tmp/nmap was pre-created by the unprivileged
> user with a small file and the application was triggered, I was able
> to modify the file immediately when it was written. In this case the
> file consistently remained owned by the unprivileged user.
> My system is running:
> Linux pwn-land 6.19.6 #1 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
> I reproduced the same behavior on an older kernel and again after upgrading
> to the latest stable kernel.
> I had a couple of questions:
>
> 1.
> Is it expected behavior that cp does not update permissions when
> overwriting an existing file created by a low-privileged user? If a
> privileged application overwrites such a file and later executes it,
> could that lead to a privilege escalation scenario?
> 2.
> Why does this race produce multiple observable outcomes (different
> ownership, sizes, and permissions)? Are there multiple internal stages
> or code paths in cp/mv that could explain these different states?
I'm not sure I fully understand your situation, but I will try to give
you some information that is hopefully helpful. I am not saying that you
are up to no good, but I hope you understand that some people might not
be brave enough to run your programs as root. :)
I assume that your /usr/bin and /tmp are on separate mount points, as
that seems to be how things are typically set up. Typically 'mv' uses
renameat which is atomic and avoids any potential races. This fails when
moving across mount points, likewise for cloning if your file system
supports it and copy_file_range() (these are also used by 'cp').
Therefore, we have to fallback to the old reliable 'read' and 'write'.
The behavior you see is surprising to me. You mention the modes 0644,
0640, and 0400 which all are unwritable for other users. You can see how
'mv' handles permissions during copying using 'strace':
$ install -m 777 /dev/null a
$ echo hello >> a
$ strace -P a -P /tmp/b -e trace='/.*(open|chmod|read|write)' \
mv -f a /tmp/b
openat(AT_FDCWD, "/tmp/b", O_RDONLY|O_PATH|O_DIRECTORY) = -1 ENOTDIR (Not a
directory)
openat(AT_FDCWD, "a", O_RDONLY|O_NOFOLLOW) = 3
openat(AT_FDCWD, "/tmp/b", O_WRONLY|O_CREAT|O_EXCL, 0700) = 4
read(3, "hello\n", 262144) = 6
write(4, "hello\n", 6) = 6
read(3, "", 262144) = 0
fchmod(4, 0100777) = 0
+++ exited with 0 +++
$ install -m 600 /dev/null a
$ echo hello >> a
$ chmod 400 a
$ strace -P a -P /tmp/b -e trace='/.*(open|chmod|read|write)' \
mv -f a /tmp/b
openat(AT_FDCWD, "/tmp/b", O_RDONLY|O_PATH|O_DIRECTORY) = -1 ENOTDIR (Not a
directory)
openat(AT_FDCWD, "a", O_RDONLY|O_NOFOLLOW) = 3
openat(AT_FDCWD, "/tmp/b", O_WRONLY|O_CREAT|O_EXCL, 0600) = 4
read(3, "hello\n", 262144) = 6
write(4, "hello\n", 6) = 6
read(3, "", 262144) = 0
fchmod(4, 0100400) = 0
So, we first create the new file so the current user can read and write
to it. After we finish copying the data we set the permission to the
source files mode, which may or may not be more permissive.
The case for 'cp' is a bit more complex and depends on the --preserve
option being used. If none are used then the source files mode is used.
Collin