0x01前言
安全研究员vakzz于4月7日在hackerone上提交了一个关于gitlab的RCE漏洞[1],在当时并没有提及是否需要登录gitlab进行授权利用,在10月25日该漏洞被国外安全公司通过日志分析发现未授权的在野利用[2],并发现了新的利用方式。根据官方漏洞通告[3]页面得知安全的版本为13.10.3、13.9.6和13.8.8。我将分篇深入分析该漏洞的形成以及触发和利用。本篇将复现分析携带恶意文件的请求是如何通过gitlab传递到exiftool进行解析的,接下来将分析exiftool漏洞的原理和最后的触发利用。预计会有两到三篇。希望读者能读有所得,从中收获到自己独特的见解。在本篇文章的编写中要感谢
chybeta[4]和rebirthwyw[5]两位师傅和团队内的师傅的帮助,他们的文章和指点给予了我许多好的思路。0x02gitlab介绍
GitLab是由GitLabInc.开发,使用MIT许可证的基于网络的Git仓库管理工具,且具有wiki和issue跟踪功能。使用Git作为代码管理工具,并在此基础上搭建起来的web服务。GitLab由乌克兰程序员DmitriyZaporozhets和ValerySizov开发。后端框架采用的是RubyonRails,它使用Ruby语言写成。后来,一些部分用Go语言重写。gitlab-ce即为社区免费版,gitlab-ee为企业收费版。下面附上两张GitLab的单机部署架构图介绍其相应组件。
可以看到在gitlab的组成中包含的各种组件,可以通过两个关键入口访问,分别是HTTP/HTTPS(TCP80,)和SSH(TCP22),请求通过nginx转发到Workhorse,然后Workhorse和Puma进行交互,这里我们着重介绍下通过Web访问的组件GitLabWorkhorse。
Puma是一个用于Ruby应用程序的简单、快速、多线程和高度并发的HTTP1.1服务器,用于提供GitLab网页和API。从GitLab13.0开始,Puma成为了默认的Web服务器,替代了之前的Unicorn。而在GitLab14.0中,Unicorn从Linux包中删除,只有Puma可用。
0x03GitLabWorkhorse
GitLabWorkhorse是一个使用go语言编写的敏捷反向代理。在gitlab_features[6]说明中可以总结大概的内容为,它会处理一些大的HTTP请求,比如文件上传、文件下载、Gitpush/pull和Git包下载。其它请求会反向代理到GitLabRails应用。可以在GitLab[7]的项目路径lib/support/nginx/gitlab中的nginx配置文件内看到其将请求转发给了GitLabWorkhorse。默认采用了unixsocket进行交互。
这篇文档还写到,GitLabWorkhorse在实现上会起到以下作用:
理论上所有向gitlab-Rails的请求首先通过上游代理,例如NGINX或Apache,然后将到达gitlab-Workhorse。workhorse能处理一些无需调用Rails组件的请求,例如静态的js/css资源文件,如以下的路由注册:u.route("",`^/assets/`,//匹配路由//处理静态文件static.ServeExisting(u.URLPrefix,staticpages.CacheExpireMax,assetsNotFoundHandler,),withoutTracing(),//Tracingonassetsisverynoisy)workhorse能修改Rails组件发来的响应。例如:假设你的Rails组件使用send_file,那么gitlab-workhorse将会打开磁盘中的文件然后把文件内容作为响应体返回给客户端。gitlab-workhorse能接管向Rails组件询问操作权限后的请求,例如处理gitclone之前得确认当前客户的权限,在向Rails组件询问确认后workhorse将继续接管gitclone的请求,如以下的路由注册:
u.route("GET",gitProjectPattern+`info/refs\z`,git.GetInfoRefsHandler(api)),u.route("POST",gitProjectPattern+`git-upload-pack\z`,contentEncodingHandler(git.UploadPack(api)),withMatcher(isContentType("application/x-git-upload-pack-request"))),u.route("POST",gitProjectPattern+`git-receive-pack\z`,contentEncodingHandler(git.ReceivePack(api)),withMatcher(isContentType("application/x-git-receive-pack-request"))),u.route("PUT",gitProjectPattern+`gitlab-lfs/objects/([0-9a-f]{64})/([0-9]+)\z`,lfs.PutStore(api,signingProxy,preparers.lfs),withMatcher(isContentType("application/octet-stream")))workhorse能修改发送给Rails组件之前的请求信息。例如:当处理GitLFS上传时,workhorse首先向Rails组件询问当前用户是否有执行权限,然后它将请求体储存在一个临时文件里,接着它将修改过后的包含此临时文件路径的请求体发送给Rails组件。workhorse能管理与Rails组件通信的长时间存活的websocket连接,代码如下:
//Terminalwebsocketu.wsRoute(projectPattern+`-/environments/[0-9]+/terminal.ws\z`,channel.Handler(api)),u.wsRoute(projectPattern+`-/jobs/[0-9]+/terminal.ws\z`,channel.Handler(api)),
使用ps-aux
grep"workhorse"命令可以看到gitlab-workhorse的默认启动参数
0x04Go语言前置知识
我会简要介绍一下漏洞涉及的相关语言前置知识,这样才能够更深入的理解该漏洞,并将相关知识点串联起来,达到举一反三。
函数、方法和接口
在golang中函数和方法的定义是不同的,看下面一段代码
packagemain//Person接口typePersoninterface{isAdult()bool}//Boy结构体typeBoystruct{NamestringAgeint}//函数funcNewBoy(namestring,ageint)*Boy{returnBoy{Name:name,Age:age,}}//方法func(p*Boy)isAdult()bool{returnp.Age18}funcmain(){//结构体调用b:=NewBoy("Star",18)println(b.isAdult())//将接口赋值b,使用接口调用varpPerson=bprintln(p.isAdult())//false}
其中NewBoy为函数,isAdult为方法。他们的区别是方法在func后面多了一个接收者参数,这个接受者可以是一个结构体或者接口,你可以把他当做某一个"类",而isAdult就是实现了该类的方法。
通过取地址操作可以将一个结构体实例化,相当于new,可以看到在NewBoy中函数封装了这种操作。在main函数中通过调用NewBoy函数实例化Boy结构体,并调用了其方法isAdult。
关于接口的实现在Go语言中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。Go语言中没有类似于implements的关键字。Go编译器将自动在需要的时候检查两个类型之间的实现关系。在类型中添加与接口签名一致的方法就可以实现该方法。如isAdult的参数和返回值均与接口Person中的方法一致。所以在main函数中可以直接将定义的接口p赋值为实例结构体b。并进行调用。
net/