HackTheBox [Desires] WriteUp

代码审计

这是道 Challenge 题,看了下分类是 Web ,但是感觉有点偏源码审计了。首先拿到源码,发现目录结构是这样的:

.
├── service
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   ├── services
│   │   ├── http.go
│   │   └── sessions.go
│   ├── static
│   │   └── styles.css
│   ├── utils
│   │   ├── redis.go
│   │   └── response.go
│   └── views
│       ├── admin.html
│       ├── dashboard.html
│       ├── login.html
│       ├── register.html
│       └── upload.html
└── sso
    ├── index.js
    ├── package-lock.json
    └── package.json

其中 service 文件夹是 golang 写的 web app ,而 sso 文件夹是 node 实现的登录模块,提供了 HTTP 的登录接口。

功能逻辑

首先梳理一下大致的逻辑,用户注册之后可以登录网站,使用上传功能,上传压缩文件到 /app/service/uploads ,压缩的文件名会使用 UUID 重命名。

之后会对上传的文件进行解压,解压后的文件会放到 /app/service/files/{username} 文件夹下。此处解压后的文件并不会做任何处理,比如重命名文件或者修改后缀。

登录逻辑

登录逻辑实现的比较反常规,所以感觉大概率突破口就在这里了,需要仔细审一下。

首先是注册模块, RegisterHandler 函数中有一处关键代码:

if strings.ContainsAny(credentials.Username, "/.\\") {
	return utils.ErrorResponse(c, "Invalid Username", http.StatusBadRequest)
}

这控制了用户名在注册的时候不能使用包含 .\/ 这三种字符,也就意味着无法将带有任意路径的用户名写入到数据库中,但是其实此处的限制并没有什么作用。

然后是登录模块,因为这个 LoginHandler 函数比较重要,所以我全贴上来了:

func LoginHandler(c *fiber.Ctx) error {
	var credentials Credentials
	if err := c.BodyParser(&credentials); err != nil {
		return utils.ErrorResponse(c, err.Error(), http.StatusBadRequest)
	}

	sessionID := fmt.Sprintf("%x", sha256.Sum256([]byte(strconv.FormatInt(time.Now().Unix(), 10))))

	err := PrepareSession(sessionID, credentials.Username)

	if err != nil {
		return utils.ErrorResponse(c, "Error wrong!", http.StatusInternalServerError)
	}

	user, err := loginUser(credentials.Username, credentials.Password)
	if err != nil {
		return utils.ErrorResponse(c, "Invalid username or Password", http.StatusBadRequest)
	}

	sessId := CreateSession(sessionID, user)

	cookie := fiber.Cookie{
		Name:    "session",
		Value:   sessId,
		Expires: time.Now().Add(3600 * time.Hour),
	}

	c.Cookie(&cookie)

	usernameCookie := fiber.Cookie{
		Name:    "username",
		Value:   credentials.Username,
		Expires: time.Now().Add(3600 * time.Hour),
	}

	c.Cookie(&usernameCookie)

	return c.Redirect("/user/upload")
}

首先可以看到 session 的生成逻辑是存在问题的,完全通过时间戳生成,是有可能被爆破的,当然,是要有 admin 权限的用户登录的情况下,并且知道用户名,才可能爆破,在这个 challenge 的情况下,应该不会有其他人登录 admin 权限账号,所以暂时没法利用。

PrepareSession 这个函数就比较有意思了,它只做了一件事,就是将用户名作为 key ,把生成的 Session 作为 value 写到一个 redis 中。

CreateSession 会将生成的 session 作为文件保存到 /tmp/sessions/{username} 中。Session 文件的格式是 JSON,目标就是把当前用户的 role 变成 admin 。

{"username":"test","id":1,"role":"user"}

最后是一个中间件函数 SessionMiddleware ,在访问任何 /user 路径下都会先调用这个函数:

func SessionMiddleware(c *fiber.Ctx) error {
	sessionID := c.Cookies("session")
	username := c.Cookies("username")
	if sessionID == "" || username == "" {
		return c.SendStatus(http.StatusUnauthorized)
	}

	session, err := GetSession(username)
	if err != nil {
		return c.SendStatus(http.StatusInternalServerError)
	}

	c.Locals("user", *session)

	return c.Next()
}

大概功能就是通过 Cookies 中的用户名去拿 Session 内容,其实此处的代码也是有问题的,可以拿到任意用户的 Session ,也就是说只要有用户是 admin 权限,并且知道用户名,就可以访问 /user/admin 了,但是对于这道题来说并没有这么一个用户。

func GetSession(username string) (*User, error) {
	sessionID, err := utils.RedisClient.Get(username)
	if err != nil {
		return nil, err
	}
	sessionJSON, err := os.ReadFile(filepath.Join("/tmp/sessions", username, sessionID))
	if err != nil {
		return nil, err
	}
	var session User
	err = json.Unmarshal(sessionJSON, &session)

	if err != nil {
		return nil, err
	}
	return &session, nil
}

GetSession 中,会先从 redis 中查询 cookies 的 username 字段,如果没有这么个用户,就会直接返回,如果查询成功,就会从 /tmp/sessions/{username} 中查到 session 并返回。

漏洞利用

mp

评论