[Go语言]SSTI从0到1

[Go语言]SSTI从0到1

  • 1.Go-web基础及示例
  • 2.参数处理
  • 3.模版引擎
    • 3.1 text/template
    • 3.2 SSTI
  • 4.[LineCTF2022]gotm
    • 1.题目源码
    • 2.WP

1.Go-web基础及示例

package main
import (
	"fmt"
	"net/http"
)
func sayHello(w http.ResponseWriter, r *http.Request) { // 定义一个
	fmt.Fprintln(w, "hello world!")
}
func main() {
	http.HandleFunc("/hello", sayHello)  //浏览器访问/hello,将会转到sayHello去处理
	http.ListenAndServe(":8888", nil) // 监听端口并处理请求
}

其中 “net/http” 是Go自带的实现web服务的模块,可以处理http请求,其中http.HandleFunc是用来绑定路由函数,相当于python的flask的app.route函数

然后就是路由函数sayHello了,这个函数接受两个参数,其中 w 是web接口参数,该变量可发送到前端web页面,r 是http报文参数,用于存放http请求的UA头,表单数据等信息

然后 fmt.Fprintln 函数是用于输出在器(io.Writer)中例如web接口

这里输出 “hello world”,如图所示

[Go语言]SSTI从0到1_第1张图片

2.参数处理

我们将sayHello函数改动如下

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

r.FormValue为接收表单数据的数组,包括GET与POST

处理表单请求的方法如下

1.直接获得

PostFormValue或FormValue方法

前者只能解析Post请求

2.ParseForm()方法间接获得

r.ParseForm()  解析请求的主体,化为键值对组合

键值对会被存储在 r.Form 和 r.PostForm 字段中

  • r.Form 是一个 url.Values 类型,它表示 URL 查询参数和 POST 表单字段的集合。可以通过 r.Form.Get(key)、r.Form[key] 或 r.FormValue(key) 方法来获取表单字段的值。
  • r.PostForm 是一个 url.Values 类型,它只包含 POST 表单字段的数据。与 r.Form 不同的是,r.PostForm 不会自动解析 URL 查询参数或其他非 POST 提交的数据。

示例如下:

[Go语言]SSTI从0到1_第2张图片

3.模版引擎

GO语言提供了两个模板包,一个是 html/template 模块,另一个是 text/template 模块,其中text/template模板引擎与Go语言的SSTI有关

3.1 text/template

示例:

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 := &User{1, "admin", "123456"}
    r.ParseForm()
    arg := strings.Join(r.PostForm["name"], "")
    tpl1 := fmt.Sprintf(`

Hi, ` + arg + `

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() }

首先讲解一下这段代码,它使用了 “text/template” 模板引擎,该模板引擎并没有对传入的数据进行html编码,可导致SSTi,然后定义了一个User结构体,用于给模版传参

type User struct {
    Id     int
    Name   string
    Passwd string
}

然后通过fmt.Sprintf函数将传入的name参数拼接到模版当中,再通过以下语句,利用template.New(“login”)创建了一个名为 login 的模板,然后使用 Parse(tpl1) 方法,被作为模版的字符串存储到login模板中

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

其中html就代表刚才创建的login模板,然后err代表创建模板过程中的报错信息(一般不会有),然后通过以下语句判断模板是否创建成功,若创建成功则继续运行,失败则退出程序并报错

html = template.Must(html, err)

最后通过 Execute 方法进行模版翻译,将user结构体中的值翻译到html的模板中,然后输出到w接口,也就是web前端

html.Execute(w, user)

3.2 SSTI

我们以上面的web源码为例

首先访问 login 路由,然后POST传入一个参数,可以看到回显正常
[Go语言]SSTI从0到1_第3张图片

我们再传入 name={{.Name}},其中双重大括号为该模板引擎的占位符,可以被翻译,我们使用 {{.参数名}} 的格式,可以访问构建模版时所传入的参数

type User struct {
    Id     int
    Name   string
    Passwd string
}

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

如图所示
[Go语言]SSTI从0到1_第4张图片

4.[LineCTF2022]gotm

1.题目源码

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"`
}

var acc []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, &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 && 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 != "" && 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

审计代码可得该web网站共有以下路由

  http.HandleFunc("/", root_handler) //模板

  http.HandleFunc("/auth", auth_handler)  //登陆

  http.HandleFunc("/flag", flag_handler)

  http.HandleFunc("/regist", regist_handler)  //注册

先分析下flag路由

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 {
			}

发现需要进行jwt伪造,让 is_admin 的值为ture

访问regist路由注册账号,形成原始jwt

[Go语言]SSTI从0到1_第5张图片

访问auth路由,登陆刚才注册的账号,得到一段jwt加密的字符串

[Go语言]SSTI从0到1_第6张图片

对其解密得,is_admin的值默认为false

[Go语言]SSTI从0到1_第7张图片

由此可得我们只需添加 X-Token 的请求头即可,内容为修改后的jwt字符串,接下来我们需要通过根路由进行SSTI,得到jwt加密的密钥,然后进行伪造

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

分析根路由可得,注入点在jwt加密数据的id部分

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)
	} 

我们再次访问regist路由,发送以下payload,来获得acc结构体的所有信息

regist?id={{.}}&pw=123
//不能使用{{.secret_key}}注入得到key字段,因为root_handler函数中得到的acc是数组中的地址,也就是get_account函数通过在全局变量acc数组中查找我们的用户,这种情况下直接注入{{.secret_key}}会返回空

访问author路由,得到SSTI的密文如下

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss

再次访问根路由,并添加 X-Token 请求头,值为刚才的jwt密文,得到jwt加密密钥(this_is_f4Ke_key)

[Go语言]SSTI从0到1_第8张图片

回到 jwt.io网站,将is_admin的值设为true,并加密

[Go语言]SSTI从0到1_第9张图片

得到密文

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.3OXFk-f_S2XqPdzHnl0esmJQXuTSXuA1IbpaGOMyvWo

访问flag路由添加X-Token请求头

[Go语言]SSTI从0到1_第10张图片

成功得到flag

你可能感兴趣的:(CTF,web安全,Go语言从0到1,golang,开发语言,web安全,CTF)