perl5 icon indicating copy to clipboard operation
perl5 copied to clipboard

Issue with $PROGRAM_NAME (sudo + checksum verification)

Open michal-josef-spacek opened this issue 6 months ago • 1 comments

The issue

The Red Hat customer reported an interesting issue with $PROGRAM_NAME ($0). There is $PROGRAM_NAME with result like /dev/fd/6 when the script is running under sudo with checksum verification.

We have /usr/local/bin/script.pl

#!/usr/bin/perl

use strict;
use warnings;

use Cwd qw( abs_path );

print "\$0: $0\n";
print "abs_path(\$0): " . abs_path($0) . "\n";

Configuration of sudo:

$ echo "user ALL=NOPASSWD: sha256:$(sha256sum /usr/local/bin/script.pl)" > /etc/sudoers.d/user

Example user:

useradd user

Running as user.

root> su user
user> sudo /usr/local/bin/script.pl

Result like (on Fedora Rawhide):

$0: /dev/fd/6
abs_path($0): /proc/1961/fd/6

I investigated the issue, and there are some consequences:

  1. Perl doesn't user prctl API for get of $PROGRAM_NAME
  2. C code with prctl works fine in this case. C code in ex1.c:
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <sys/prctl.h>

int main(void) {
    char name[17] = {0};

    if (prctl(PR_GET_NAME, (unsigned long)name, 0, 0, 0) == -1) {
        perror("prctl(PR_GET_NAME)");
        return 1;
    }

    printf("Name of program: %s\n", name);
    return 0;
}

Compilation:

gcc -o ex1  ex1.c

The result is:

Name of program: ex1
  1. When I looked at the code, it is in perl.c:
...
        /* if find_script() returns, it returns a malloc()-ed value */
        scriptname = PL_origfilename = find_script(scriptname, dosearch, NULL, 1);
        s = scriptname + strlen(scriptname);

        if (strBEGINs(scriptname, "/dev/fd/")
            && isDIGIT(scriptname[8])
            && grok_atoUV(scriptname + 8, &uv, &s)
            && uv <= PERL_INT_MAX
        ) {
            fdscript = (int)uv;
            if (*s) {
                /* PSz 18 Feb 04
                 * Tell apart "normal" usage of fdscript, e.g.
                 * with bash on FreeBSD:
                 *   perl <( echo '#!perl -DA'; echo 'print "$0\n"')
                 * from usage in suidperl.
                 * Does any "normal" usage leave garbage after the number???
                 * Is it a mistake to use a similar /dev/fd/ construct for
                 * suidperl?
                 */
                *suidscript = TRUE;
                /* PSz 20 Feb 04
                 * Be supersafe and do some sanity-checks.
                 * Still, can we be sure we got the right thing?
                 */
                if (*s != '/') {
                    Perl_croak(aTHX_ "Wrong syntax (suid) fd script name \"%s\"\n", s);
                }
                if (! *(s+1)) {
                    Perl_croak(aTHX_ "Missing (suid) fd script name\n");
                }
                scriptname = savepv(s + 1);
                Safefree(PL_origfilename);
                PL_origfilename = (char *)scriptname;
            }
        }
    }
... 

The condition:

if (strBEGINs(scriptname, "/dev/fd/")
            && isDIGIT(scriptname[8])
            && grok_atoUV(scriptname + 8, &uv, &s)
            && uv <= PERL_INT_MAX
        ) {

hit the code, but there is no implementation for this situation.

  1. I found another situation with similar output
perl <( echo '#!perl -DA'; echo 'print "$0\n"');

The resolution

I tried to implement the resolving of /dev/fd/ to program name via readlink like:

...
                PL_origfilename = (char *)scriptname;
+            } else {
+                char proc_fd_path[64];
+                snprintf(proc_fd_path, sizeof(proc_fd_path), "/proc/self/fd/%d", fdscript);
+                char target_path[PATH_MAX];
+                ssize_t len = readlink(proc_fd_path, target_path, sizeof(target_path) - 1);
+                if (len != -1) {
+                    target_path[len] = '\0';
+                    PL_origfilename = savepv(target_path);
+                }
            }
...

The result in 3) is /usr/local/bin/script.pl The result in 4) is pipe:number

Questions

What do you think about this?

michal-josef-spacek avatar Jun 02 '25 09:06 michal-josef-spacek

There is another possible solution in the prctl implementation for get of $PROGRAM_NAME.

michal-josef-spacek avatar Jun 03 '25 09:06 michal-josef-spacek

This is also a valid solution, and works now under the environment in question:

use FindBin ':ALL';
print "RealBin/RealScript is $RealBin/$RealScript\n";

FindBin is shipped in core: https://metacpan.org/pod/FindBin

ChibaPet avatar Jun 19 '25 21:06 ChibaPet

This is also a valid solution, and works now under the environment in question: This is not a valid solution; this is a workaround.

There is a difference between the situation in the Linux kernel (usage with prctl) and Perl. We need to fix or/and add support for prctl for getting of PROGRAM_NAME.

michal-josef-spacek avatar Jun 20 '25 08:06 michal-josef-spacek

A security/exploit question that needs to be answered. Is Perl's responsibility regarding $0 and $^X, limited to capturing and forever returning the strings seen at /usr/bin/perl initial process startup? Or is the Perl interpreter responsible to make sure regarding $0 and $^X are live values at all times and accurately reflect all possible file delete/unlink/rename/move/hard and soft link system calls executed by the perl process or any other parallel executing OS process between initial process startup, and where ever, whenever, $0 and $^X executed in the run loop?

Let us assume "initial process startup" means first microsecond of wall time inside C's main().

This problem has already been used for 0-day exploits, or at least written up as 0 day exploits in CVEs, for both unix and windows OSes before.

I personally don't care either way, what Perl's official answer is on this so called exploit category. My gut instinct is that "the fix" is an Perl PP POD API docs change. Not some attempt at file locking $^X and $0 at process startup or capturing inode numbers or current Perl's Taint Mode's RWX perms checking, or asking the Linux and Windows kernels what is the LIVE absolute disk path of the current virtual address space formerly known as /usr/bin/perl ?

I've seen on the WWW the sample code on fixing the exploit inside https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamew and asking the NT Kernel's MMU for the current absolute, not startup absolute, pathname of the process, I decided in a couple seconds looking at that sample code, to never write that patch for WinPerl. Its slower than GetModuleFileName(), and its out of scope for the Perl VM to fight same machine hostile malicious automata and hostile malicious mouse clicks.

The WWW sample code is also useless and broken, since it can not be mathematically proven, if between the system kernel returning execution control back to your process, your process's /usr/bin/perl has not been renamed or deleted, before reentering the system kernel inside exec() or spawn(). I can easily fake this with a couple strategic breakpoints set ahead of time inside my C debugger inside Perl, and then a couple more strategic mischievous mouse clicks at Perl runtime, inside my C debugger, and inside my File Explorer GUI, and after doing all of that artificial stuff, I can get an unintentional disk file to execute in connection with the Perl ecosystem.

See very long ago fixed race conditions https://github.com/Perl/perl5/issues/12979 and https://github.com/Perl/perl5/issues/13328 https://github.com/Perl/perl5/issues/7714 and maybe https://github.com/Perl/perl5/issues/17429 that 99.999% of P5P C devs couldn't reproduce, except my Server 2003 4 socket 8 core HP DL585 in my house and someone's Win32 blead perl smoke tester box on a cloud server in a data center.

bulk88 avatar Jun 21 '25 23:06 bulk88

I forgot there was a tech term for what I was thinking.

Is Perl ACID compliant?

Is Torvalds's and GNU's C code ACID compliant?

Is the OP's patch ACID compliant?

https://news.ycombinator.com/item?id=11803431

Either patch the perldocs or fetch a new CVE number jk jk

bulk88 avatar Jun 22 '25 00:06 bulk88

https://linux.die.net/man/2/getdents64

https://perldoc.perl.org/functions/readdir

https://github.com/Perl/perl5/blob/blead/pp_sys.c#L4312

How much is the bug bounty again? <sarcasm off>

I just learned GNUware's ls is atomic. Perl is not. Your shell is not. Your video card is not. And ls's output is mathematically uninitialized memory by the time it reaches your eyes. Does a tree make noise if it falls in a forest still has not been solved by science.


Current POD answer for Perl vs ACID vs Linux vs this proposed improvement

https://perldoc.perl.org/perlsec#Shebang-Race-Condition

10 too long paragraphs that say nothing, one way or the other way, for ACID compliance.

The OP's proposal is still a good thing and should be published in blead perl, since it protects against accidents with production code, not against Spectre and unlimited CPU time pen testing.

bulk88 avatar Jun 22 '25 00:06 bulk88

@bulk88

[...] Or is the Perl interpreter responsible to make sure regarding $0 and $^X are live values at all times and accurately reflect all possible file delete/unlink/rename/move/hard and soft link system calls [...]

A script can assign to $0, so its value definitely can't be backed by kqueue or inotify or any other type of ongoing measure to "keep it up to date".

since it can not be mathematically proven,

rofl

guest20 avatar Jun 23 '25 02:06 guest20

A script can assign to $0, so its value definitely can't be backed by kqueue or inotify or any other type of ongoing measure to "keep it up to date".

So being able to same-perl-process mock $0 for unit tests is part of public API and not an exploit? ☺️ What a relief, phew.

But reading the OP's complaint, what is the point and rational of libperl.so supporting or knowing of this high-security Linux feature https://askubuntu.com/questions/1536418/securing-sudoers-entry-with-sha512-checksum that protects against other-process other-address-space hostile malicious mouse clicks on the same filing system?

Why should libperl.so bother asking the ring 0 Linux kernel if the current system's glibc.so is infected with "hostile malicious mouse clicks" or "hostile malicious machine code" and double checking if glibc.so is passing false data to /usr/bin/perls main() inside arg char ** argv? Which is the whole point of this demo and patch.

If there is a production code example why to ask the ring 0 Linux kernel vs trusting glibc.so , or some end user friendly-ness reason, very good! Add the code to blead perl. But carefully document this improvement is only for end user friendly-ness, and the new code has nothing to do with CVEs/pen testing and security of public facing IPv4/IPv6 Perl WWW servers.

Someone need to explain how this prctl(PR_GET_NAME, , , ); patch is not security critical like that https://wiki.gentoo.org/wiki/Project:Perl/Dot-In-INC-Removal and https://nvd.nist.gov/vuln/detail/cve-2016-1238

I, @bulk88, personally think this prctl(PR_GET_NAME, , , ); patch and its demo code, are the same mechanism of action as CVE-2016-1238 . And because of that, prctl(PR_GET_NAME, , , ); patch and its demo code, need a very heavy review, and lookover/thinkover, for more defects, flaws, and less-than-perfect security grey-zones inside the interp, and a quick look for additional easy C code tweaks inside libperl.so that the OP didn't do in their minimalist patch.

Or someone should thoroughly explain why prctl(PR_GET_NAME, , , ); patch and its demo code, are not a rehash of CVE-2016-1238, with the only change being $cve_exploit_summary =~ s/\Q@INC\E/\Q$0\E/;.

Or someone needs to call out CVE-2016-1238 as fear mongering nonsense in 2025. I dislike the no-DOT-in-INC change since it's Day 1 announcement.

My argument is over the text in commit test/src code comments/perldocs pod, not the C code the CC will see.

since it can not be mathematically proven,

rofl

All IT industry people younger than me by age (geek friends), that I first met in a non-IT, non-engineering, public social setting, all of them graduated college, with a Bachelors of CyberSecurity. So CyberSecurity is the current fad for on a top 10 best paid IT jobs click bait article.

Therefore, I need to pretend I am a CyberSecurity Analyst instead of SysAdmin with a heavy Perl/C/JS/Win32 background.

In reality I care more about TUI/GUI responsiveness of Perl and WinPerl, for me, or my non-IT employees.. 2nd highest priority is easy of use for me writing new Perl or Perl XS/C code. Security and Perl is irrelevant for me. The IT Security policy is terminating an employee mid-shift and having them leave the building unpaid.

bulk88 avatar Jun 23 '25 23:06 bulk88

Quoth @bulk88 :

So being able to same-perl-process mock $0 for unit tests is part of public API and not an exploit? ☺️ What a relief, phew.

Not a unit test. Not a mock.

Not a girl ~~ Janet

Quoth perldoc perlvar :

On Linux as of perl v5.14.0 the legacy process name will be set with prctl(2), in addition to altering the POSIX name via argv[0] as perl has done since version 4.000.

$0 is not a regular variable that just sits there. It does stuff when you read/write it. Just like $> or $$ do...

I don't even think you can local $0. And if you could, what would that even mean?

Since

  • the docs for $^X say that you shouldn't trust it if you care about security... and
  • OP is running their script as a more privileged user other than the operator, and
  • OP cares enough about security to hard-code the sha512 of their script into a sudo.conf

... it seems like they'd already know to not trust $0 / $^X for locating their own script/interpreter.

I think the main reason this is an issue is because basename(/usr/local/bin/foobar) makes for a better syslog ident / usage message than basename(/dev/fd/6)

guest20 avatar Jun 24 '25 00:06 guest20