4T$ CTF | My Sky Blog (Web, Golang Template Injection)

Difficulty: Easy

Golang templates include little to no features to allow for file read or RCE but be careful when passing certain variables as they can be accessed and cause damage in some ways.

Our objective in this challenge is to read the flag, one way is to gain access to an admin account and read it from /flag path.

Let's start with reading the code, we have 3 releveant files :

index.go :

package main

import (
	"bytes"
	"fmt"
	"html/template"
	"math/rand"

	"github.com/labstack/echo/v4"
)

var randomSentences = []string{
	"Hey, %s; there are %d posts right now !",
	"What's up %s ? We're at %d ! Aiming for the stars !",
	"Want a coffee %s ? Ofc there's %d posts in here !",
	"Hello there, General %s, have you seen ? There's %d posts !",
}

func index(c echo.Context) error {
	s := c.Get("session").(*Session)
	templatePath := "templates/index.html"

	indexTemplate := template.Must(template.ParseFiles(templatePath))

	if s.User == nil {
		return c.Redirect(302, "/login")
	}

	// We do a cool sentence for our users :D
	chosenSentence := fmt.Sprintf(randomSentences[rand.Intn(len(randomSentences))], s.User.Username, s.NbPosts)
	coolSentence := template.Must(indexTemplate.New("cool").Parse(chosenSentence))

	// Execute our cool template 😎
	var buf bytes.Buffer
	if err := coolSentence.Execute(&buf, s); err != nil {
		fmt.Println("Error executing template:", err)
		return indexTemplate.Execute(c.Response().Writer, s)
	}

	s.CoolSentence = buf.String()

	return indexTemplate.Execute(c.Response().Writer, s)
}

sessions.go :

package main

import (
	"time"

	"github.com/google/uuid"
)

type Session struct {
	// Private fields
	users []*User
	id    string

	// Public fields
	User    *User
	Posts   []*Post
	NbPosts int

	HasError bool
	Error    string

	CoolSentence string
}

var sessions = make(map[string]*Session)

func CreateEmptySession() *Session {
	admin := &User{
		isAdmin:  true,
		Username: "admin",
	}

	// Get a random password
	randomPassword := uuid.New().String()

	admin.ChangePassword(randomPassword)

	id := uuid.New().String()

	return &Session{
		users: []*User{
			admin,
		},
		id: id,

		User: nil,
		Posts: []*Post{
			{
				Author:    admin,
				Title:     "Welcome to my beautiful Sky Blog!",
				Body:      "I welcome you to my blog, where I'll post about my adventures in the sky !",
				UpdatedAt: time.Date(2024, 5, 1, 12, 54, 30, 20, time.UTC),
			},
		},
		NbPosts: 1,
	}
}

and users.go :

package main

import (
	"crypto/sha256"
	"encoding/hex"

	"github.com/sirupsen/logrus"
)

type User struct {
	isAdmin  bool
	Username string
	Password string
}

func (u *User) ChangeUsername(username string) bool {
	u.Username = username

	return true
}

func (u *User) ChangePassword(password string) bool {
	logrus.Infof("Changing password for user %s", u.Username)

	h := sha256.New()
	h.Write([]byte(password))

	u.Password = hex.EncodeToString(h.Sum(nil))

	return true
}

func (u *User) CheckPassword(password string) bool {
	h := sha256.New()
	h.Write([]byte(password))

	return u.Password == hex.EncodeToString(h.Sum(nil))
}

we can see in the index.go that there is a way for SSTI, we can make q username that would inject code into the template through the random messages, we can test that with {{.}}.

we get a bunch of addresses and values, now we see what variables are passed to the template.

return indexTemplate.Execute(c.Response().Writer, s)

the return function in index.go passes the whole session object, we know that we have all the posts stored in the session and since the first post is the one from admin we can change the password of the admin from it and gain access to the app as admin.

we can delete the cookie and create a new user with this username :

{{range .Posts}} {{.Author.ChangePassword `test` }}{{end}}

now the admin password is test, we can login as that account, access the home page and then get the flag.

Last updated