Hi Collin,
Thank you for the detailed explanation and for taking the time to look into 
this.
I wanted to provide a bit more information about the environment and what I 
observed during testing.
First, in my setup /tmp and /usr/bin are actually on the same filesystem:
$ df -T /tmp
Filesystem Type 1K-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 ext4 982862268 896113064 36748872 97% /
$ df -T /usr/bin
Filesystem Type 1K-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 ext4 982862268 896112852 36749084 97% /
So, in this case, mv -f /tmp/nmap /usr/bin/nmap should perform an atomic 
rename() rather than a cross-filesystem copy.
During testing I also experimented with different cp options. Originally the 
application used:
cp -f /usr/bin/nmap /tmp/nmap
I replaced it with:
cp -a /usr/bin/nmap /tmp/nmap
to see whether preserving metadata would change the behavior, but the race 
condition still allowed me to obtain a writable handle to /tmp/nmap as an 
unprivileged user and overwrite the beginning of the file.
In several runs I was able to modify the first bytes of the file while cp was 
copying it, which produced results such as:

  *
full nmap-sized file with the first bytes replaced by attacker-controlled data
  *
truncated files containing only the payload
  *
alternating ownership between root and the unprivileged user

Because /tmp is world-writable, I suspect the window may occur during the copy 
phase before cp restores the final permissions/ownership of the destination 
file descriptor.
Of course the main design issue is that the application stages a privileged 
binary in /tmp, which makes this race exploitable. However, the ability for 
another process to obtain a writable descriptor to the destination during the 
copy phase seemed surprising to me, so I wanted to confirm whether this is 
expected behavior from cp.
Please let me know if I may be misunderstanding something in the copy path.
Thank you again for your time and explanation.



[undefined]

Warm Regards,

Ajay SK

Firmware Security Researcher

Payatu Security Consulting Pvt Ltd.

Reach me on: 7397338492

This email and any files transmitted with it are confidential and intended 
solely for the use of the individual or entity to whom they are addressed. If 
you have received this email in error, please notify the system manager. Please 
note that any views or opinions presented in this email are solely those of the 
author and do not necessarily represent those of the company. Finally, the 
recipient should check this email and any attachments for the presence of 
Malwares. The company accepts no liability for any damage caused by any Malware 
transmitted by this email.

________________________________
From: Collin Funk <[email protected]>
Sent: 08 March 2026 14:34
To: Ajay S.K <[email protected]>
Cc: [email protected] <[email protected]>
Subject: Re: bug#80572: [BUG] Privilege escalation via cp trying to replace 
file contents using root privileges

[You don't often get email from [email protected]. Learn why this is 
important at https://aka.ms/LearnAboutSenderIdentification ]

CAUTION: This email originated from outside of the organization. Do not click 
links or open attachments unless you recognize the sender and know the content 
is safe.

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

Reply via email to