原文:https://security.szurek.pl/gitea-1-4-0-unauthenticated-rce.html
Gitea主页:
引言
该文档也可从GitHub上下载。
本文是介绍Gitea和Gogs内部漏洞的系列文章中的一部分。
您还可以在YouTube上观看解说视频:Race condition and git hooks vs Gitea server。
Gitea是一个利用Go语言编写的git服务器。
它不仅易于安装,同时还提供了许多有趣的选项。
下面演示的漏洞利用过程涉及多个要素,当它们组合使用时,就可以完全接管该服务器了。
首先,我们需要通过GIT LFS实现中的漏洞来获得app.ini文件的内容。
然后,从这个文件中读取用于为JWT令牌签名的SECRET。
这样一来,我们就能够发送伪造的用户会话文件了。
然后,使用新建的管理员会话创建一个新的存储库。注意,这里需要一个管理员帐户,因为只有管理员才有权修改git hook。
接着,设法把要在服务器上执行的恶意代码放入update hook中。
然后,只要把任意的源代码修改操作推送到存储库,就可以运行恶意代码了。
好了,漏洞利用的原理已经介绍过了,接下来,将详细讨论其中的各个要素。
被遗忘的关键词:return
PostHandler函数的作用是创建新的LFS对象。
表面上看,一切都很正常。例如,如果用户没有相应的权限,则会调用requireAuth函数,并设置相应的WWW-Authenticate头部以及401状态。
但是,当我们深入考察源代码时,就会发现这个函数即使正常使用的话,也会出现问题。
这里的问题在于缺少关键词return:如果有该关键词的话,一旦发生故障,立即终止PostHandler函数。
如果没有关键词return,将执行requireAuth函数,然后,程序会继续执行下一个操作,就这里来说,就是创建LFS对象。
这样的话,我们就可以绕过用户权限的验证机制了。换句话说,现在我们能够为任意的存储库创建任意的LFS对象。
func PostHandler(ctx *context.Context) {
if !setting.LFS.StartServer {
writeStatus(ctx, 404)
return
}
if !MetaMatcher(ctx.Req) {
writeStatus(ctx, 400)
return
}
rv := unpack(ctx)
repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo)
if err != nil {
log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err)
writeStatus(ctx, 404)
return
}
if !authenticate(ctx, repository, rv.Authorization, true) {
requireAuth(ctx)
# !!!!! MISSING RETURN HERE
}
meta, err := models.NewLFSMetaObject(&models.LFSMetaObject{Oid: rv.Oid, Size: rv.Size, RepositoryID: repository.ID})
if err != nil {
writeStatus(ctx, 404)
return
}
ctx.Resp.Header().Set("Content-Type", metaMediaType)
sentStatus := 202
contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
if meta.Existing && contentStore.Exists(meta) {
sentStatus = 200
}
ctx.Resp.WriteHeader(sentStatus)
enc := json.NewEncoder(ctx.Resp)
enc.Encode(Represent(rv, meta, meta.Existing, true))
logRequest(ctx.Req, sentStatus)
}
任意文件读取
getContentHandler函数的作用,是根据其Oid从LFS存储库中检索文件的内容。
首先,它会检查当前用户是否有权读取存储库。这就是我们需要使用公共可用存储库的原因——任何用户(甚至没有登录)都可以从中下载任何文件。
然后,使用ContentStore检索文件的路径。
接着,将LFS_CONTENT_PATH目录与oid参数连接起来。
然后,名为transformKey的函数将为该文件生成新的路径。
func transformKey(key string) string {
if len(key) < 5 {
return key
}
return filepath.Join(key[0:2], key[2:4], key[4:])
}
该路径构建方式是,先取标识符的前两个字符,然后添加一个反斜杠,再取标识符接下来的两个字符,然后又加入一个反斜杠,最后,取标识符的其余部分。
abcdefgh -> ab\cd\efgh
通过用点号替换oid参数,我们可以得到如下结果:
gitea\data\lfs\..\..\custom\conf\app.ini
在Windows平台上, ../ 表示向上移动到父目录。这样的话,我们就能够读取app.ini文件的内容了。
为JWT令牌签名
在配置文件中,已经含有LFS_JWT_SECRET。
APP_NAME = Gitea: Git with a cup of tea
RUN_USER = root
RUN_MODE = prod
[security]
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
INSTALL_LOCK = true
SECRET_KEY = 79jOlo4qSO
[database]
DB_TYPE = sqlite3
HOST = 127.0.0.1:3306
NAME = gitea
USER = gitea
PASSWD =
SSL_MODE = disable
PATH = data/gitea.db
LFS_JWT_SECRET的值就是用于为JWT令牌签名的密钥。将LFS GIT文件发送到服务器时,系统会对这些令牌进行检查。
由于我们知道LFS_JWT_SECRET的值,所以,可以随心所欲地使用任意oid将任意文件发送到任意存储库。
接下来,我们将再次使用“4点”技巧来创建新的LFS对象。但是,这次我们将使用sessions目录的路径作为oid。
....data/sessions/1/1/11customsession
这个目录中的某些文件,存放的就是前登录用户的相关信息。
使用Gitea,我们可以向服务器发送一个名为i_like_gitea的cookie。
服务器将会检查该目录中是否存在名为cookie的文件。
如果有的话,服务器就会读取存储在会话中的当前用户信息。
之后,就可以使用一个伪造的管理员帐户发送我们自己的会话文件了。
Why?听我细细道来。当我们检查允许在服务器上保存文件的函数时,就会发现它的工作原理与用于下载文件的函数完全相同。
它们之间只有一个不同之处,那就是.tmp字符串将添加到正在创建的文件的名称中。
对我们这些攻击者而言,这意味着我们可以将文件发送到任何位置。但是,它的扩展名总是.tmp。
竞争条件
不幸的是,我们无法使用自己发送的会话,因为它被立即从服务器中删除。
实际上,关键字defer就是用来负责这一操作的——Put函数一旦完成其操作,就会立即删除其创建的文件。
为了克服这个限制,我们将搬出一种称为竞争条件的法宝。
将POST请求发送到服务器时,Content-Length头部将与待发送的数据一起传递过去。
它的作用是告诉服务器,用户打算传输多少数据。这样的话,服务器就可以知道用户当前处于数据传输的哪个阶段。
这里的技巧是将这个头部的值设置为一个非常大的数字。
服务器从用户接收数据后,会立即保存到文件中。
但是,该函数会等待传输完成,直到接收到的数据的大小等于Content-Length头部中给出的数字为止。
这样一来,我们的文件就不会被立即删除了。
相反,这样我们就可以争取到几秒钟的时间,如果用于利用我们的会话的话,这些时间就足够了。
Git hooks
现在,我们可以使用伪造的管理员帐户创建一个新的存储库。
接下来,就可以对存储库进行设置了。由于我们的身份是管理员,所以,我们可以访问Git hooks选项。
实际上,所谓hook就是位于各个存储库的.git/hooks目录中的脚本。
在对存储库执行操作时,系统就会执行这些脚本。
例如,在响应git push命令时,Git会执行update脚本。
我们可以把需要执行的命令放到update脚本中,就这里来说,这些命令的运行结果将写入objects/info/exploit文件。
现在,我们只需要将新文件添加到我们的存储库,并通过git push命令将其发送到服务器就行了。
此时,服务器将执行update hook,并将该命令的结果写入名为exploit的文件中。
我们可以通过下载对象来显示该命令的结果:
http://localhost:3000/root/test/objects/info/exploit
看到了吧,在没有登录名和密码的情况下,我们照样能够在远程服务器上执行代码。
POC
https://github.com/kacperszurek/exploits/blob/master/Gitea/gitea_lfs_rce.py