4H Du CTF — Hello World Writeup

Ryan Wise
6 min readMar 3, 2021

Intro and Recon

I was teaming up with some of my Canadian friends recently for a small 24 hour CTF where we found this Hello World challenge listed as a medium challenge. What interested me about it was it dealt with Golang, my fav. So let’s dive in and see what this challenge was all about.

Main page with a password input and submit button
The Main Page

The challenge asks you to input a password to retrieve the flag, which is simple enough. If we look at the top there is a recover password button. We are going there next.

Web form for password recovery with a password hint that’s a SHA256 hash
The Recover Password Page

Okay, so it looks like this page generates a hashed version of the password. It’s time to do some more recon and see what’s what.

The first thing I looked at was the robots.txt which had a disallow entry for /storage/ neat, if I go into there, it's a simple directory listing with the following content:

golang  - Ascii image of Go logo
main.go - Go server file
prt/ - Access Denied (Password Recovery Tool)
todo - "learn more go"

Cool we get access to some of the go code running on this server. But not all of it, it looks like /storage/prt contains the rest of it

main.go

package mainimport (
"fmt"
"os"
"strings"
"main/prt"
"net/http"
"html/template"
)
const storagePath = "/storage"
const recoverPath = "/recover"
type secureFS struct {
http.FileSystem
}
func (fs secureFS) Open(name string) (http.File, error) {
file, err := fs.FileSystem.Open(RemovePrefix(storagePath,name))
if err != nil {
return nil, err
}
return file, err
}
func RemovePrefix(p, s string) (o string) {
o = strings.ReplaceAll(s, p, "")
if len(o) == 0 {
return "/"
}
return
}
func BlockAccess(p string) bool {
blocked := []string{
"/prt",
}
return ContainsOneOf(p, blocked)
}
func ContainsOneOf(s string, arr []string) bool {
for _, c := range arr {
if strings.Contains(s, c) {
return true
}
}
return false
}
func main() {
var base_path string
if len(os.Args) < 2 {
fmt.Println("Use ./server <files path>")
} else {
base_path = os.Args[1]
}
templates := template.Must(template.ParseGlob(base_path+"/templates/*")) fs := secureFS{http.Dir(base_path+storagePath)} mux := http.NewServeMux()
mux.Handle(storagePath+"/", http.FileServer(fs))
mux.Handle(recoverPath, prt.NewPasswordRecoveryTool(os.Getenv("LOGINPASSWORD"), 1, templates))
mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
templates.ExecuteTemplate(w, "robots.txt", nil)
})
mux.HandleFunc("/", func(w http.ResponseWriter, r*http.Request) {
fmt.Println("Received " + r.URL.Path)
if r.URL.Path != "/" {
http.Redirect(w,r,"/",http.StatusSeeOther)
return
}
switch r.Method {
case "GET":
templates.ExecuteTemplate(w, "login.html", nil)
return
case "POST":
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if r.FormValue("pwd") == os.Getenv("LOGINPASSWORD") {
templates.ExecuteTemplate(w, "login.html", os.Getenv("CTFFLAG"))
} else {
templates.ExecuteTemplate(w, "login.html", "Access Denied.")
}
return
}})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if BlockAccess(r.URL.Path) {
http.Error(w, "Access Denied.", http.StatusForbidden)
return
}
mux.ServeHTTP(w,r)
})
port := "8080"
fmt.Println("Hosting on port", port)
err := http.ListenAndServe(":"+port, nil)
fmt.Println("Server crashed", err)
}

Vuln #1 — Server Side Filter Bypass

After doing some brainstorming and seeing what’s what I decided the first vulnerability is probably in the code that blocks access to the /storage/prt directory. So let's look at that code only:

const storagePath = "/storage"func (fs secureFS) Open(name string) (http.File, error) {
// Removes /storage from the URL
file, err := fs.FileSystem.Open(RemovePrefix(storagePath,name))
if err != nil {
return nil, err
}
return file, err
}
func RemovePrefix(p, s string) (o string) {
o = strings.ReplaceAll(s, p, "")
if len(o) == 0 {
return "/"
}
return
}
func BlockAccess(p string) bool {
blocked := []string{
// If the url contains /prt AT ALL block access to it
"/prt",
}
return ContainsOneOf(p, blocked)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if BlockAccess(r.URL.Path) {
http.Error(w, "Access Denied.", http.StatusForbidden)
return
}
mux.ServeHTTP(w,r)
})

After trying some basic path traversal and substitutions for the / character I decided think a little bit harder on this and study the code above. If we look closely we can see that the blockAccess code happens first and then it gets passed onto the second http handler which will remove certain prefixes. Like the storage prefix.

So if I were to try to go to a URL like this:

http://challenges.ctfd.io:30537/storage/pr/storaget/

We get a successful directory listing!

Response from the get request, a link to prt.go
Woohoo!

So because blockAccess happens before anything else it doesn’t see an exact match for “/prt” and when the next handler removes “/storage” we get a valid path to the prt directory! More importantly the code for password recovery!

Vuln #2 — Unsigned Integer Overflow

Let’s download ourselves some password recovery tool and get to work.

prt.go

package prtimport (
"crypto/sha256"
"encoding/hex"
"net/http"
"strconv"
"html/template"
)
type PasswordRecoveryTool struct {
Password []byte
Minimum uint64
templates *template.Template
}
func NewPasswordRecoveryTool(s string, minimum uint64, tmpl *template.Template) *PasswordRecoveryTool {
return &PasswordRecoveryTool{[]byte(s), minimum, tmpl}
}
func (prt PasswordRecoveryTool) hashPassword(extraTimes uint64) string {
tmp := prt.Password
var hash [32]byte
for x := prt.Minimum; x <= prt.Minimum + extraTimes && x <= 30; x++ {
hash = sha256.Sum256(tmp)
tmp = hash[:]
}
return hex.EncodeToString(tmp)
}
func (prt PasswordRecoveryTool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
extra, err := strconv.ParseUint(r.FormValue("x"), 10, 64)
if err != nil {
extra = 0
}
h := prt.hashPassword(extra) prt.templates.ExecuteTemplate(w,"recover.html", h)
return
}

Now, why the hell is there a form value involved with hashing a password? that’s where I should focus my efforts.

I tried using negative numbers at first because I didn’t realize it was an unsigned int due to the fact I get too excited when I make progress and want to get it done as fast as possible. Also, I’m dumb :)

In addition to the negative number, I tried words, special characters, etc, etc to maybe trigger some kind of error. But alas it would always return the same hashed password.

At this point, I decided since I have all the code I will just run it on my computer that way I can debug it with GoLand to step through and see what’s what.

I took a small break at this point to get some lunch and brainstorm how I can convince the server to give me an unhashed password. I thought about what data types (uint64) were used for user inputs and how they are used in the code.

Side note, did you know the largest uint64 that’s possible is 18,446,744,073,709,551,615? This is interesting because our values are uint64s and in our use case 2 uint64s get added together :) so if I pass in a value it can’t calculate correctly it will just return the password in hex form and just skip the for loop in the hashpassword function

I send it off with this curl

curl --request POST \\
--url <http://challenges.ctfd.io:30537/recover> \\
--header 'Content-Type: application/x-www-form-urlencoded' \\
--data x=18446744073709551615 \\
--data =

Response to the hex-encoded password :D

Converting the hex to ascii gives us this as a result: WowDidYouReallyCrackThisPassword yep that looks like a standard CTF password! Let's through it in the admin panel and see if we get a flag.

Look at that! Perfect!

the flag “I just learned Golang”: FLAG-{I_Jus7_L34rNed_G0Lan6}
Success!

Retrospective

This was a fairly easy challenge but it was fun since it was the first CTF I’ve done in quite a few months. I love challenges where you get to read the code and extract a vuln from it. It makes it easier but it’s great to see the thought process behind blocking security vulns.

If you have any questions or comments on how I went about this, feel free to reach out to me!

--

--

Ryan Wise

Sr. Software Engineer @ Smarter Services, passionate about security, embedded electronics and going fast.