December 8th, 2007, 13:54
I'm working on some PHP code to manage parts of a server. Mainly creating a user, setting the user's groups, and creating folders in the user's home with the correct permissions. Of course, I don't want to set apache to be run by root, and was wondering if anyone had any ideas to go about this without making the apache server an internet time-bomb.

I guess I could use sudo without a password for www, but want to hear any other ideas.

December 8th, 2007, 18:02
Not sure if this helps atall:

December 9th, 2007, 15:04
KK, there are a few possible safe solutions to this.

I would first look at creating a small setuid binary applet that has the capability of creating the user for you. The key here is that the app must be extremely careful about how it goes about the process (verify that you aren't being tricked into following any symlinks for example; not be susceptible to remove/create race conditions). This binary will be called from your PHP scripts, become root, do the potentially dangerous operations first, then give away its root-ness to do any further operations like creating sub-folders -- ie, after creating the user "foo", become user foo to do as many things as possible.

Verify all arguments passed for bad strings (eg: usernames should only contain letters and digits and enforce that).

If you really want to be paranoid, verify in the app that you are being called by the user that your PHP scripts run as. I would also chroot/jail the app to be able to only run in the /usr/home/ tree so it can't be coerced to do damage elsewhere.

Another solution is to maintain a user-creating daemon that runs as root and design a trivial socket-based protocol to pass in usernames/groups. That's the Postfix way of firewalling privilege.

The really important thing to keep in mind with any setuid programs is: single-purpose. Don't create a setuid app that calls other scripts as root--that's asking for trouble. Don't try to create some all-purpose swiss-army-knife setuid tool. Don't set setuid privs on any kind of script.

Oh, and make sure your setuid binary discards its environment before going to work. PHP is notorious for passing environment strings from Apache and the whole GET/POST operation on to forked processes. You don't want to follow a $PATH that's been corrupted. You should even statically link the binary so you can't have the path for shared libraries affect you.

December 9th, 2007, 15:23
Thanks for the replies guys!

BMW, I think I will give that a shot. Yes, I'm paranoid, so every precaution will be taken. My initial idea was to hold the user/group/pass in a MySQL database, so I can do a lot of the functionality PHP-side before the arguments are sent to the exec() (I will be using safe-mode of course). So, before the args are passed, I'll make sure to check for bad chars, malicious content, etc. Thanks for the input, and I'll finish up the PHP side to make sure I limit as much as possible to be sent to the binary. Of course then, I'll work on the binary. ;)

December 9th, 2007, 15:41
Not to belabor the point, but make sure your setuid binary rechecks all arguments passed to it. Ie: don't assume that the PHP script did all the sanity testing so you don't need to do it again. That's where many security problems arise. Your setuid app should assume that anyone could be running it and trying to shove random input at it.

(It's not uncommon for databases to get improper data edited into them via various SQL-injection paths, then for an innocent script to get called which happily accepts what's in the db and happily allows someone to get root.)


December 9th, 2007, 15:58
So true. I will make a note to add sanity checks on both. It did sound like PHP could do it, but then I thought about it after you last post, and really, I should see where data could be malformed. Thanks again. :)

December 14th, 2007, 07:09
Bruce, do you have any examples of setuid being used in C (I'm guessing that would be the best way)? I'm not seeing anything on Google, but sometimes, it's just hard for me to find things like this when there are a million forum posts about something completely different, but has the keyword 'setuid'.

EDIT: I tried the code in the manpage, but it wont even compile due to missing deps (not sure what to include). Here is that code:

int fd;
/* ... */

fd = open("/path/to/sensitive/data", O_RDWR);
if (fd == -1)
err(1, "open");

* Set close-on-exec flag; see fcntl(2) for more information.
if (fcntl(fd, F_SETFD, FD_CLOEXEC) == -1)
err(1, "fcntl(F_SETFD)");
/* ... */
execve(path, argv, environ);

December 14th, 2007, 12:17
An excellent example is the daemon startup code in Postfix. If I recall accurately, each of the daemons (eg "smtpd") starts as a root process, opens some config files and lookup databases, optionally chroots, then drops privs and runs as the user "postfix". There's a common template source that each of the daemons uses that contains its main(). [I don't have the sources handy at the mo' -- I don't work on it daily anymore. :-) ]

That pseudocode above is emphasizing the importance of being careful about access rights. Mainly, it's missing an #include <fcntl.h> to resolve some defines, and even still it doesn't define path, argv or environ which normally come by way of main().

I'll see if I can find a more succinct example to post here. If I have time I'll unpack Postfix and snarf the code bits I'm thinking of from there.

December 14th, 2007, 14:46
Thanks a ton. I'll try to figure out what I can, but I'd love to see what you are talking about. :)

December 17th, 2007, 10:12
I couldn't find the code fragment I was thinking of but I found some discussions on the topic of setuid(). Seems that the canonical method is: make your app setuid root; do your stuff that requires root permissions when your app starts; then when you want to drop back to being the person who invoked the app (ie not root anymore), you do this:

err = setuid(getuid());

Interestingly there's a hack involving ACLs (eg in Linux) that can prevent this from working and was one of the many sendmail vulnerabilities fixed over the years. So you must check the error return from setuid() to see if it worked, plus as a safety check, attempt to do setuid(0) and test if that succeeded -- if it did, you are still root and should exit right away.

See this doc ...

Hope that helps, KK.

December 18th, 2007, 00:04
Awesome, thank you. I'll mess with that for a bit. :)