Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.

Commit

Permalink
*: support self-executable JARs
Browse files Browse the repository at this point in the history
  • Loading branch information
ericchiang committed Dec 30, 2021
1 parent 3ccb453 commit c92bc84
Show file tree
Hide file tree
Showing 10 changed files with 469 additions and 12 deletions.
69 changes: 69 additions & 0 deletions jar/jar.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ import (
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"

zipfork "github.com/google/log4jscanner/third_party/zip"
)

const (
Expand Down Expand Up @@ -68,6 +71,72 @@ func Parse(r *zip.Reader) (*Report, error) {
}, nil
}

// ReadCloser mirrors zip.ReadCloser.
type ReadCloser struct {
zip.Reader

f *os.File
}

// Close closes the underlying file.
func (r *ReadCloser) Close() error {
return r.f.Close()
}

// OpenReader mirrors zip.OpenReader, loading a JAR from a file, but supports
// self-executable JARs. See NewReader() for details.
func OpenReader(path string) (r *ReadCloser, offset int64, err error) {
f, err := os.Open(path)
if err != nil {
return
}
info, err := f.Stat()
if err != nil {
f.Close()
return
}
zr, offset, err := NewReader(f, info.Size())
if err != nil {
f.Close()
return
}
return &ReadCloser{*zr, f}, offset, nil
}

// offsetReader is a io.ReaderAt that starts at some offset from the start of
// the file.
type offsetReader struct {
ra io.ReaderAt
offset int64
}

func (o offsetReader) ReadAt(p []byte, off int64) (n int, err error) {
return o.ra.ReadAt(p, off+o.offset)
}

// NewReader is a wrapper around zip.NewReader that supports self-executable
// JARs. JAR files with prefixed data, such as a bash script to allow them to
// run directly.
//
// If the ZIP contains a prefix, the returned offset indicates the size of the
// prefix.
//
// See:
// - https://kevinboone.me/execjava.html
// - https://github.com/golang/go/issues/10464
func NewReader(ra io.ReaderAt, size int64) (zr *zip.Reader, offset int64, err error) {
zr, err = zip.NewReader(ra, size)
if err == nil || !errors.Is(err, zip.ErrFormat) {
return zr, 0, err
}
offset, err = zipfork.ReadZIPOffset(ra, size)
if err != nil {
return nil, 0, err
}
zr, err = zip.NewReader(offsetReader{ra, offset}, size-offset)
return zr, offset, err
}

type checker struct {
// Does the JAR contain the JNDI lookup class?
hasLookupClass bool
Expand Down
9 changes: 6 additions & 3 deletions jar/jar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package jar

import (
"archive/zip"
"path/filepath"
"testing"
)
Expand Down Expand Up @@ -51,6 +50,7 @@ func TestParse(t *testing.T) {
// Test case where it contains a JndiLookupOther.class file that shouldn't be detected as vulnerable
{"similarbutnotvuln.jar", false},
{"vuln-class.jar", true},
{"vuln-class-executable", true},
{"vuln-class.jar.patched", false},
{"good_jar_in_jar.jar", false},
{"good_jar_in_jar_in_jar.jar", false},
Expand All @@ -61,11 +61,14 @@ func TestParse(t *testing.T) {
{"bad_jar_with_invalid_jar.jar", true},
{"bad_jar_with_invalid_jar.jar.patched", false},
{"good_jar_with_invalid_jar.jar", false},
{"helloworld-executable", false},
{"helloworld.jar", false},
{"helloworld.signed.jar", false},
}
for _, tc := range testCases {
t.Run(tc.filename, func(t *testing.T) {
p := testdataPath(tc.filename)
zr, err := zip.OpenReader(p)
zr, _, err := OpenReader(p)
if err != nil {
t.Fatalf("zip.OpenReader failed: %v", err)
}
Expand All @@ -85,7 +88,7 @@ func TestParse(t *testing.T) {
func BenchmarkParse(b *testing.B) {
filename := "safe1.jar"
p := testdataPath(filename)
zr, err := zip.OpenReader(p)
zr, _, err := OpenReader(p)
if err != nil {
b.Fatalf("zip.OpenReader failed: %v", err)
}
Expand Down
43 changes: 43 additions & 0 deletions jar/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,50 @@ var skipSuffixes = [...]string{
".SF",
}

// RewriteJAR is like Rewrite but accounts for self-executable JARs, copying
// any prefixed data that may be included in the JAR.
func RewriteJAR(dest io.Writer, src io.ReaderAt, size int64) error {
zr, offset, err := NewReader(src, size)
if err != nil {
return err
}

if offset > 0 {
src := io.NewSectionReader(src, 0, offset)
if _, err := io.CopyN(dest, src, offset); err != nil {
return err
}
}
return Rewrite(dest, zr)
}

// Rewrite attempts to remove any JndiLookup.class files from a JAR.
//
// Rewrite does not account for self-executable JARs and does not preserve the
// file prefix. This must be explicitly handled, or use RewriteJAR() to do so
// automatically.
//
// zr, offset, err := jar.NewReader(ra, size)
// if err != nil {
// // ...
// }
// dest, err := os.CreateTemp("", "")
// if err != nil {
// // ...
// }
// defer dest.Close()
//
// if offset > 0 {
// // Rewrite prefix.
// src := io.NewSectionReader(ra, 0, offset)
// if _, err := io.CopyN(dest, src, offset); err != nil {
// // ...
// }
// }
// if err := jar.Rewrite(dest, zr); err != nil {
// // ...
// }
//
func Rewrite(w io.Writer, zr *zip.Reader) error {
zw := zip.NewWriter(w)
for _, zipItem := range zr.File {
Expand Down
113 changes: 106 additions & 7 deletions jar/rewrite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,22 @@ func cpFile(t *testing.T, dest, src string) {
}

func autoMitigateJAR(path string) error {
r, err := zip.OpenReader(path)
r, err := os.Open(path)
if err != nil {
return fmt.Errorf("open reader: %v", err)
return fmt.Errorf("open flie: %v", err)
}
defer r.Close()
info, err := r.Stat()
if err != nil {
return fmt.Errorf("stat file: %v", err)
}

f, err := os.CreateTemp("", "")
if err != nil {
return fmt.Errorf("create temp: %v", err)
}
defer f.Close()
if err := Rewrite(f, &r.Reader); err != nil {
if err := RewriteJAR(f, r, info.Size()); err != nil {
return fmt.Errorf("rewriting zip: %v", err)
}

Expand Down Expand Up @@ -173,6 +178,100 @@ func TestAutoMitigateJAR(t *testing.T) {
"bad_jar_in_jar_in_jar.jar",
"bad_jar_with_invalid_jar.jar",
"vuln-class.jar",
"vuln-class-executable",
} {
tc := tc
t.Run(tc, func(t *testing.T) {
t.Parallel()
src := testdataPath(tc)
dest := filepath.Join(t.TempDir(), tc)

cpFile(t, dest, src)

if err := autoMitigateJAR(dest); err != nil {
t.Fatalf("autoMitigateJar(%s) failed: %v", dest, err)
}

before, _, err := OpenReader(src)
if err != nil {
t.Fatalf("zip.OpenReader(%q) failed: %v", src, err)
}
defer before.Close()
after, _, err := OpenReader(dest)
if err != nil {
t.Fatalf("zip.OpenReader(%q) failed: %v", dest, err)
}
defer after.Close()
checkJARs(t, func(name string) bool {
return path.Base(name) == "JndiLookup.class"
}, &before.Reader, &after.Reader)
})
}
}

func TestAutoMitigateExecutable(t *testing.T) {
for _, tc := range []string{
"helloworld-executable",
"vuln-class-executable",
} {
tc := tc
t.Run(tc, func(t *testing.T) {
t.Parallel()
src := testdataPath(tc)
dest := filepath.Join(t.TempDir(), tc)

cpFile(t, dest, src)

if err := autoMitigateJAR(dest); err != nil {
t.Fatalf("autoMitigateJar(%s) failed: %v", dest, err)
}

sf, err := os.Open(src)
if err != nil {
t.Fatalf("open file %s: %v", src, err)
}
defer sf.Close()
info, err := sf.Stat()
if err != nil {
t.Fatalf("stat file %s: %v", src, err)
}

_, offset, err := NewReader(sf, info.Size())
if err != nil {
t.Fatalf("new jar reader %s: %v", src, err)
}
if offset <= 0 {
t.Errorf("expected offset for executable %s: got=%d", src, offset)
}

df, err := os.Open(dest)
if err != nil {
t.Fatalf("open file %s: %v", dest, err)
}
defer df.Close()

got := make([]byte, offset)
want := make([]byte, offset)
if _, err := io.ReadFull(sf, want); err != nil {
t.Fatalf("reading prefix from file %s: %v", src, err)
}
if _, err := io.ReadFull(df, got); err != nil {
t.Fatalf("reading prefix from file %s: %v", dest, err)
}
if !bytes.Equal(got, want) {
t.Errorf("prefix did not match after rewrite, got=%q, want=%q", got, want)
}
})
}
}
func TestAutoMitigate(t *testing.T) {
for _, tc := range []string{
"arara.jar",
"bad_jar_in_jar.jar",
"bad_jar_in_jar_in_jar.jar",
"bad_jar_with_invalid_jar.jar",
"vuln-class.jar",
"vuln-class-executable",
} {
tc := tc
t.Run(tc, func(t *testing.T) {
Expand All @@ -186,12 +285,12 @@ func TestAutoMitigateJAR(t *testing.T) {
t.Fatalf("autoMitigateJar(%s) failed: %v", dest, err)
}

before, err := zip.OpenReader(src)
before, _, err := OpenReader(src)
if err != nil {
t.Fatalf("zip.OpenReader(%q) failed: %v", src, err)
}
defer before.Close()
after, err := zip.OpenReader(dest)
after, _, err := OpenReader(dest)
if err != nil {
t.Fatalf("zip.OpenReader(%q) failed: %v", dest, err)
}
Expand Down Expand Up @@ -220,12 +319,12 @@ func TestAutoMitigateSignedJAR(t *testing.T) {
t.Fatalf("autoMitigateJar(%s) failed: %v", dest, err)
}

before, err := zip.OpenReader(src)
before, _, err := OpenReader(src)
if err != nil {
t.Fatalf("zip.OpenReader(%q) failed: %v", src, err)
}
defer before.Close()
after, err := zip.OpenReader(dest)
after, _, err := OpenReader(dest)
if err != nil {
t.Fatalf("zip.OpenReader(%q) failed: %v", dest, err)
}
Expand Down
29 changes: 29 additions & 0 deletions jar/testdata/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash -e

# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

echo '#!/bin/bash
file_path=`realpath $0`
exec java -jar $file_path "$@"
' > helloworld-executable
cat helloworld.jar >> helloworld-executable
chmod +x helloworld-executable

echo '#!/bin/bash
file_path=`realpath $0`
exec java -jar $file_path "$@"
' > vuln-class-executable
cat vuln-class.jar >> vuln-class-executable
chmod +x vuln-class-executable
Binary file added jar/testdata/helloworld-executable
Binary file not shown.
Binary file added jar/testdata/vuln-class-executable
Binary file not shown.
4 changes: 2 additions & 2 deletions jar/walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func (w *walker) visit(p string, d fs.DirEntry) error {
if !ok {
return fmt.Errorf("file doesn't implement reader at: %T", f)
}
zr, err := zip.NewReader(ra, info.Size())
zr, _, err := NewReader(ra, info.Size())
if err != nil {
if err == zip.ErrFormat {
// Not a JAR.
Expand Down Expand Up @@ -174,7 +174,7 @@ func (w *walker) visit(p string, d fs.DirEntry) error {
}
defer tf.Close()

if err := Rewrite(tf, zr); err != nil {
if err := RewriteJAR(tf, ra, info.Size()); err != nil {
return fmt.Errorf("failed to rewrite %s: %v", p, err)
}
f.Close()
Expand Down
Loading

0 comments on commit c92bc84

Please sign in to comment.