in reply to Not understanding the code to drop privileges in perlsec

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.)

Replies are listed 'Best First'.
Re^2: Not understanding the code to drop privileges in perlsec
by Nocturnus (Scribe) on Feb 23, 2024 at 18:12 UTC

    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.