Enumeration#

From the Dockerfile, we know that the objective is to obtain a flag provided as a CLI parameter to a go HTTP server:

RUN go build -o main .
# ...
CMD ["./main", "--FLAG=AKASEC{REDACTED}"]

The app fetches the contents of the provided URL and renders it into a Result div.

In case of an error, the error message is rendered instead.

{{if .error}}
<div class="alert alert-danger mt-4">{{.error}}</div>
{{end}} {{if .result}}
<div class="mt-4">
  <h2>Result:</h2>
  <div class="border p-3" style="height: 300px; overflow: auto">
    <div>{{.result}}</div>
  </div>
</div>
{{end}}

The URL is sanitized by the safeurl package (in the newest version). If it doesn’t match the set criteria, an error is rendered instead. Providing a port flagged the URL as dangerous.

From the route handlers, we know that the /flag route is where the --FLAG parameter is accessed. However, the tricky 1 == 0 condition always makes the app render Nahhhhhhh. This indicates that the /flag route is likely a red herring.

func flagHandler(w http.ResponseWriter, r *http.Request) {
	args := os.Args
	flag := args[1]

	if 1 == 0 { // can you beat this :) !?
	    fmt.Fprint(w, flag)
	} else {
	    fmt.Fprint(w, "Nahhhhhhh")
	}
}

func main() {
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/flag", flagHandler)
    http.ListenAndServe(":1339", nil)
}

Another important aspect is the indexHandler that handles all routes that don’t match /flag, with no 404 page provided.

When we request GET /, only the form is rendered. Upon requesting POST / url=http://example.com, the result is fetched and rendered.

func indexHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        config := safeurl.GetConfigBuilder().
            Build()

        client := safeurl.Client(config)

        url := r.FormValue("url")

        _, err := client.Get(url)
        if err != nil {
            renderTemplate(w, "index", map[string]string{"error": "The URL you entered is dangerous and not allowed."})
            fmt.Println(err)
            return
        }

        resp, err := http.Get(url)
        if err != nil {
            fmt.Println(err)
            return
        }
        defer resp.Body.Close()

        body, _ := io.ReadAll(resp.Body)
        renderTemplate(w, "index", map[string]interface{}{"result": template.HTML(body)})
        return
    }

    renderTemplate(w, "index", nil)
}

One observation is that safeurl is only used to determine if the URL is safe, but the safe URL wasn’t reused for http.Get(url).

Exploitation#

I attempted many differed things.

Go Template Injection with:

{{ .os.Args[1] }}

was not possible due to template.HTML() being used for parsing the result body.

Trying path traversal also proved unsuccessful, as safeurl flagged it as dangerous. Even if we could bypass that, we would still end up at the intentionally broken /flag route.

Different protocols were also caught by safeurl - it only allows http and https by default. Not that I know what would I do next if that worked…

Despite the challenge’s name, attempting to build a reverse proxy in Golang didn’t succeed as well.

The only vulnerability I knew of was XSS, but it was not useful in this context. And searching for a 0day in net/http didn’t sound like a feasible solution.

After exhausting these options, I decided to rest for the day.

The next day, I ran the Docker image locally. I realized that the Dockerfile was copied into the image. I attempted to access it by exploiting the route handlers but found them virtually un-overridable.

After struggling for a few hours, I took a break and returned with a fresh perspective.

I delved into understanding how the used go packages function, as I had focused too much on html/template previously and neglected the other packages.

I then explored net/http/pprof used for debugging - jackpot!

ℹ️ Info

Package pprof serves via its HTTP server runtime profiling data in the format expected by the pprof visualization tool.

The package is typically only imported for the side effect of registering its HTTP handlers. The handled paths all begin with /debug/pprof/.

Given that it registers its handlers at /debug/pprof/, I investigated the path and, ultimately, found the solution:

Pwned#

Examining the path below revealed the CLI parameters used to start the app, and the flag was mine 💪

http://TARGET_IP:PORT/debug/pprof/cmdline?debug=1

ℹ️ Info

The challenge author notes that he forgot to bind /debug/pprof/cmdline to localhost:PORT making it public, which effectively rendered the challenge easier than intended.

If he didn’t forget, I would need to exploit the SSRF (probably proxying the url with sth like proxysite.cc would suffice. If not, then using the custom domain would).

Key takeaways#

  • In CTF competitions, there are lots of dead ends that can consume a lot of time. This app turned out to be a one, giant red herring.
    • If one approach fails, pivot to another. You can always revisit the initial strategy.
  • Always start a white-box challenge by examining its dependencies.