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.

http://35.94.129.106:3001arrow-up-right

vikemerch.ziparrow-up-right

Analysis

chevron-rightDockerfilehashtag

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.

chevron-rightmain.gohashtag
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 SSTIarrow-up-right.

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 Queriesarrow-up-right which are mostly safe from SQLi.

For password comparision subtle.ConstantTimeComparearrow-up-right is used, which is most secure function so far to compare 2 strings AFAIK. This also means no timing attacksarrow-up-right.

So where is the attack vector?...

assets endpoint was only way to exfiltrate data, but filepath.Cleanarrow-up-right is not exactly "safe" or works how we think. More about filepath.Cleanarrow-up-right

Example: Playgroundarrow-up-right

Solution

Login as admin from UI and get flag. or curl:

circle-check

Ponies

Description

OH NO, where did all these ponies come from??? Quick, get the flag and sail away before we are overrun!

http://35.94.129.106:3009arrow-up-right

Solution

If we take a look at source code we can see javascript file being included:

circle-check

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!

http://35.94.129.106:3003arrow-up-right

Analysis

The application let's us query movies and it has many filters.

moviedb-1

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...

  1. flag.txt's content is really no

  2. Some kind of IP block, e.g.: only localhost can access it.

Before tampering with headers I decided to backtrack a little.

Notice the slash at the end of the path.

Solution

http://35.94.129.106:3003/static/flag.txt/arrow-up-right -> vikeCTF{y0u_tH0Gh7_iT_w4S_5QL_1Nj3c7i0n}

The flag.txt was a route, not file.

circle-check

Jarls Weakened Trust

Description

Jarl's been bragging about becoming an admin on the new axe sharing network. Can you?

http://35.94.129.106:3004arrow-up-right

Solution

The application is based on JWT token. If you login with anything you get:

jarl-1

JWT Token has random userId and admin set to false by default. I had 2 attack vectors in mind:

  1. Change algorithm

    • none algorithm completely removes use of secret key.

  2. Bruteforce the key

    • Can be done with john/hashcat/jwt_tool.

I first used https://token.devarrow-up-right to change algorithm to none, admin -> true and finally change the cookie to become admin.

jarl-2

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.devarrow-up-right removed the dot and hence the first approach failed miserably.

circle-check

Last updated