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
