[Go language]SSTI from 0 to 1

[Go language]SSTI from 0 to 1

  • 1.Go-web basics and examples
  • 2.Parameter processing
  • 3. Template engine
    • 3.1 text/template
    • 3.2 SSTI
  • 4.[LineCTF2022]gotm
    • 1. Question source code
    • 2.WP

1.Go-web basics and examples

package main
import (
"fmt"
"net/http"
)
func sayHello(w http.ResponseWriter, r *http.Request) {<!-- --> // Define a
fmt.Fprintln(w, "hello world!")
}
func main() {<!-- -->
http.HandleFunc("/hello", sayHello) //When the browser accesses /hello, it will be transferred to sayHello for processing
http.ListenAndServe(":8888", nil) // Listen to the port and process the request
}

Among them, “net/http” is Go’s own module for implementing web services, which can handle http requests. http.HandleFunc is used to bind the routing function, which is equivalent to the app.route function of python’s flask.

Then there is the routing function sayHello. This function accepts two parameters, where w is the web interface parameter. This variable can be sent to the front-end web page. r is the http message parameter, which is used to store the UA header, form data and other information of the http request.

Then the fmt.Fprintln function is used to output in the writer (io.Writer) such as the web interface

Here the output is “hello world”, as shown in the figure

2. Parameter processing

We change the sayHello function as follows

func sayHello(w http.ResponseWriter, r *http.Request) {<!-- -->
  var name = r.FormValue("name")
  var para = ("hello," + name + "!")
  fmt.Fprintln(w, para)
}

r.FormValue is an array for receiving form data, including GET and POST

Here’s how to handle form requests:

1. Obtain directly

PostFormValue or FormValue method

The former can only parse Post requests

2. Obtained indirectly by ParseForm() method

r.ParseForm() parses the body of the request into a combination of key-value pairs

Key-value pairs will be stored in r.Form and r.PostForm fields

  • r.Form is a url.Values type that represents a collection of URL query parameters and POST form fields. You can get the value of a form field through the r.Form.Get(key), r.Form[key] or r.FormValue(key) method.
  • r.PostForm is a url.Values type that only contains data for POST form fields. Unlike r.Form, r.PostForm does not automatically parse URL query parameters or other non-POST submitted data.

Examples are as follows:

3. Template engine

GO language provides two template packages, one is the html/template module and the other is the text/template module. The text/template template engine is related to the SSTI of the Go language.

3.1 text/template

Example:

package main

import (
    "fmt"
    "net/http"
    "strings"
    "text/template"
)

type User struct {<!-- -->
    ID int
    Name string
    Passwd string
}

func StringTplExam(w http.ResponseWriter, r *http.Request) {<!-- -->
    user := & amp;User{<!-- -->1, "admin", "123456"}
    r.ParseForm()
    arg := strings.Join(r.PostForm["name"], "")
    tpl1 := fmt.Sprintf(`<h1>Hi, ` + arg + `</h1> Your name is ` + arg + `!`)
    html, err := template.New("login").Parse(tpl1)
    html = template.Must(html, err)
    html.Execute(w, user)
}

func main() {<!-- -->
    server := http.Server{<!-- -->
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/login", StringTplExam)
    server.ListenAndServe()
}

First, let’s explain this code. It uses the “text/template” template engine. This template engine does not perform html encoding on the incoming data, which can cause SSTi. Then it defines a User structure for passing parameters to the template.

type User struct {<!-- -->
    ID int
    Name string
    Passwd string
}

Then use the fmt.Sprintf function to splice the incoming name parameter into the template, and then use template.New(“login”) to create a template named login through the following statement, and then use the Parse(tpl1) method to be used as The template string is stored in the login template

 html, err := template.New("login").Parse(tpl1)

Among them, html represents the login template just created, and err represents the error message during the creation of the template (usually there will be no). Then use the following statement to determine whether the template is successfully created. If it is created successfully, it will continue to run. If it fails, it will exit the program and report an error.

html = template.Must(html, err)

Finally, the template is translated through the Execute method, and the values in the user structure are translated into the HTML template, and then output to the w interface, which is the web front end.

html.Execute(w, user)

3.2 SSTI

Let’s take the above web source code as an example

First access the login route, and then POST a parameter. You can see that the echo is normal.

We then pass in name={{.Name}}, where the double braces are placeholders for the template engine and can be translated. We use { {.parameter name}} format, you can access the parameters passed in when building the template

type User struct {<!-- -->
    ID int
    Name string
    Passwd string
}

user := & amp;User{1, "admin", "123456"}

as the picture shows

4.[LineCTF2022]gotm

1. Question source code

main.go

package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"text/template"

"github.com/golang-jwt/jwt"
)

type Account struct {<!-- -->
ID string
pw string
is_admin bool
secret_key string
}

type AccountClaims struct {<!-- -->
Id string `json:"id"`
Is_admin bool `json:"is_admin"`
jwt.StandardClaims
}

type Resp struct {<!-- -->
Status bool `json:"status"`
Msg string `json:"msg"`
}

type TokenResp struct {<!-- -->
Status bool `json:"status"`
Token string `json:"token"`
}

varacc[]Account
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")

func clear_account() {<!-- -->
acc = acc[:1]
}

func get_account(uid string) Account {<!-- -->
for i := range acc {<!-- -->
if acc[i].id == uid {<!-- -->
return acc[i]
}
}
return Account{<!-- -->}
}

func jwt_encode(id string, is_admin bool) (string, error) {<!-- -->
claims := AccountClaims{<!-- -->
id, is_admin, jwt.StandardClaims{<!-- -->},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret_key))
}

func jwt_decode(s string) (string, bool) {<!-- -->
token, err := jwt.ParseWithClaims(s, & amp;AccountClaims{<!-- -->}, func(token *jwt.Token) (interface{<!-- -->}, error) {<! -- -->
return []byte(secret_key), nil
})
if err != nil {<!-- -->
fmt.Println(err)
return "", false
}
if claims, ok := token.Claims.(*AccountClaims); ok & amp; & amp; token.Valid {<!-- -->
return claims.Id, claims.Is_admin
}
return "", false
}

func auth_handler(w http.ResponseWriter, r *http.Request) {<!-- -->
uid := r.FormValue("id")
upw := r.FormValue("pw")
if uid == "" || upw == "" {<!-- -->
return
}
if len(acc) > 1024 {<!-- -->
clear_account()
}
user_acc := get_account(uid)
if user_acc.id != "" & amp; & amp; user_acc.pw == upw {<!-- -->
token, err := jwt_encode(user_acc.id, user_acc.is_admin)
if err != nil {<!-- -->
return
}
p := TokenResp{<!-- -->true, token}
res, err := json.Marshal(p)
if err != nil {<!-- -->
}
w.Write(res)
return
}
w.WriteHeader(http.StatusForbidden)
return
}
func regist_handler(w http.ResponseWriter, r *http.Request) {<!-- -->
uid := r.FormValue("id")
upw := r.FormValue("pw")

if uid == "" || upw == "" {<!-- -->
return
}
if get_account(uid).id != "" {<!-- -->
w.WriteHeader(http.StatusForbidden)
return
}
if len(acc) > 4 {<!-- -->
clear_account()
}
new_acc := Account{<!-- -->uid, upw, false, secret_key}
acc = append(acc, new_acc)
p := Resp{<!-- -->true, ""}
res, err := json.Marshal(p)
if err != nil {<!-- -->
}
w.Write(res)
return
}
func flag_handler(w http.ResponseWriter, r *http.Request) {<!-- -->
token := r.Header.Get("X-Token")
if token != "" {<!-- -->
id, is_admin := jwt_decode(token)
if is_admin == true {<!-- -->
p := Resp{<!-- -->true, "Hi " + id + ", flag is " + flag}
res, err := json.Marshal(p)
if err != nil {<!-- -->
}
w.Write(res)
return
} else {<!-- -->
w.WriteHeader(http.StatusForbidden)
return
}
}
}
func root_handler(w http.ResponseWriter, r *http.Request) {<!-- -->
token := r.Header.Get("X-Token")
if token != "" {<!-- -->
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {<!-- -->
}
tpl.Execute(w, & acc)
} else {<!-- -->

return
}
}
func main() {<!-- -->
admin := Account{<!-- -->admin_id, admin_pw, true, secret_key}
acc = append(acc, admin)
http.HandleFunc("/", root_handler)
http.HandleFunc("/auth", auth_handler)
http.HandleFunc("/flag", flag_handler)
http.HandleFunc("/regist", regist_handler)
log.Fatal(http.ListenAndServe("0.0.0.0:11000", nil))
}

2.WP

The audit code shows that the web site has the following routes:

 http.HandleFunc("/", root_handler) //Template

  http.HandleFunc("/auth", auth_handler) //Log in

  http.HandleFunc("/flag", flag_handler)

  http.HandleFunc("/regist", regist_handler) //Register

First analyze the flag routing

id, is_admin := jwt_decode(token)
if is_admin == true {<!-- -->
p := Resp{<!-- -->true, "Hi " + id + ", flag is " + flag}
res, err := json.Marshal(p)
if err != nil {<!-- -->
}

It is found that jwt forgery needs to be carried out and the value of is_admin is true.

Visit the regist route to register an account and form the original jwt

Access the auth route, log in to the account you just registered, and get a jwt encrypted string.

After decrypting it, the value of is_admin defaults to false.

From this, we only need to add the X-Token request header, and the content is the modified jwt string. Next, we need to perform SSTI through the root route, obtain the jwt encrypted key, and then forge it.

token := r.Header.Get("X-Token")

Analysis of the root route shows that the injection point is in the id part of the jwt encrypted data.

if token != "" {<!-- -->
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {<!-- -->
}
tpl.Execute(w, & acc)
}

We access the register route again and send the following payload to obtain all the information of the acc structure.

regist?id={<!-- -->{<!-- -->.}} & amp;pw=123
//You cannot use {<!-- -->{.secret_key}} to inject the key field, because the acc obtained in the root_handler function is the address in the array, that is, the get_account function searches our user in the global variable acc array. , in this case directly injecting {<!-- -->{.secret_key}} will return empty

Access the author route and get the SSTI ciphertext as follows:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss

Visit the root route again and add the X-Token request header, the value is the jwt ciphertext just now, and get the jwt encryption key (this_is_f4Ke_key)

Go back to the jwt.io website, set the value of is_admin to true, and encrypt

get cipher text

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.3OXFk-f_S2XqPdzHnl0esmJQXuTSXuA1IbpaGOMyvWo

Access the flag route and add the X-Token request header

Successfully got the flag