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
Dockerfile
Dockerfile is normal usual build you can find everywhere, but with a bit of twist. scratch container image is used to run single binary files so the container only has binary, assets and database.
FROM golang:1.22.0-alpine as builder
RUN apk update && apk add xxd sqlite tar xz
WORKDIR /zig
ADD https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz zig.tar.xz
RUN tar -xf zig.tar.xz
RUN mv zig-linux-x86_64-0.11.0/* . && rmdir zig-linux-x86_64-0.11.0
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY views/ ./views/
COPY main.go ./
# zig cc is for static CGO binaries
RUN CGO_ENABLED=1 GOOS=linux \
CC="/zig/zig cc -target native-native-musl" \
CXX="/zig/zig cc -target native-native-musl" \
go build -v -o vikemerch .
COPY seed.sh ./
RUN ./seed.sh
RUN rm -rf views main.go go.mod go.sum seed.sh
FROM scratch
COPY --from=alpine:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
WORKDIR /app
COPY assets/ ./assets/
COPY --from=builder /app/ .
EXPOSE 8080
CMD ["./vikemerch"]The main binary used is main.go which handles all requests, it's using latest version of packages so no vulnaribility there.
main.go
package main
import (
"crypto/subtle"
"embed"
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
//go:embed views
var viewFS embed.FS
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "Fatal error:", err)
os.Exit(1)
}
}
var flag string = os.Getenv("FLAG")
type Cents int
func (c Cents) String() string {
return fmt.Sprintf("$%v.%02d", int(c)/100, int(c)%100)
}
type Listing struct {
ID string
Title string
Description string
PriceCents Cents `db:"priceCents"`
Image string
}
type User struct {
Username string
Password string
}
func main() {
db := sqlx.MustOpen("sqlite3", "file:db.sqlite3")
e := gin.Default()
e.SetHTMLTemplate(template.Must(template.ParseFS(viewFS, "views/**")))
e.GET("/", func(c *gin.Context) {
listings := make([]Listing, 0)
err := db.Select(&listings, "SELECT * FROM listing;")
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.HTML(http.StatusOK, "index.html", gin.H{"Listings": listings})
})
e.GET("/search", func(c *gin.Context) {
query := c.Query("q")
listings := make([]Listing, 0)
err := db.Select(&listings, `
SELECT *
FROM listing
WHERE title LIKE '%' || ? || '%'
OR description LIKE '%' || ? || '%';
`, query, query)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.HTML(http.StatusOK, "search.html", gin.H{
"Listings": listings,
"Query": query,
})
})
e.GET("/product", func(c *gin.Context) {
id := c.Query("id")
var listing Listing
err := db.Get(&listing, "SELECT * from listing WHERE id = ?;", id)
if err != nil {
c.AbortWithError(404, err)
return
}
c.HTML(http.StatusOK, "product.html", listing)
})
e.GET("/assets", func(c *gin.Context) {
id := c.Query("id")
path := filepath.Join("assets", filepath.Clean(id))
c.File(path)
})
e.GET("/cart", underConstruction)
e.GET("/admin", func(c *gin.Context) {
cookie, err := c.Cookie("FLAG")
if err != nil || subtle.ConstantTimeCompare([]byte(cookie), []byte(flag)) == 0 {
c.HTML(http.StatusOK, "admin.html", nil)
return
}
c.String(http.StatusOK, flag)
})
e.POST("/admin", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
var user User
err := db.Get(&user, "SELECT * FROM user WHERE username = ?", username)
if err != nil {
c.HTML(http.StatusUnauthorized, "admin.html", "Username or password is incorrect")
return
}
if subtle.ConstantTimeCompare([]byte(password), []byte(user.Password)) == 0 {
c.HTML(http.StatusUnauthorized, "admin.html", "Username or password is incorrect")
return
}
c.Writer.Header().Add("Set-Cookie", "FLAG="+flag)
c.Writer.Header().Add("Content-Type", "text/plain")
c.Writer.WriteString(flag)
})
if os.Getenv("LIVE_RELOAD") != "" {
e.Use(func(c *gin.Context) {
e.LoadHTMLGlob("views/**")
})
}
must(e.Run("0.0.0.0:8080"))
}
func underConstruction(c *gin.Context) {
c.HTML(http.StatusOK, "under-construction.html", gin.H{"BackURL": c.Request.Referer()})
}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
Example: Playground
Solution
Login as admin from UI and get flag. or curl:
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:
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.
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
noSome 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:

JWT Token has random userId and admin set to false by default. I had 2 attack vectors in mind:
Change algorithm
nonealgorithm 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.
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