# [JMCTF_2021]GoOSS

image-20250318191045829

典型的 json 的页面,有源码可以下载代码审计

package main
import (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"github.com/gin-gonic/gin"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"strings"
	"time"
)
type File struct {
	Content string `json:"content" binding:"required"`
	Name string `json:"name" binding:"required"`
}
type Url struct {
	Url string `json:"url" binding:"required"`
}
func md5sum(data string) string{
	s := md5.Sum([]byte(data))
	return hex.EncodeToString(s[:])
}
func fileMidderware (c *gin.Context){
	fileSystem := http.Dir("./files/")
	if c.Request.URL.String() == "/"{
		c.Next()
		return
	}
	f,err := fileSystem.Open(c.Request.URL.String())
	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 uploadController(c *gin.Context) {
	var file File
	if err := c.ShouldBindJSON(&file); err != nil {
		c.JSON(500, gin.H{"msg": err})
		return
	}
	dir := md5sum(file.Name)
	_,err:= http.Dir("./files").Open(dir)
	if err != nil{
		e := os.Mkdir("./files/"+dir,os.ModePerm)
		_, _ = http.Dir("./files").Open(dir)
		if e != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": e.Error()})
			return
		}
	}
	filename := md5sum(file.Content)
	path := "./files/"+dir+"/"+filename
	err = ioutil.WriteFile(path, []byte(file.Content), os.ModePerm)
	if err != nil{
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(200, gin.H{
		"message": "file upload succ, path: "+dir+"/"+filename,
	})
}
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 文件:

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

基本上就是直接访问这个文件就可以进行文件包含了

在端口 80 上,看来估计就是需要 ssrf 了

在 main.go 发现是 gin 框架

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
}

有两个路由:vul 和 upload 在 1234 端口

看一下这两个路由的函数怎么写的:

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()})
}

只允许 http://127.0.0.1:1234 / 开头的访问,否则 403

再看看其他的代码:

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,
			})
		}

如果传的值是文件夹且 c.Request.URL.String () 不以 “/” 结尾的话,那么就会重定向到 c.Request.URL.String ()+"/",实质上就是进行了一次请求,而 c.Request.URL.String () 是我们可控的,所以这里村子啊 ssrf,但是直接再浏览器中构造发现无论怎么构造最后解析的路由都是 “/”,无法利用,禹是游把目光放到之前的 /vul 路由上

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)

虽然这里的 client.Get () 限制了 url 必须以 http://127.0.0.1:1234 / 开头,但是我们之前发现的那段 302 的代码不也正是这个端口的服务吗,我们可以试试通过这里的请求执行 302 跳转

可以试试http://127.0.0.1:1234//127.0.0.1/..

image-20250318193306629

可以直接读文件了(那么这时候只需要通过 get 传一个文件名过去,就可以读到 flag 文件了,但是我们不能控制 url 的后半段,所以我们只能通过绝对路径来读 flag 文件,但是在最后加上… 之后又会跳转,读不到文件,这时想到 url 带参数的符号是 &,于是加了一个 & 试试,就读到 flag 了。)

{"url":"http://127.0.0.1:1234//127.0.0.1/index.php?file=/flag&../../../.."}

image-20250318193435150

参考链接:BUUCTF-GoOss wp - Yhck - 博客园

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

odiws WeChat Pay

WeChat Pay

odiws Alipay

Alipay

odiws PayPal

PayPal