I wanted to launch a bash script (read: bash not sh script) as a root not as the user calling it, however bash ignore setuid
on scripts, so I chose to write a very small script that takes a script/arguments and call it with setuid
set.
This worked well and I went even further to verify that the script has setuid
set on, executable and setuid()
called on the owner of the file and not as root, to avoid any misuse of the program and I ended up with the program below.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char **argv)
{
char *command;
int i, file_owner, size = 0;
struct stat status_buf;
ushort file_mode;
// Check argc
if (argc < 2) {
printf("Usage: %s <script> [arguments]\n", argv[0]);
return 1;
}
// Make sure the script does exist
if(fopen(argv[1], "r") == NULL) {
printf("The file %s does not exist.\n", argv[1]);
return 1;
}
// Get the attributes of the file
stat(argv[1], &status_buf);
// Get the permissions of the file
file_mode = status_buf.st_mode;
// Make sure it's executable and it's setuid
if(file_mode >> 6 != 567) {
printf("The file %s should be executable and should have setuid set, please chmod it 0106755.\n", argv[1]);
return 1;
}
// Get the owner of the script
file_owner = status_buf.st_uid;
// setuid
setuid(file_owner);
// Generate the command
for (i = 1; i < argc; i++) {
size += strlen(argv[i]);
}
command = (char *) malloc( (size + argc + 11) * sizeof(char) );
sprintf(command, "/bin/bash %s", argv[1]);
if (argc > 2) {
for (i = 2; i < argc; i++) {
sprintf(command, "%s %s", command, argv[i]);
}
}
// Execute the command
system(command);
// free memory
free(command);
return 0;
}
The exercise was not only to solve my problem, but it was also a way to get more into C, so what do you suggest? Is there anything I should improve?
-
3\$\begingroup\$ Next time don't double post, just use the flag tool on your original post and ask a moderator to migrate it to the suggested site. \$\endgroup\$Caleb– Caleb2011年06月09日 09:58:09 +00:00Commented Jun 9, 2011 at 9:58
-
\$\begingroup\$ I'll make sure to do that next time, thanks for the tip \$\endgroup\$kalbasit– kalbasit2011年06月09日 10:40:01 +00:00Commented Jun 9, 2011 at 10:40
3 Answers 3
Check for errors in
stat
,setuid
,fork
, andexecvp
, to name a few. If the exec fails in the child you should callexit
.Do you really want the
p
inexecvp
? This is not guaranteed to be the same as theargv[1]
you juststat
-ed. Ifargv[1]
isls
yourstat
will look for a file at./ls
and it will likely find the program in/bin
. I would useexecve
and either do thatPATH
lookup yourself or simply omit that part and require the user to specify a full path for something inPATH
(eg./bin/ls
instead of justls
).The
stat
+ observe state +exec
thing is a race condition. Another process can change the attributes on the file in that timing window. This may or may not be important to you. Given that this is a security-ish program I would say it may very well be.Instead of returning 0, you might want to return the child process's exit code (which you can get with
waitpid
.) You might also want to return nonzero when the functions I mention in #1 fail. This way a shell script or something calling you programmatically can determine success or failure.
-
\$\begingroup\$ That was very very helpful, thank you.. I would like to know more about #3, how could I prevent anyone from changing the file's attributes before calling exec, to avoid someone tempering with the attributes while the script is running? should I lock the file ? \$\endgroup\$kalbasit– kalbasit2011年06月14日 21:43:20 +00:00Commented Jun 14, 2011 at 21:43
-
\$\begingroup\$ I don't know if there is a good way around it for this. I don't think
flock
makes sense here either. \$\endgroup\$asveikau– asveikau2011年06月14日 23:49:36 +00:00Commented Jun 14, 2011 at 23:49 -
\$\begingroup\$ I added V3 in my original post, I modified it following #1. #2 and #4, I still have to figure out how to fix #3 \$\endgroup\$kalbasit– kalbasit2011年06月15日 12:42:57 +00:00Commented Jun 15, 2011 at 12:42
If I understand you correctly you would install your program (edit V3) like this
$ gcc -o edit_v3 edit_v3.c
$ ls -l edit_v3
-rwxrwxr-x 1 erik erik 7698 2012年04月15日 12:22 edit_v3
$ sudo chown root.root edit_v3
$ sudo chmod 4755 edit_v3
$ ls -l edit_v3
-rwsr-xr-x 1 root root 7698 2012年04月15日 12:22 edit_v3
But a user could then easily get root access
$ ls -l /bin/su
-rwsr-xr-x 1 root root 31116 2011年06月24日 11:37 /bin/su
$ ./edit_v3 /bin/su
# id
uid=0(root) gid=0(root) grupper=0(root),116(pulse),117(pulse-access)
It is difficult to write secure setuid programs. Over the years a lot of security vulnerabilities have been found in such program.
Instead of writing your own setuid program, you could also use sudo. You would then have to edit the configuration file /etc/sudoers
-
\$\begingroup\$ Actually it is one of the reasons why I abandoned the idea entirely, I wanted some way to provision server configuration (i.e Nginx conf files) but I finally settled on a more Capistrano/Chef solution as it's easier and well a lot safer than having any kind of security holes on the server. Thanks anyway :) \$\endgroup\$kalbasit– kalbasit2012年05月14日 17:52:12 +00:00Commented May 14, 2012 at 17:52
One thing I'd do is
change
sprintf(command, "%s %s", command, argv[i]);
to use strcat
while this does work on a number of implementations, it's not considered "safe"
refer https://stackoverflow.com/questions/1283354/is-sprintfbuffer-s-buffer-safe
-
2\$\begingroup\$ How about using
strncat()
? \$\endgroup\$Olivier Pons– Olivier Pons2011年06月11日 09:33:29 +00:00Commented Jun 11, 2011 at 9:33 -
\$\begingroup\$ thank you, I've already changed the script to use fork()/exec() instead of looping on the command variable myself, modified script is just above. \$\endgroup\$kalbasit– kalbasit2011年06月14日 21:44:11 +00:00Commented Jun 14, 2011 at 21:44