~nabijaczleweli/ossp

ePerl [2.2.0, 2.2.14] input sanitisation bug causes unintended potentially-remote code execution vulnerability

Details
Message ID
<vbemeg42slelczbsq3dggk5f77qw4k7fpbtwlbozdaxivkwjlc@tarta.nabijaczleweli.xyz>
DKIM signature
pass
Download raw message
Patch: +83 -32
(a) ossp-eperl is a macro system for embedding Perl code within text documents
    for example, a file like
      abc
      <: print 2*3 :>
      def
    turns into a perl program similar to
      print "abc\n";
      print 2*3; print "\n";
      print "def\n";
    which is then evaluated to produce
      abc
      6
      def
(b) eperl has a C-like preprocessor that has
    #include (normal) and #sinclude ("safe") directives
(c) as with the C preprocessor, inclusions are recursive 
    for a file structure of
      a1: #include b1
      b1: #include c1
      c1: <: system('id') :>
    preprocessing a1 yields a file consisting of
      <: system('id') :>
    that then gets turned into perl program 
    	system('id');
    that does what one'd expect
(d) "safe" inclusions are identical, except they remove the begin/end tags
    (this notice uses <:/:> but they can differ sometimes)
(e) they do /not/ remove the would-be contents of the tags,
    it's effectively a s/<://g s/:>//g operation on the result, so given
      a1s: #sinclude b1
    preprocessing a1s yields a file consisting of
       system('id')
    that then gets turned into perl program 
    	print " system('id'); \n";
    which also does what one'd expect
(f) files prefixed with "http://" are processed specially by issuing an HTTP GET
    so a1/a1s could just as well have said
      #include http://hinfo.network/index.html

This is all documented behaviour. It's also quite dangerous.
Debian attempts to patch this seemingly-known bug, saying
  eperl (2.2.14-3) unstable; urgency=low
  
    * The #sinclude directive calling an external server could allow
      execution of arbitrary code (Bug reported by David Madison on the
      ePerl mailing list).
  
   -- Denis Barbier <barbier@debian.org>  Sat, 16 Jun 2001 00:29:53 +0200
with
  --- eperl-2.2.14.orig/eperl_pp.c
  +++ eperl-2.2.14/eperl_pp.c
  @@ -278,6 +281,11 @@
                           cp3 += l1;
                       else if (strncasecmp(cp3, ePerl_end_delimiter, l2) == 0)
                           cp3 += l2;
  +                    else if (strncmp(cp3, "#include", 8) == 0) {
  +                        /*  Replace all occurences of #include by #sinclude */
  +                        strcpy(cp4, "#sinclude");
  +                        cp4 += 9;
  +                    }
                       else
                           *cp4++ = *cp3++;
                   }
full context at
  https://git.sr.ht/~nabijaczleweli/ossp-eperl/tree/ePerl_2_2_14/item/eperl_pp.c#L271
but the relevant bit is
                   /* recursive usage */
                   if ((cp = ePerl_PP_Process(caName, cppINC, 0 /*mode=file*/)) == NULL)
                       return NULL;
   
                   /* make it secure by removing all begin/end delimiters!! */
                   if ((cp2 = (char *)malloc(strlen(cp))) == NULL)
                       return NULL;
                   l1 = strlen(ePerl_begin_delimiter);
                   l2 = strlen(ePerl_end_delimiter);
                   for (cp3 = cp, cp4 = cp2; *cp3 != NUL; ) {
                       if (strncasecmp(cp3, ePerl_begin_delimiter, l1) == 0)
                           cp3 += l1;
                       else if (strncasecmp(cp3, ePerl_end_delimiter, l2) == 0)
                           cp3 += l2;
                       else
                           *cp4++ = *cp3++;
                   }

It's not necessarily easy to see, but it is obvious once you notice,
that all this does is actually just damage the result,
and doesn't change anything formally:
ePerl_PP_Process() returns a fully-preprocessed result
(#-directives removed and resolved),
and cp2/cp4 are copied to the output verbatim.

I can't seem to find the "ePerl mailing list" but it's been 23 years;
The vulnerability can be triggered with the obvious conclusion that
(g) if you instead do
      a2: #sinclude b2
      b2: #include c2
      c2: <<:: system('id') ::>>
    preprocessing a2 yields a file consisting of
      <: system('id') :>
    that then gets turned into perl program 
      system('id')
    oops!
(tbc the same applies if a2 instead said "#include c2",
 but this further illustrates that the Debian patch is meaningless).

The manual recommends using #sinclude for data you don't trust (hence, http://...):
  https://srhtcdn.githack.com/~nabijaczleweli/ossp-eperl/blob/13ef21849157bcb160de27c88f39402cd4b5edbc/ossp-eperl.pdf#page=7

This bug wouldn't be anything out of the ordinary:
it's been 23 years since the original report (and 27 since the bug was introduced),
and the only use-case for eperl is wml ‒ no-one is using http includes there ‒
but Debian actually purports to fix this,
so this was first reported to debian-security@ on 2024-09-22.

This behaviour is present in all ePerl versions since 2.2.0 (1997-07-18)
(previous versions didn't have a preprocessor:
 https://git.sr.ht/~nabijaczleweli/ossp-eperl/tree/tarballs/item/NEWS#L64
 https://git.sr.ht/~nabijaczleweli/ossp-eperl/tree/tarballs/item/ChangeLog#L341-505)
and, thus, AFAICT, every version ever distributed by anyone.

Upcoming ossp-eperl 2.2.15 fixes this with
commit 5971b87 ("Fix #sinclude to /actually/ remove all delimiters. Add test to validate this")
<https://git.sr.ht/~nabijaczleweli/ossp-eperl/commit/5971b873fcfd34e51d8c1605b3a58e006538eb05>
(please cf. https://sr.ht/~nabijaczleweli/ossp for more about the new upstream).

Backports are included below.

bpo-2.2.14.diff           is based against 2.2.14
bpo-Debian-2.2.14-24.diff is based against Debian 2.2.14-24
                          (and the current Fedora patch bundle(?),
                           since it includes Debian 2.2.14-15.1 wholesale)

The patch against 2.2.14 also fixes an overflow if #sincluded data
contains more #include directives than space is left by the removed delimiters.

Neither patch fixes strncasecmp() always being used,
regardless of current delimiter-casedness configuration,

The patch against Debian doesn't fix the "#include" => "#sinclude" mangling
(it's easy to rip out, but the test also tests it, so rip that out too).

Such is the nature of a backport. At least it reduces the working set.

-- >8 --                        bpo-2.2.14.diff                         -- >8 --
diff --git a/eperl_pp.c b/eperl_pp.c
index c4061ae..90d9db2 100644
--- a/eperl_pp.c
+++ b/eperl_pp.c
@@ -269,21 +269,13 @@ char *ePerl_PP_Process(char *cpInput, char **cppINC, int mode)
                    return NULL;

                /* make it secure by removing all begin/end delimiters!! */
                if ((cp2 = (char *)malloc(strlen(cp))) == NULL)
                if ((cp = (char *)realloc(cp, strlen(cp)*9/8+1)) == NULL)
                    return NULL;
                l1 = strlen(ePerl_begin_delimiter);
                l2 = strlen(ePerl_end_delimiter);
                for (cp3 = cp, cp4 = cp2; *cp3 != NUL; ) {
                    if (strncasecmp(cp3, ePerl_begin_delimiter, l1) == 0)
                        cp3 += l1;
                    else if (strncasecmp(cp3, ePerl_end_delimiter, l2) == 0)
                        cp3 += l2;
                    else
                        *cp4++ = *cp3++;
                }
                *cp4 = NUL;
                free(cp);
                cp = cp2;
#define KILLDEL(delimiter) \
                while ((cp2 = strcasestr(cp, delimiter))) \
                    memmove(cp2, cp2 + strlen(delimiter), (strlen(cp) + 1) - (cp2 - cp) - strlen(delimiter));
                KILLDEL(ePerl_begin_delimiter)
                KILLDEL(ePerl_end_delimiter)
            }
            else if (strncmp(cp, "#if", 3) == 0) {
                /* 
diff --git a/t/11_validate_sinclude.t b/t/11_validate_sinclude.t
new file mode 100644
index 0000000..01774a7
--- /dev/null
+++ b/t/11_validate_sinclude.t
@@ -0,0 +1,34 @@
# SPDX-License-Identifier: 0BSD
require "TEST.pl";
TEST::init();

print "1..1\n";

$included2 = TEST::tmpfile(<<"EOF");
<<::system("id")::>>
EOF

$included = TEST::tmpfile(<<"EOF");
<:="A":>
#include $included2
<:="B":>
EOF

$basefile = TEST::tmpfile(<<"EOF");
a
#sinclude $included
b
EOF

$result = `../eperl -P $basefile`;

$want = <<"EOF";
a
="A"
system("id")
="B"
b
EOF
print ($want eq $result ? "ok" : "not ok");

TEST::cleanup();

- >8 --                   bpo-Debian-2.2.14-24.diff                    -- >8 --
diff --git a/eperl_pp.c b/eperl_pp.c
index 644bb30..5928eea 100644
--- a/eperl_pp.c
+++ b/eperl_pp.c
@@ -272,26 +272,17 @@ char *ePerl_PP_Process(char *cpInput, char **cppINC, int mode)
                    return NULL;

                /* make it secure by removing all begin/end delimiters!! */
                if ((cp2 = (char *)malloc(strlen(cp)*9/8)) == NULL)
                if ((cp = (char *)realloc(cp, strlen(cp)*9/8+1)) == NULL)
                    return NULL;
                l1 = strlen(ePerl_begin_delimiter);
                l2 = strlen(ePerl_end_delimiter);
                for (cp3 = cp, cp4 = cp2; *cp3 != NUL; ) {
                    if (strncasecmp(cp3, ePerl_begin_delimiter, l1) == 0)
                        cp3 += l1;
                    else if (strncasecmp(cp3, ePerl_end_delimiter, l2) == 0)
                        cp3 += l2;
                    else if (strncmp(cp3, "#include", 8) == 0) {
                        /*  Replace all occurences of #include by #sinclude */
                        strcpy(cp4, "#sinclude");
                        cp4 += 9;
                    }
                    else
                        *cp4++ = *cp3++;
#define KILLDEL(delimiter) \
                while ((cp2 = strcasestr(cp, delimiter))) \
                    memmove(cp2, cp2 + strlen(delimiter), (strlen(cp) + 1) - (cp2 - cp) - strlen(delimiter));
                KILLDEL(ePerl_begin_delimiter)
                KILLDEL(ePerl_end_delimiter)
                while ((cp2 = strstr(cp, "#include"))) {
                    memmove(cp2 + sizeof("#sinclude")-1, cp2 + sizeof("#include")-1, (strlen(cp) + 1) - (cp2 - cp) - (sizeof("#include")-1));
                    memcpy(cp2, "#sinclude", sizeof("#sinclude")-1);
                }
                *cp4 = NUL;
                free(cp);
                cp = cp2;
            }
            else if (strncmp(cp, "#if", 3) == 0) {
                /* 
diff --git a/t/11_validate_sinclude.t b/t/11_validate_sinclude.t
new file mode 100644
index 0000000..1235f99
--- /dev/null
+++ b/t/11_validate_sinclude.t
@@ -0,0 +1,34 @@
# SPDX-License-Identifier: 0BSD
require "TEST.pl";
TEST::init();

print "1..1\n";

$included2 = TEST::tmpfile(<<"EOF");
<<::system("id#include")::>>
EOF

$included = TEST::tmpfile(<<"EOF");
<:="A":>
#include $included2
<:="B":>
EOF

$basefile = TEST::tmpfile(<<"EOF");
a
#sinclude $included
b
EOF

$result = `../eperl -P $basefile`;

$want = <<"EOF";
a
="A"
system("id#sinclude")
="B"
b
EOF
print ($want eq $result ? "ok" : "not ok");

TEST::cleanup();
Reply to thread Export thread (mbox)