From 53f56145cc4fc9f14060c0a022f502c210d175a7 Mon Sep 17 00:00:00 2001
From: Lasse Collin <lasse.collin@tukaani.org>
Date: Mon, 28 Mar 2022 09:35:27 -0700
Subject: [PATCH 1/6] zgrep: avoid exploit via multi-newline file names

* zgrep.in: The issue with the old code is that with multiple
newlines, the N-command will read the second line of input,
then the s-commands will be skipped because it's not the end
of the file yet, then a new sed cycle starts and the pattern
space is printed and emptied. So only the last line or two get
escaped. This patch makes sed read all lines into the pattern
space and then do the escaping.

This vulnerability was discovered by:
cleemy desu wayo working with Trend Micro Zero Day Initiative
---
 zgrep.in | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/zgrep.in b/zgrep.in
index 345dae3..bdf7da2 100644
--- a/zgrep.in
+++ b/zgrep.in
@@ -222,9 +222,13 @@ do
 '* | *'&'* | *'\'* | *'|'*)
         i=$(printf '%s\n' "$i" |
             sed '
-              $!N
-              $s/[&\|]/\\&/g
-              $s/\n/\\n/g
+              :start
+              $!{
+                N
+                b start
+              }
+              s/[&\|]/\\&/g
+              s/\n/\\n/g
             ');;
       esac
       sed_script="s|^|$i:|"
-- 
2.35.1.677.gabf474a5dd


From 9973b95d1ea12c3372805074f152bec66d81a9b4 Mon Sep 17 00:00:00 2001
From: Jim Meyering <meyering@fb.com>
Date: Mon, 28 Mar 2022 09:35:27 -0700
Subject: [PATCH 2/6] zgrep: add NEWS and tests for this exploitable bug

* tests/zgrep-abuse: New file, based on PoC by cleemy desu wayo.
* tests/Makefile.am (TESTS): Add it.
* NEWS: Mention the exploit.
The bug appears to have been present since the beginning.
---
 NEWS              |  3 +++
 tests/Makefile.am |  1 +
 tests/zgrep-abuse | 43 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 47 insertions(+)
 create mode 100755 tests/zgrep-abuse

diff --git a/tests/Makefile.am b/tests/Makefile.am
index d09672e..5f148d6 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -36,6 +36,7 @@ TESTS =					\
   z-suffix				\
   zdiff					\
   zgrep-f				\
+  zgrep-abuse				\
   zgrep-context				\
   zgrep-signal				\
   znew-k
diff --git a/tests/zgrep-abuse b/tests/zgrep-abuse
new file mode 100755
index 0000000..f1d6699
--- /dev/null
+++ b/tests/zgrep-abuse
@@ -0,0 +1,43 @@
+#!/bin/sh
+# Show how zgrep applied to a crafted file name may overwrite
+# a selected file with chosen content.  Fixed in gzip-1.12.
+
+# Copyright (C) 2022 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+# limit so don't run it by default.
+
+. "${srcdir=.}/init.sh"; path_prepend_ ..
+
+f=z
+echo test > $f || framework_failure_
+gzip $f || framework_failure_
+cp $f.gz 'zfile_crafted|
+p
+1s|.*|hacked1|
+1w hacked
+etouch .\x2fhacked2
+d
+#
+#' || framework_failure_
+
+fail=0
+
+zgrep test z*
+
+# Before, each of these would be created.
+test -f hacked && fail=1
+test -f hacked2 && fail=1
+
+Exit $fail
-- 
2.35.1.677.gabf474a5dd


From 7801e55ae389b24dfda1d29bf7cadb434472c8dd Mon Sep 17 00:00:00 2001
From: Paul Eggert <eggert@cs.ucla.edu>
Date: Mon, 28 Mar 2022 09:35:27 -0700
Subject: [PATCH 3/6] zgrep: port to POSIX sed
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* zgrep.in (res): When escaping the file name do not rely on GNU
sed’s extension to POSIX with respect to s/.../\n/.  Instead, use
features that should also work with AIX and/or Solaris sed.  This is
simpler anyway, and would have prevented the recently-fixed bug.
---
 zgrep.in | 15 ++++-----------
 1 file changed, 4 insertions(+), 11 deletions(-)

diff --git a/zgrep.in b/zgrep.in
index bdf7da2..6a16dd1 100644
--- a/zgrep.in
+++ b/zgrep.in
@@ -220,18 +220,11 @@ do
       case $i in
       (*'
 '* | *'&'* | *'\'* | *'|'*)
-        i=$(printf '%s\n' "$i" |
-            sed '
-              :start
-              $!{
-                N
-                b start
-              }
-              s/[&\|]/\\&/g
-              s/\n/\\n/g
-            ');;
+          icolon=$(printf '%s\n' "$i:" |
+                     sed -e 's/[&\|]/\\&/g' -e '$!s/$/\\/');;
+      (*) icolon="$i:";;
       esac
-      sed_script="s|^|$i:|"
+      sed_script="s|^|$icolon|"

       # Fail if grep or sed fails.
       r=$(
-- 
2.35.1.677.gabf474a5dd


From ce2bf56a5e3e1814c08513c2d4141a50041e1ae4 Mon Sep 17 00:00:00 2001
From: Paul Eggert <eggert@cs.ucla.edu>
Date: Mon, 28 Mar 2022 09:35:27 -0700
Subject: [PATCH 4/6] gzip: mention when fixed bugs were introduced

---
 NEWS | 2 ++
 1 file changed, 2 insertions(+)

-- 
2.35.1.677.gabf474a5dd


From 42641236d0bb239768794eec93a86c42cbfc2f6a Mon Sep 17 00:00:00 2001
From: Paul Eggert <eggert@cs.ucla.edu>
Date: Mon, 28 Mar 2022 09:35:27 -0700
Subject: [PATCH 5/6] gzexe: optimize out a grep
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* gzexe.in: Avoid an unnecessary invocation of ‘grep’,
by using sed instead.  Also, look only for at-most-3-digit numbers,
for consistency with the rest of the script.
---
 gzexe.in | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/gzexe.in b/gzexe.in
index 04b06a9..1a691e0 100644
--- a/gzexe.in
+++ b/gzexe.in
@@ -91,10 +91,11 @@ for i do
     continue
   fi
   if test $decomp -eq 0; then
-    if sed -e 1d -e 2q "$file" | grep "^skip=[0-9][0-9]*$" >/dev/null; then
+    case `sed -n -e 1d -e '/^skip=[0-9][0-9]*$/p' -e 2q "$file"` in
+    skip=[0-9] | skip=[0-9][0-9] | skip=[0-9][0-9][0-9])
       printf >&2 '%s\n' "$0: $i is already gzexe'd"
-      continue
-    fi
+      continue;;
+    esac
   fi
   if test -u "$file"; then
     printf >&2 '%s\n' "$0: $i has setuid permission, unchanged"
-- 
2.35.1.677.gabf474a5dd


From 526466454cef8dbfcf552e16a171a44f181c8ca0 Mon Sep 17 00:00:00 2001
From: Paul Eggert <eggert@cs.ucla.edu>
Date: Mon, 28 Mar 2022 09:35:27 -0700
Subject: [PATCH 6/6] maint: use C locale more often

* gzexe.in, zdiff.in, zgrep.in:
Run expr and sed in the C locale when it might help to avoid
undefined behavior on non-GNU platforms.
* sample/zfile, znew.in: Run in the C locale, for simplicity and
to avoid undefined behavior on non-GNU platforms.
---
 gzexe.in     |  4 ++--
 sample/zfile |  3 +++
 zdiff.in     |  4 ++--
 zgrep.in     | 29 +++++++++++++++++------------
 znew.in      |  3 +++
 5 files changed, 27 insertions(+), 16 deletions(-)

diff --git a/gzexe.in b/gzexe.in
index 1a691e0..5fc7204 100644
--- a/gzexe.in
+++ b/gzexe.in
@@ -91,7 +91,7 @@ for i do
     continue
   fi
   if test $decomp -eq 0; then
-    case `sed -n -e 1d -e '/^skip=[0-9][0-9]*$/p' -e 2q "$file"` in
+    case `LC_ALL=C sed -n -e 1d -e '/^skip=[0-9][0-9]*$/p' -e 2q "$file"` in
     skip=[0-9] | skip=[0-9][0-9] | skip=[0-9][0-9][0-9])
       printf >&2 '%s\n' "$0: $i is already gzexe'd"
       continue;;
@@ -203,7 +203,7 @@ EOF
   else
     # decompression
     skip=44
-    skip_line=`sed -e 1d -e 2q "$file"`
+    skip_line=`LC_ALL=C sed -e 1d -e 2q "$file"`
     case $skip_line in
     skip=[0-9] | skip=[0-9][0-9] | skip=[0-9][0-9][0-9])
       eval "$skip_line";;
diff --git a/sample/zfile b/sample/zfile
index 6b4514c..d6e7a59 100755
--- a/sample/zfile
+++ b/sample/zfile
@@ -1,5 +1,8 @@
 #!/bin/sh

+LC_ALL=C
+export LC_ALL
+
 if test $# = 0; then
   echo 'zfile: file(1) for programs which may be compressed with gzexe'
   echo usage: `basename $0`  files...
diff --git a/zdiff.in b/zdiff.in
index 012024e..eb4752b 100644
--- a/zdiff.in
+++ b/zdiff.in
@@ -59,7 +59,7 @@ do
   --h*) printf '%s\n' "$usage"   || exit 2; exit;;
   --v*) printf '%s\n' "$version" || exit 2; exit;;
   --) shift; break;;
-  -*\'*) cmp="$cmp '"`printf '%sX\n' "$1" | sed "$escape"`;;
+  -*\'*) cmp="$cmp '"`printf '%sX\n' "$1" | LC_ALL=C sed "$escape"`;;
   -?*) cmp="$cmp '$1'";;
   *) break;;
   esac
@@ -103,7 +103,7 @@ case $file2 in
 if test $# -eq 1; then
   case $1 in
   *[-.]gz* | *[-.][zZ] | *.t[ga]z)
-    FILE=`expr "X$1" : 'X\(.*\)[-.][zZtga]*$'`
+    FILE=`LC_ALL=C expr "X$1" : 'X\(.*\)[-.][zZtga]*$'`
     gzip_status=$(
       exec 4>&1
       (gzip -cd -- "$1" 4>&-; echo $? >&4) 3>&- | eval "$cmp" - '"$FILE"' >&3
diff --git a/zgrep.in b/zgrep.in
index 6a16dd1..2cb2426 100644
--- a/zgrep.in
+++ b/zgrep.in
@@ -64,30 +64,33 @@ while test $# -ne 0; do

   case $option in
   (-[0123456789EFGHIKLPRTUVZabchilnoqrsuvwxyz]*[!0123456789]*)
-    arg2=-\'$(expr "X$option" : 'X-.[0-9]*\(.*\)' | sed "$escape")
+    arg2=-\'$(LC_ALL=C expr "X$option" : 'X-.[0-9]*\(.*\)' |
+                LC_ALL=C sed "$escape")
     eval "set -- $arg2 "'${1+"$@"}'
-    option=$(expr "X$option" : 'X\(-.[0-9]*\)');;
+    option=$(LC_ALL=C expr "X$option" : 'X\(-.[0-9]*\)');;
   (--binary-*=* | --[lm]a*=* | --reg*=*)
     ;;
   (-[ABCDXdefm] | binary-* | --file | --[lm]a* | --reg*)
     case ${1?"$option option requires an argument"} in
     (*\'*)
-      optarg=" '"$(printf '%s\n' "$1" | sed "$escape");;
+      optarg=" '"$(printf '%s\n' "$1" | LC_ALL=C sed "$escape");;
     (*)
       optarg=" '$1'";;
     esac
     shift;;
   (-f?*\'*)
-    optarg=" '"$(expr "X$option" : 'X-f\(.*\)' | sed "$escape")
+    optarg=" '"$(LC_ALL=C expr "X$option" : 'X-f\(.*\)' |
+                   LC_ALL=C sed "$escape")
     option=-f;;
   (-f?*)
-    optarg=" '"$(expr "X$option" : 'X-f\(.*\)')\'
+    optarg=" '"$(LC_ALL=C expr "X$option" : 'X-f\(.*\)')\'
     option=-f;;
   (--file=*\'*)
-    optarg=" '"$(expr "X$option" : 'X--file=\(.*\)' | sed "$escape")
+    optarg=" '"$(LC_ALL=C expr "X$option" : 'X--file=\(.*\)' |
+                   LC_ALL=C sed "$escape")
     option=--file;;
   (--file=*)
-    optarg=" '"$(expr "X$option" : 'X--file=\(.*\)')\'
+    optarg=" '"$(LC_ALL=C expr "X$option" : 'X--file=\(.*\)')\'
     option=--file;;
   (--)
     break;;
@@ -96,7 +99,8 @@ while test $# -ne 0; do
   (*)
     case $option in
     (*\'*)
-      operands="$operands '"$(printf '%s\n' "$option" | sed "$escape");;
+      operands="$operands '"$(printf '%s\n' "$option" | LC_ALL=C sed "$escape")
+      ;;
     (*)
       operands="$operands '$option'";;
     esac
@@ -169,7 +173,7 @@ while test $# -ne 0; do

   case $option in
   (*\'?*)
-    option=\'$(printf '%s\n' "$option" | sed "$escape");;
+    option=\'$(printf '%s\n' "$option" | LC_ALL=C sed "$escape");;
   (*)
     option="'$option'";;
   esac
@@ -182,7 +186,7 @@ eval "set -- $operands "'${1+"$@"}'
 if test $have_pat -eq 0; then
   case ${1?"missing pattern; try \`$0 --help' for help"} in
   (*\'*)
-    grep="$grep -- '"$(printf '%s\n' "$1" | sed "$escape");;
+    grep="$grep -- '"$(printf '%s\n' "$1" | LC_ALL=C sed "$escape");;
   (*)
     grep="$grep -- '$1'";;
   esac
@@ -221,7 +225,7 @@ do
       (*'
 '* | *'&'* | *'\'* | *'|'*)
           icolon=$(printf '%s\n' "$i:" |
-                     sed -e 's/[&\|]/\\&/g' -e '$!s/$/\\/');;
+                     LC_ALL=C sed -e 's/[&\|]/\\&/g' -e '$!s/$/\\/');;
       (*) icolon="$i:";;
       esac
       sed_script="s|^|$icolon|"
@@ -229,7 +233,8 @@ do
       # Fail if grep or sed fails.
       r=$(
         exec 4>&1
-        (eval "$grep" 4>&-; echo $? >&4) 3>&- | sed "$sed_script" >&3 4>&-
+        (eval "$grep" 4>&-; echo $? >&4) 3>&- |
+           LC_ALL=C sed "$sed_script" >&3 4>&-
       ) || { r=$?; test $r -lt 2 && r=2; }
       test 256 -le $r && r=$(expr 128 + $r % 128)
       exit $r
diff --git a/znew.in b/znew.in
index 45d0764..ea76a22 100644
--- a/znew.in
+++ b/znew.in
@@ -18,6 +18,9 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

+LC_ALL=C
+export LC_ALL
+
 version="znew (gzip) @VERSION@
 Copyright (C) 2010-2018 Free Software Foundation, Inc.
 This is free software.  You may redistribute copies of it under the terms of
-- 
2.35.1.677.gabf474a5dd
