Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

Not understanding the code to drop privileges in perlsec

by Nocturnus (Beadle)
on Feb 21, 2024 at 20:56 UTC ( [id://11157831]=perlquestion: print w/replies, xml ) Need Help??

Nocturnus has asked for the wisdom of the Perl Monks concerning the following question:

Dear seekers of wisdom,

I have found the following piece of code here:

0 use English; 1 my @temp = ($EUID, $EGID); 2 my $orig_uid = $UID; 3 my $orig_gid = $GID; 4 $EUID = $UID; 5 $EGID = $GID; 6 # Drop privileges 7 $UID = $orig_uid; 8 $GID = $orig_gid; 9 # Make sure privs are really gone 10 ($EUID, $EGID) = @temp; 11 die "Can't drop privileges" 12 unless $UID == $EUID && $GID eq $EGID;

I have slightly shortened it and have added line numbers.

Although I have tried very hard for several hours, I am not able to understand how it works. I believe that I understand it from line 9 on: Obviously, in line 10, we try to gain back EUID and EGID. This should not be possible if we really have dropped privileges. It it was possible, EUID now is different from UID or EGID now is different from GID. That means that if the conditions in line 12 are true the assignment in line 10 has failed, which is what we expect since we have already dropped privileges.

My main problem are lines 2 and 3 in conjunction with lines 7 and 8. Focusing on the user id only, we here have basically the sequence 2. $orig_UID = $UID followed by 7. $UID = $orig_UID. That does not make any sense IMHO, unless $UID is altered somewhere in between. The only place where this could happen is probably line 4. However, I can't see how the statement $EUID = $UID could alter $UID.

My tests seem to confirm that point of view. I have made a test environment in Linux (Debian bullseye) where I can run the script setsuid-root or setsuid-other_user, and have inserted print statements after every assignment. Regardless of what my real user id was, and regardless of the setsuid of the script, I never encountered a situation where $UID and $orig_UID were different immediately before executing line 7.

Please note that I am aware that it is normally not sufficient to set the setuid flag on script files to actually make the script run as the respective user; this is because the interpreter usually does not care about that flag. However, I am using suid-wrapper to run the script setsuid-root or setsuid-other_user.

So I guess that I'd like to know what the "# Drop privileges" part (lines 7 and 8) actually effect and how they work.

Thank you very in advance, and best regards ...

Replies are listed 'Best First'.
Re: Not understanding the code to drop privileges in perlsec
by hippo (Bishop) on Feb 22, 2024 at 10:35 UTC

    I agree with your analysis. The example given in perlsec is at best confusing. Honestly, I am not convinced it would pass a code review.

    • If @temp, $orig_uid and $orig_gid are just there to hold the starting values, why is one an array and the other a set of 2 scalars? I think this is because the $GID contains not only the primary group but also all the secondary groups of the real user too and because it is a dualvar and therefore would unroll when the array were evaluated. I'd much prefer to have had that commented.
    • On the last line, there a numeric equality test between the UIDs and a string equality test between the GIDs. Again this is probably because of the potential non-numeric nature of $GID. But this is also uncommented in the code so we can only assume.
    • Like you, I don't see why the values of $UID and $GID are explicitly reset. Given the logic, this appears to be a no-op. Unless it is there to get around the "saved UID" - but again if that's the case it really should have a comment to say so.
    • The original code in perlsec uses a bareword filehandle which is generally discouraged nowadays.

    So, the code could work correctly and maybe each line is there for a purpose but it is very far from clear as written. It would be useful if others more familiar with the internals of all this could chip in and correct any of my suppositions here which might well be wrong as it is not my area of expertise.


    🦛

      I think your claim that it's a no-op is wrong.

      setuid() sets the effective user ID of the calling process. If the calling process is privileged (more precisely: if the process has the CAP_SETUID capability in its user namespace), the real UID and saved set-user-ID are also set.

      Under Linux, setuid() is implemented like the POSIX version with the _POSIX_SAVED_IDS feature. This allows a set-user-ID (other than root) program to drop all of its user privileges, do some un-privileged work, and then reengage the original effective user ID in a secure manner.

      It was suggested to the OP that they to trace the ids throughout the program when they posted the exact same question on StackOverflow. You should also adopt that recommendation!

        Thank you very much!

        On SO I have been told to set the script suid or sgid, which leads to nowhere because the interpreter does not care about those flags. I admit that I should have written a further comment that describes my solution to that problem and my new findings.

        However, in the first post here, I have described that I have amended the code with print statements and what happened then. From the post:

        "My tests seem to confirm that point of view. I have made a test environment in Linux (Debian bullseye) where I can run the script setsuid-root or setsuid-other_user, and have inserted print statements after every assignment. Regardless of what my real user id was, and regardless of the setsuid of the script, I never encountered a situation where $UID and $orig_UID were different immediately before executing line 7."

        And that's still my problem. After I had learned how to execute a script suid / sgid, I have conducted a lot of tests, but could not construct a situation where lines 7 and 8 actually would effect anything.

        I've got an additional question:

        Before posting, I had read the man pages of setuid() and its colleagues, and I guess I have understood them.

        However, I couldn't find a statement anywhere about what function Perl actually uses to perform assignments to $(E)[U|G]ID. perlvar only tells us that it uses a syscall, which made me believe that it is not one of the setuid() functions. The latter are not syscalls; they are from the C library (please correct me if I am wrong).

        But from your post I got the impression that Perl uses indeed the setuid() functions to perform assignments to the user and group variables. Did I understand this correctly?

      Thank you very much for your support!

      I am sure that your analysis regarding the comparison operators is correct. perlvar states that the group ids actually are strings (and thus should be compared using eq in Perl) and that user ids are numbers (and thus should be compared using ==).

      That they use an array one time and two separate variables the other time does not worry me too much. Of course I agree with you that there should be much more comments.

      The main problem is that dropping privileges is seemingly is a no-op, but I am quite sure that actually it isn't one. For the life of me, I don't understand the sense, and even worse, I have conducted a lot of tests with setgid / setuid flags, but couldn't construct a situation where the code under # Drop privileges was not a no-op.

Re: Not understanding the code to drop privileges in perlsec
by hv (Prior) on Feb 23, 2024 at 01:13 UTC

    I'm not sure if this is already clear, but the upper-case variables in the example are all special variables: reading from them or writing to them invokes magic in the perl interpreter. Lines 4, 5, 7, 8 and 10 are all invoking "set" magic on one or two of those variables.

    Or at least they would be if the code was prefaced with use English;. The special variables are usually called the "punctuation variables", since they have names like $<. The English module exists to give them more readable aliases like $UID. If you do not preface the code with use English;, then those names are just plain variables and the example code is essentially an extended no-op.

    I don't know much about how these things work at the OS level, but within Perl the magic happens within mg.c:Perl_magic_set, in a case statement based on the names of the underlying punctuation variables - perlvar will tell you that $UID, $EUID, $GID, EGID are the English names for $<, $>, $(, $) respectively.

    So for assignment to $UID, the relevant code is in case '<', where we see that it will use the first available of setruid(uid), setreuid(uid, -1), setresuid(uid, -1, -1) or (with more caveats) setuid(uid).

    However, there is a wrinkle: if these variables form part of the left hand side of a list assignment, as in line 10 of your example code, additional effort is made to do multiple changes atomically. This is done by setting the interpreter variable PL_delaymagic = DM_DELAY in the handling of list assignment by pp_hot.c:PP(pp_aassign), then in Perl_magic_set setting more bits in PL_delaymagic to record what needs doing, then finally calling pp_hot.c:S_aassign_uid to do the simultaneous assignments.

    In this case that's all a bit of a waste of effort, since the point is to assign to ($UID, $EUID) or to ($GID, $EGID) simultaneously, but the list assignment in the example code is to ($EUID, $EGID). It does mean, though, that the order of attempts is different: it is now the first available of setresuid(), setreuid(), setruid/seteuid(), setuid().

    (That the order it chooses is different for scalar or list assignment may be a subtle bug; I've opened issue #22018 to get that checked out.)

      Thank you very much for the in-depth explanation.

      I have to apologize that I left out the use English; statement at the begin of the example code. Of course, it is in my test program :-) It fell off the table during copy-and-paste.

      I also would like to state that my test scenario works. For example, when I make the script setuid-root at the file system level, then log in as a normal user with user id 1016 and execute the script, $EUID is 0 and $UID is 1016 at the beginning (before executing line 4).

      It also works if I make the script setuid-<user with id 1015>, log in with user id 1016 and run the script. Then $EUID is 1015 and $UID is 1016 at the beginning (before executing line 4).

      With another scenario, I have found a bug in the meantime (I can't judge if Perl or if the script is the culprit). I'll describe it in a separate post below.

      Hoping to gain some deeper understanding, I have studied the man pages of the library functions you mentioned. But I seem to be blind: I still can't understand the sense of lines 7 and 8. They would only make any sense if the assignments in lines 4 and 5 would change not only the left hand side, but also the right hand side.

      IMHO, dropping privileges actually happens in lines 4 and 5, not in lines 7 and 8 as the comment implies. What lines 7 and 8 do will probably remain the author's secret.

      Anyway, due to the bug I have found, I am unsure whether I should trust that script at all. Too sad that it's just perlsec that shows a buggy and totally incomprehensible script as reference for how to securely drop privileges.

        if I make the script setuid-<user with id 1015>, log in with user id 1016 and run the script

        My understanding of the setuid situation is that the only safe way to actually make a script setuid is to not do that, and use the sudo tool. setuid with a scripting language has a much larger attack surface due to the interpreter using various environment variables like 'PERL5OPT' that the linux loader won't be aware of (it has some protections for LD_LIBRARY_PATH, but it can't know about much more than that) Making something setuid without opening up the ability to run arbitrary code as the other user is fairly difficult. sudo helps with this by sanitizing the environment before the script interpreter gets invoked.

        Maybe this is what you meant elsewhere by "the right way to make a script setuid" but I didn't see any mention of sudo, so I figured I'd warn you at least.

Re: Not understanding the code to drop privileges in perlsec
by talexb (Chancellor) on Feb 22, 2024 at 05:28 UTC

    The documentation you probably need is here, and is referred to in the English module. The leading E is Effective; U is User and G is Group. ID is the Linux user or group id. The documentation link should tell you what you need to know about the difference between the effective and the real IDs.

    Alex / talexb / Toronto

    Thanks PJ. We owe you so much. Groklaw -- RIP -- 2003 to 2013.

      Thank you very much!

      Of course, I have studied the docs before asking here, and have read the relevant portions of perlvar. The leading E and U and G were clear so far :-)

      However, the problem is that perlvar does not state how an assignment to these variables is implemented behind the scenes. At the API level, there is a myriad of functions that could be used, some of them only altering EUID / EGID, some of them (potentially) altering UID / GID at the same time, some of them exchanging EUID vs UID, and so on. perlvar also tells that it uses a syscall to implement the assignments, but does not tell which syscall that is.

      Also, despite intensive tests, I have never encountered a situation where $UID / $GID were different from $orig_UID / $orig_GID at line 7, so I don't understand the sense of this assignment and how it drops privileges.

          Also, despite intensive tests, I have never encountered a situation where $UID / $GID were different from $orig_UID / $orig_GID at line 7, so I don't understand the sense of this assignment and how it drops privileges.

        That may be more of a Linux/Unix question than a Perl question -- and it's nothing I can help you with. :) Good luck!

        Alex / talexb / Toronto

        Thanks PJ. We owe you so much. Groklaw -- RIP -- 2003 to 2013.

Re: Not understanding the code to drop privileges in perlsec
by choroba (Cardinal) on Feb 22, 2024 at 10:12 UTC
    Crossposted to StackOverflow.

    Crossposting is permitted, but it's considered polite to inform about it to prevent people not attending both sites from wasting their time answering a question already answered elsewhere.

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]

      You are right. I have asked that question on SO a few days ago (in slightly different form). I thought that crossposting would be OK because stackoverflow.com and perlmonks.org are different sites. I'll do it right next time.

Re: Not understanding the code to drop privileges in perlsec
by Nocturnus (Beadle) on Feb 23, 2024 at 18:53 UTC

    I have found a bug with this script, and I am looking forward to whether people consider this a bug in Perl or a bug in the script. Anyway, before going into the details I would like to state my Perl version: 5.32.1

    I have amended the original script with print statements. The following is the same script as in my first post, but with those statements included and with the last two lines removed:

    #!/usr/bin/perl use English; my @temp = ($EUID, $EGID); my $orig_uid = $UID; my $orig_gid = $GID; print('$UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: '.$UID.", ".$GI +D.", ".$EUID.", ".$EGID.", ".$orig_uid.", ".$orig_gid."\n"); print('@temp: '.$temp[0].", ".$temp[1]."\n\n"); $EUID = $UID; $EGID = $GID; print('$UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: '.$UID.", ".$GI +D.", ".$EUID.", ".$EGID.", ".$orig_uid.", ".$orig_gid."\n"); print('@temp: '.$temp[0].", ".$temp[1]."\n\n"); # Drop privileges $UID = $orig_uid; $GID = $orig_gid; print('$UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: '.$UID.", ".$GI +D.", ".$EUID.", ".$EGID.", ".$orig_uid.", ".$orig_gid."\n"); print('@temp: '.$temp[0].", ".$temp[1]."\n\n"); # Make sure privs are really gone ($EUID, $EGID) = @temp; print('$UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: '.$UID.", ".$GI +D.", ".$EUID.", ".$EGID.", ".$orig_uid.", ".$orig_gid."\n"); print('@temp: '.$temp[0].", ".$temp[1]."\n\n");

    Then I have setup the following test scenario: I set the script suid to the user with id 1015, log in as root and let the script run. This is quite unusual I guess, but since we are testing, this case must be included. The script produces the following output:

    $UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: 0, 0 0, 1015, 1001 0, +0, 0 0 @temp: 1015, 1001 0 $UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: 0, 0 0, 0, 0 0, 0, 0 0 @temp: 1015, 1001 0 $UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: 0, 0 0, 0, 0 0, 0, 0 0 @temp: 1015, 1001 0 $UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: 0, 0 0, 1015, 0 0, 0, +0 0 @temp: 1015, 1001 0

    Please note the last block of the output. We see clearly that the assignment ($EUID, $EGID) = @temp; fails, although we are still root at this moment. Perl has changed only $EUID, not $EGID.

    There are two possible conclusions: Either it is generally wrong to assign UID, EUID, GID or EGID in list context (then the bug would be in the script), or Perl should handle that situation correctly (then the bug would be in Perl).

    The script works correctly if I replace

    ($EUID, $EGID) = @temp;
    by
    $EGID = $temp[1]; $EUID = $temp[0];

    The script then produces this output:

    $UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: 0, 0 0, 1015, 1001 0, +0, 0 0 @temp: 1015, 1001 0 $UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: 0, 0 0, 0, 0 0, 0, 0 0 @temp: 1015, 1001 0 $UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: 0, 0 0, 0, 0 0, 0, 0 0 @temp: 1015, 1001 0 $UID, $GID, $EUID, $EGID, $orig_UID, $orig_GID: 0, 0 0, 1015, 1001 0, +0, 0 0 @temp: 1015, 1001 0

    The last block of output shows that Perl now has changed $EUID as well as $EGID.

    I have read somewhere that we always should change $EGID before $EUID; maybe that's the problem with the assignment in list context. However, I also have replaced @temp = ($EUID, $EGID) by @temp = ($EGID, $EUID) at the begin of the script and ($EUID, $EGID) = @temp; by ($EGID, $EUID) = @temp; at the end of the script. This didn't effect anything; Perl still changed only $EUID, not $EGID.

      I have read somewhere that we always should change $EGID before $EUID

      Can you track down where you read that?

      It isn't obvious to me that this is a bug in perl: in the list-assignment case, we drop privileges (by setting a non-root EUID) before trying to set EGID; it does not seem unreasonable to me that the attempt to set EGID then fails.

      However I can also see the argument (particularly if you can find the above reference) that for list assignment perl should choose to modify groups before users, either always or only in specific (documented) cases.

      And certainly if perl is correct to treat this the way it does, then it is wrong to recommend the code it does in perlsec.

      Sadly I cannot readily test any of this, since on my Ubuntu system the set[ug]id bits are ignored for scripts. I do think it is worth opening an issue on github for this specific aspect of the perlsec code.

        Oops, I didn't intend to be logged out when I posted that.

        Can you track down where you read that?

        I can't remember for sure, but it was eventually here. There is a comment in the middle of his code that states this. I believe it is not a constraint that's officially documented in an API; rather, it is logical, because we can't change the GID if we are already a non-privileged user (that is, if we have already changed our EUID away from root or from the suid-user, respectively).

        I agree with you that this probably is not a bug Perl, but it's a bug in the script, which, after all, is probably used by a lot of people because it claims to show how to securely drop privileges, preventing them from coding own solutions because nobody wants to have errors in security related code.

        in the list-assignment case, we drop privileges (by setting a non-root EUID) before trying to set EGID; it does not seem unreasonable to me that the attempt to set EGID then fails.

        This is what I initially thought, too. But as mentioned above, I have reversed the order of $EUID and $EGID in the two list context assignments at the begin and the end of the script for testing. This didn't change anything. I can't explain this, because I would have expected that list assignments go from the left to the right on both sides, and thus that the group id assignment would happen before the user id assignment after that change.

        Perhaps Perl tries to be overly smart; in every case, for whatever reason, it insists on changing the user id first regardless of the variable order in the list. Anyway, I am not that deep in Perl, so I can't tell whether it guarantees a certain order of single assignments in a list assignment at all. To make it work as expected, I had to write the two assignments each on its own in scalar context in the correct order.

        Sadly I cannot readily test any of this, since on my Ubuntu system the setugid bits are ignored for scripts.

        In case you are interested, I describe how I test it. Actually, I was in the same situation as you: Debian bullseye (or the interpreters, respectively) also ignores those bits on scripts. Plus, most of the suid-wrappers contain bugs and don't work as expected as well.

        However, I have found that one of those wrappers works like a charm: suid-wrapper. It is also available as a snap (Ubuntu can handle snaps, correct?), but downloading the source and compiling it really was a matter of two minutes, and it worked out of the box. No annoying study of man pages ... just copy the example command from the Github page and go on.

        In case of a Perl script without command line parameters (like our example script), it even simpler. Just execute sudo suid-wrapper --output root_pl test.pl This generates the stub root_pl, which is an executable that runs test.pl. The set[u|g]id flags on the script test.pl itself are still ignored, but you can change the owner of the stub executable root_pl to whatever user and group you want and then set the suid and sgid flags as you like.

        Furthermore, you can alter the actual script test.pl afterwards without generating a new stub, as long as you don't change its name of course. This is extremely nice.

        As a final remark, suid-wrapper by default sets the stub it generates suid-root and setgid-root, but as mentioned above, you can change the owner and group as you like. Just remember to make such changes to the stub, not the script. And please remember that Ubuntu probably resets suid and sgid flags on the stub if you change its owner or group. At least, this happens on Debian (and has driven me mad because I didn't notice it at once). It is a safety mechanism I guess.

Re: Not understanding the code to drop privileges in perlsec
by LanX (Saint) on Feb 22, 2024 at 16:56 UTC
    It certainly looks redundant, I suppose (or hope) it's in order to make it work on all platforms.

    If course testing it on every platform to see why it does what it does is difficult to achieve.

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    see Wikisyntax for the Monastery

      It certainly looks redundant, I suppose (or hope) it's in order to make it work on all platforms.

      Probably not. It surely should work on anything that looks like Unix / POSIX, surely on Linux and the *BSDs, probably also Mac OS X. On DOS, you permanently have full privileges, no way to drop privileges. The Win32 API works very differently (I think privileges are somehow tied to some kind of token). I have no idea how OS/2, Amiga, Risc OS, VMS, VOS, z/OS and the other platforms from perlport handle users and privileges.

      Alexander

      --
      Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
Re: Not understanding the code to drop privileges in perlsec
by ikegami (Patriarch) on Feb 22, 2024 at 14:55 UTC

    If you don't understand the program, then you should observe what it does. As it was previously suggested, should you trace through the program and see the effects on the real and effective ids.

      Thank you very much for bothering. I've got it :-)

      But I did so and couldn't find a situation where lines 7 and 8 actually effected anything. That is, in every test I conducted, $UID was equal to $orig_UID before line 7; the same goes for $GID and $orig_GID. I have described this in my first post here.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11157831]
Approved by marto
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others having a coffee break in the Monastery: (3)
As of 2024-04-14 14:23 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found