3

I was trying to learn how setuid works.

So I made a dummy program which just prints the current user:

#include<bits/stdc++.h>
using namespace std;


int main(){
    cout << system("id -a") << "\n";
    cout << system("whoami")  << "\n";
}

I compiled and created the executable my-binary under the user anmol:

-rwxrwxr-x 1 anmol anmol 9972 Feb  1 16:54 my-binary

Then, I set the setuid option using chmod +s:

-rwsrwsr-x 1 anmol anmol 9972 Feb  1 16:54 my-binary

If I execute it normally, I get the following output:

uid=1000(anmol) gid=1000(anmol) groups=1000(anmol),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),122(sambashare)
anmol

Now, if I change to another user using su user2, and then execute it, I get:

uid=1001(user2) gid=1001(user2) groups=1001(user2)
user2

And when I execute it using sudo ./my-binary, I get:

uid=1001(root) gid=1001(root) groups=1001(root)
root

As far as I understand, no matter how I run it, should I not get the 1st output everytime?

I checked other similar questions over here and some suggested me to check if the filesystem is mounted using nosuid option, so I executed mount | /dev/sda1 and got the output:

/dev/sda1 on / type ext4 (rw,relatime,errors=remount-ro,data=ordered)

Which means that this option is not enabled.

Any hints on why am I not getting the expected output?

  • 1
    It's definitely not a duplicate of the second and third questions, which are using set-UID on shell scripts, not on compiled binaries as in this question here. An answer to those involves wrappers that defeat the script interpreter's behaviour. An answer to this involves re-writing the program at hand to better do the simple task that its author wants, which is not in fact done by the answer to the first question. – JdeBP Feb 01 '20 at 14:04
  • @JdeBP happy to defer to you in this instance. 2nd and 3rd suggested links now removed – Chris Davies Feb 03 '20 at 11:34
  • @AndrewHenle Thanks. I did it just because its a dummy program. – Anmol Singh Jaggi Feb 04 '20 at 12:59

1 Answers1

3

The system(3) library function runs its command argument by passing it to /bin/sh -c, and the /bin/sh on Linux (either bash, dash or mksh) gives up any setuid or setgid privileges unless called with the -p option.

The bash manpage says:

If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied [...] the effective user id is set to the real user id.

With dash (the /bin/sh from Debian/Ubuntu), this is kind of new: it wasn't yet the case in Debian 9 Stretch (2017), and it's only a Debian-specific change, still not in the upstream sources as of 2020-02-04. bash has had this since its 2.X versions (first included in RedHat 7.X, 2000).

This is still not the case with other shells (ksh93, zsh, etc) or with the /bin/sh from other systems (OpenBSD, FreeBSD, Solaris; but not NetBSD where it was changed to work like bash).

If they have it, their privileged mode works differently than in bash: it's turned on by default when the shell is run in setuid mode, and you have to turn it off with set +p in order to have the shell drop the setuid privileges.


If you want to use system(), popen(), or run a shell or an executable shell script directly from your setuid binary, then you should give up any "split personality" and completely switch over to your real or effective credentials via setres[ug]id:

% cat a.cc
#include <unistd.h>
#include <stdlib.h>
int main(){
    uid_t euid = geteuid();
    setresuid(euid, euid, euid);
    system("id");
}
% c++ a.cc
% chmod u+s a.out
% ./a.out
uid=1002(fabe)
% su -c ./a.out
uid=1002(fabe)

If you want just to check that your binary really switched its effective credentials, do it directly, not by invoking an external program via system():

#include <iostream>
#include <unistd.h>
#include <pwd.h>
using namespace std;
int main(){
    uid_t euid = geteuid(); struct passwd *pw = getpwuid(euid);
    cout << "euid=" << euid;
    if(pw) cout << ", " << pw->pw_name;
    cout << endl;
}
  • I don't believe it's new. See the possible duplicate and the two other links I've suggested – Chris Davies Feb 01 '20 at 12:05
  • Yes, it's new. It wasn't the case with the default shell in Debian 9. And check the formatting of "new". –  Feb 01 '20 at 12:08
  • @roaima At this has NOTHING to do with setuid scripts. Read again, the OP created a setuid binary by compiling a C program. –  Feb 01 '20 at 12:10
  • It has everything to do with the system(3) call and /bin/*sh. – Chris Davies Feb 01 '20 at 12:12
  • 1
    I was talking about the irrelevant "setuid scripts" links that you dropped (second and third); they have nothing to do with system() or this bash feature. –  Feb 01 '20 at 12:15
  • I think we'll have to disagree. I believe they are all relevant to the issue as described: that putting a setuid bit on a wrapper script and then calling system('/bin/sh...') is not a sufficient way of getting setuid privilege carried through to the destination application. – Chris Davies Feb 01 '20 at 12:18
  • 1
    No, only the first link is relevant. But then your answer goes on and advocates sudo, which ruins everything. –  Feb 01 '20 at 12:32
  • This is most definitely not new. The Bourne Again and Korn shells have had the "privileged mode" mechanism for at least 30 years. The sh in Debian 9 is the Debian Almquist shell, which (in some versions) does not have this mechanism. (It has now.) On the other hand, this answer is better than roaima's inasmuch as the right thing here is to stop using system() for stuff that is dead simple to write in in-process code. – JdeBP Feb 01 '20 at 14:15
  • 1
    @JdeBP the change in dash is only in Debian, not upstream: if https://git.kernel.org/pub/scm/utils/dash/dash.git/log is really the upstream (it looks active). This is the bug report which resulted in the change: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=734869 –  Feb 01 '20 at 15:01
  • I explicitly wrote "Debian Almquist". Claiming that this is new, as this answer still erroneously does, is the illogic of claiming that something is new because it wasn't in a completely different thing to the thing where it has existed for >30 years. It's also ahistoric and Debian-myopic. There have been many cases where sh on Linux operating systems has been the Bourne Again shell. Switching to the Debian Almquist shell and losing this decades-old behaviour, then to regain it, is the relatively recent thing in the lifetime of Unix and Linux, which did not happen on (say) Fedora. – JdeBP Feb 02 '20 at 12:30
  • @JdeBP The only erroneous thing was the bogus claim that the privileged mode in the Korn shell ever worked the same as in the Bourne Again shell -- with the implication that system() would work the same if /bin/sh were ksh instead of bash or dash. That claim wasn't in my first version -- I had added it because of your comment ;-) Feel free to fact-check the corrected version. –  Feb 03 '20 at 09:55