专栏名称: 天融信阿尔法实验室
天融信阿尔法实验室将不定期推出技术研究新方向成果,专注安全攻防最前沿技术
51好读  ›  专栏  ›  天融信阿尔法实验室

CVE-2021-22205 GitLab RCE之未授权访问深入分析(一)

天融信阿尔法实验室  · 公众号  ·  · 2021-11-29 17:11

正文




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] 两位师傅和团队内的师傅的帮助,他们的文章和指点给予了我许多好的思路。


0x02 gitlab介绍


GitLab是由GitLabInc.开发,使用MIT许可证的基于网络的Git仓库管理工具,且具有wiki和issue跟踪功能。使用Git作为代码管理工具,并在此基础上搭建起来的web服务。GitLab由乌克兰程序员DmitriyZaporozhets和ValerySizov开发。后端框架采用的是Ruby on Rails,它使用 Ruby语言 写成。后来,一些部分用 Go语言 重写。gitlab-ce即为社区免费版,gitlab-ee为企业收费版。下面附上两张GitLab的单机部署架构图介绍其相应组件。

可以看到在gitlab的组成中包含的各种组件,可以通过两个关键入口访问,分别是HTTP/HTTPS(TCP 80,443)和SSH(TCP 22),请求通过nginx转发到Workhorse,然后Workhorse和Puma进行交互,这里我们着重介绍下通过Web访问的组件GitLab Workhorse。

Puma 是一个用于 Ruby 应用程序的简单、快速、多线程和高度并发的 HTTP 1.1 服务器,用于提供GitLab网页和API。从 GitLab 13.0 开始,Puma成为了默认的Web服务器,替代了之前的Unicorn。而在GitLab 14.0中,Unicorn 从Linux 包中删除,只有Puma可用。


0x03 GitLab Workhorse


GitLab Workhorse是一个使用go语言编写的敏捷反向代理。在 gitlab_features [6] 说明中可以总结大概的内容为,它会处理一些大的HTTP请求,比如 文件上传 、文件下载、Git push/pull和Git包下载。其它请求会反向代理到GitLab Rails应用。可以在 GitLab [7] 的项目路径 lib/support/nginx/gitlab 中的nginx配置文件内看到其将请求转发给了GitLab Workhorse。默认采用了unix socket进行交互。

这篇文档还写到,GitLab Workhorse在实现上会起到以下作用:

  • 理论上所有向gitlab-Rails的请求首先通过上游代理,例如 NGINX 或 Apache,然后将到达gitlab-Workhorse。
  • workhorse 能处理一些无需调用 Rails 组件的请求,例如静态的 js/css 资源文件,如以下的路由注册:
u.route(
 ""`^/assets/`,//匹配路由
  //处理静态文件
 static.ServeExisting(
  u.URLPrefix,
  staticpages.CacheExpireMax,
  assetsNotFoundHandler,
 ),
 withoutTracing(), // Tracing on assets is very noisy
)
  • workhorse能修改Rails组件发来的响应。例如:假设你的Rails组件使用 send_file ,那么gitlab-workhorse将会打开磁盘中的文件然后把文件内容作为响应体返回给客户端。
  • gitlab-workhorse能接管向Rails组件询问操作权限后的请求,例如处理 git clone 之前得确认当前客户的权限,在向Rails组件询问确认后workhorse将继续接管 git clone 的请求,如以下的路由注册:
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 组件之前的请求信息。例如:当处理 Git LFS 上传时,workhorse 首先向 Rails 组件询问当前用户是否有执行权限,然后它将请求体储存在一个临时文件里,接着它将修改过后的包含此临时文件路径的请求体发送给 Rails 组件。
  • workhorse 能管理与 Rails 组件通信的长时间存活的websocket连接,代码如下:
  // Terminal websocket
  u.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的默认启动参数



0x04 Go语言前置知识


我会简要介绍一下漏洞涉及的相关语言前置知识,这样才能够更深入的理解该漏洞,并将相关知识点串联起来,达到举一反三。

函数、方法和接口

在golang中函数和方法的定义是不同的,看下面一段代码

package main

//Person接口
type Person interface{
 isAdult() bool
}

//Boy结构体
type Boy struct {
 Name string
 Age  int
}

//函数
func NewBoy(name string, age int) *Boy {
 return &Boy{
  Name: name,
  Age:  age,
 }
}

//方法
func (p *Boy) isAdult() bool {
 return p.Age > 18
}

func main() {
 //结构体调用
 b := NewBoy("Star"18)
 println(b.isAdult())

 //将接口赋值b,使用接口调用
 var p Person = b
 println(p.isAdult())//false
}

其中 NewBoy 为函数, isAdult 为方法。他们的区别是方法在func后面多了一个接收者参数,这个接受者可以是一个结构体或者接口,你可以把他当做某一个"类",而 isAdult 就是实现了该类的方法。

通过 & 取地址操作可以将一个结构体实例化,相当于 new ,可以看到在 NewBoy 中函数封装了这种操作。在main函数中通过调用 NewBoy 函数实例化Boy结构体,并调用了其方法 isAdult

关于接口的实现在Go语言中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。Go语言中没有类似于implements 的关键字。Go编译器将自动在需要的时候检查两个类型之间的实现关系。 在类型中添加与接口签名一致的方法就可以实现该方法。 isAdult 的参数和返回值均与接口 Person 中的方法一致。所以在main函数中可以直接将定义的接口 p 赋值为实例结构体 b 。并进行调用。

net/http

在golang中可以通过几行代码轻松实现一个http服务

package main

import (
     "net/http"
     "fmt"
)

func main() {
    http.HandleFunc("/", h)
    http.ListenAndServe(":2333",nil)
}
func h(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello world")
}

其中的 http.HandleFunc() 是一个注册函数,用于注册路由。具体实现为绑定路径 / 和处理函数 h 的对应关系,函数 h 的类型是 (w http.ResponseWriter, r *http.Request) 。而 ListenAndServe() 函数封装了底层TCP通信的实现逻辑进行连接监听。第二个参数用于全局请求处理。如果没有传入自定义的handler。则会使用默认的 DefaultServeMux 对象处理请求最后到达 h 处理函数。

type Handler interface {
 ServeHTTP(ResponseWriter, *Request)
}

在go中的任何结构体,只要实现了上方的 ServeHTTP 方法,也就是实现了 Handler 接口,并进行了路由注册。内部就会调用其ServeHTTP方法处理请求并返回响应。但是我们看到函数 h 并不是一个结构体方法,为什么可以处理请求呢?原来在 http.HandleFunc() 函数调用后,内部还会调用 HandlerFunc(func(ResponseWriter, *Request)) 将传入的函数 h 转换为一个具有ServeHTTP方法的handler。

具体定义如下。 HandlerFunc 为一个函数类型,类型为 func(ResponseWriter, *Request) 。这个类型有一个方法为 ServeHTTP ,实现了这个方法就实现了Handler接口, HandlerFunc 就成了一个Handler。上方的调用就是类型转换。

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
 f(w, r)
}

当调用其ServeHTTP方法时就会调用函数 h 本身。

中间件

框架中还有一个重要的功能是中间件,所谓中间件,就是连接上下级不同功能的函数或者软件。通常就是包裹函数为其提供和添加一些功能或行为。前文的 HandlerFunc 就能把签名为 func(w http.ResponseWriter, r *http.Reqeust) 的函数 h 转换成handler。这个函数也算是中间件。

了解实现概念,在具有相关基础知识前提下就可以尝试着手动进行实践,达到学以致用,融会贯通。下面就来动手实现两个中间件 LogMiddleware AuthMiddleware ,一个用于日志记录的,一个用于权限校验。可以使用两种写法。

  • 写法一
package main

import (
   "log"
   "net/http"
   "time"
   "encoding/json"
)

//权限认证中间件
type AuthMiddleware struct {
   Next http.Handler
}


//日志记录中间件
type LogMiddleware struct {
   Next http.Handler //这里为AuthMiddleware
}

//返回信息结构体
type Company struct {
   ID int
   Name string
   Country string
}

//权限认证请求处理
func (am *AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   //如果没有嵌套中间件则使用默认的DefaultServeMux
   if am.Next == nil {
    am.Next = http.DefaultServeMux
   }

   //判断Authorization头是否不为空
   auth := r.Header.Get("Authorization")
   if auth != "" {
    am.Next.ServeHTTP(w, r)
   }else{
    //返回401
    w.WriteHeader(http.StatusUnauthorized)
   }
}


//日志请求处理
func (am *LogMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   if am.Next == nil {
    am.Next = http.DefaultServeMux
   }
   
   start := time.Now()
   //打印请求路径
   log.Printf("Started %s %s", r.Method, r.URL.Path)

   //调用嵌套的中间件,这里为AuthMiddleware
   am.Next.ServeHTTP(w, r)
   //打印请求耗时
   log.Printf("Comleted %s in %v", r.URL.Path, time.Since(start))
}


func main() {
   //注册路由
   http.HandleFunc("/user"func(w http.ResponseWriter, r *http.Request) {
    //实例化结构体返回json格式数据
    c := &Company{
     ID:123,
     Name:"TopSec",
     Country: "CN",
    }
    enc := json.NewEncoder(w)
    enc.Encode(c)
   })
   
   //监听端口绑定自定义中间件
   http.ListenAndServe(":8000",&LogMiddleware{
    Next:new(AuthMiddleware),
   })
}

上方代码中手动声明了两个结构体 AuthMiddleware LogMiddleware ,实现了handler接口的 ServeHTTP 方法。在 ListenAndServe 中通过传入结构体变量嵌套绑定了这两个中间件。

当收到请求时会首先调用 LogMiddleware 中的 ServeHTTP 方法进行日志打印,其后调用 AuthMiddleware 中的 ServeHTTP 方法进行权限认证,最后匹配路由 /user ,调用转换好的handler处理器返回JSON数据,如下图。

当权限认证失败会返回401状态码。

  • 写法二
package main

import (
 "log"
 "net/http"
 "time"
 "encoding/json"
)

//返回信息
type Company struct {
 ID int
 Name string
 Country string
}

//权限认证中间件
func AuthHandler(next http.Handler) http.Handler {
 //这里使用HandlerFunc将函数包装成了httpHandler并返回给LogHandler的next
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){

 //如果没有嵌套中间件则使用默认的DefaultServeMux
 if next == nil {
  next = http.DefaultServeMux
 }

 //判断Authorization头是否不为空
 auth := r.Header.Get("Authorization")
 if auth != "" {
  next.ServeHTTP(w, r)
 }else{
  //返回401
  w.WriteHeader(http.StatusUnauthorized)
 }
 })
}


//日志请求中间件
func LogHandler(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
 if next == nil {
  next = http.DefaultServeMux
 }
 start := time.Now()
 //打印请求路径
 log.Printf("Started %s %s", r.Method, r.URL.Path)

 //调用嵌套的中间件,这里为AuthMiddleware
 next.ServeHTTP(w, r)
 //打印请求耗时
 log.Printf("Comleted %s in %v", r.URL.Path, time.Since(start))
 })
}

func main() {
 //注册路由
 http.HandleFunc("/user"func(w http.ResponseWriter, r *http.Request) {
  //实例化结构体返回json格式数据
  c := &Company{
   ID:123,
   Name:"TopSec",
   Country: "CN",
  }
  enc := json.NewEncoder(w)
  enc.Encode(c)
 })
 
 //监听端口绑定自定义中间件
 http.ListenAndServe(":8000",LogHandler(AuthHandler(nil)))

}

写法二和写法一的区别在于写法一手动实现了 ServeHTTP 方法,而写法二使用函数的形式在其内部通过 HandlerFunc 的转换返回了一个handler处理器,这个handler实现了 ServeHTTP 方法,调用 ServeHTTP 方法则会调用其本身,所以同样也能当做中间件做请求处理。

提供两种方式的原因是当存在一个现有的类型需要转换为handler时只需要添加一个 ServeHTTP 方法即可。关于http和中间件更详细的分析就不在这里一一展开了,感兴趣的读者可以参考这两篇文章: net/http库源码笔记 [8] Go的http包详解 [9]


0x05 ruby语言前置知识


在ruby中当要调用方法时,可以不加括号只使用方法名。实例变量使用@开头表示。

元编程

通过元编程是可以在运行时动态地操作语言结构(如类、模块、实例变量等)

instance_variable_get(var) 方法可以取得并返回对象的实例变量var的值。

instance_variable_set(var, val) 方法可以将val的值赋值给对象实例变量var并返回该值。

instance_variable_defined(var) 方法可以判断对象实例变量var是否定义。

yield 关键字

函数调用时可以传入语句块替换其中的yield关键字并执行。如下示例:

def a
  return 4
end

def b
  puts yield
end

b{a+1}

调用b时会将yield关键字替换为语句块a+1,所以会调用a返回4然后加上1打印5。

Web框架rails

  • 路由

在rails中的路由文件一般位于 config/routes.rb 下,在路由里面可以将请求和处理方法关联起来,交给指定controller里面的action,如下形式:

  post 'account/setting/:id',
    to: 'account#setting',
    constraints: { id: /[A-Z]\d{5}/ }

account/setting/ 是请求的固定url, :id 表示带参数的路由。to表示交给 account controller下的action setting 处理。constraints定义了路由约束,使用正则表达式来对参数 :id 进行约束。

  • 过滤器

rails中可以插入定义好的类方法实现 过滤器 [10] ,一般分为 before_action after_action around_action 分别表示调用action"之前"、"之后"、"围绕"需要执行的操作。如:

before_action :find_productonly: [:show]

上方表示在执行特定 Action show 之前,先去执行 find_product 方法。

还可以使用 skip_before_action 跳过之前 before_action 指定的方法。

class ApplicationController 
  before_action :require_login
end

class LoginsController 
  skip_before_action :require_loginonly: [:new:create]
end

如在父类 ApplicationController 定义了一个 before_action ,在子类可以使用 skip_before_action 跳过,只针对于 new create 的调用。


0x06 漏洞简要介绍


根于gitlab的 官方漏洞issues [11] 来看,当访问接口 /uploads/user 上传图像文件时,GitLab Workhorse会将扩展名为jpg、jpeg、tiff文件传递给ExifTool。用于删除其中不合法的标签。具体的标签在 workhorse/internal/upload/exif/exif.go 中的 startProcessing 方法中有定义,为白名单处理,函数内容如下:

func (c *cleaner) startProcessing(stdin io.Reader) error {
 var err error
  //白名单标签
 whitelisted_tags := []string{
  "-ResolutionUnit",
  "-XResolution",
  "-YResolution",
  "-YCbCrSubSampling",
  "-YCbCrPositioning",
  "-BitsPerSample",
  "-ImageHeight",
  "-ImageWidth",
  "-ImageSize",
  "-Copyright",
  "-CopyrightNotice",
  "-Orientation",
 }

  //传入参数
 args := append([]string{"-all=""--IPTC:all""--XMP-iptcExt:all""-tagsFromFile""@"}, whitelisted_tags...)
 args = append(args, "-")
  
  //使用CommandContext执行命令调用exiftool
 c.cmd = exec.CommandContext(c.ctx, "exiftool", args...)

  //获取输出和错误
 c.cmd.Stderr = &c.stderr
 c.cmd.Stdin = stdin

 c.stdout, err = c.cmd.StdoutPipe()
 if err != nil {
  return fmt.Errorf("failed to create stdout pipe: %v", err)
 }

 if err = c.cmd.Start(); err != nil {
  return fmt.Errorf("start %v: %v", c.cmd.Args, err)
 }

 return nil
}

而ExifTool在解析文件的时候会忽略文件的扩展名,尝试根据文件的内容来确定文件类型,其中支持的类型有DjVu。

DjVu是由AT&T实验室自1996年起开发的一种图像压缩技术,已发展成为标准的图像文档格式之一

ExifTool是一个独立于平台的Perl库,一款能用作多功能图片信息查看工具。可以解析出照片的exif信息,可以编辑修改exif信息,用户能够轻松地进行查看图像文件的EXIF信息,完美支持exif信息的导出。

关键在于ExifTool在解析DjVu注释的 ParseAnt 函数中存在漏洞,所以我们就可以通过构造DjVu文件并插入恶意注释内容将其改为jpg后缀上传,因为gitlab并未在这个过程中验证文件内容是否是允许的格式,最后让ExifTool以DjVu形式来解析文件,造成了ExifTool代码执行漏洞。

该漏洞存在于ExifTool的7.44版本以上,在12.4版本中修复。Gitlab v13.10.2使用的ExifTool版本为11.70。并且接口 /uploads/user 可通过获取的X-CSRF-Token和未登录Session后来进行未授权访问。最终造成了GitLab未授权的远程代码执行漏洞。


0x07 漏洞补丁分析


根据官方通告得知安全版本之一有13.10.3,那么我们直接切换到分支13.10.3查看 补丁提交记录 [12] 即可,打开页面发现在4月9日和11日有两个关于本次漏洞的commits,在其后的4月13日进行了合并。

在commit Check content type before running exiftool 中添加了 isTIFF isJPEG 两个方法到 workhorse/internal/upload/rewrite.go 分别对TIFF文件解码或读取JPEG前512个字节来进行文件类型检测。

func isTIFF(r io.Reader) bool 
  //对TIFF文件解码
 _, err := tiff.Decode(r)
 if err == nil {
  return true
 }
 if _, unsupported := err.(tiff.UnsupportedError); unsupported {
  return true
 }
 return false
}

func isJPEG(r io.Reader) bool {
  //读取JPEG前512个字节
 // Only the first 512 bytes are used to sniff the content type.
 buf, err := ioutil.ReadAll(io.LimitReader(r, 512))
 if err != nil {
  return false
 }
 return http.DetectContentType(buf) == "image/jpeg"
}

在commit Detect file MIME type before checking exif headers 中添加了方法 check_for_allowed_types lib/gitlab/sanitizers/exif.rb 检测mime_type是否为JPG或TIFF。

      def check_for_allowed_types(contents)
        mime_type = Gitlab::Utils::MimeType.from_string(contents)
        unless ALLOWED_MIME_TYPES.include?(mime_type)
          raise "File type #{mime_type} not supported. Only supports #{ALLOWED_MIME_TYPES.join(", ")}."
        end
      end

不过在rails中的exiftool调用是以 Rake任务 [13] 存在的。以下是rails中的rake文件,位于 lib/tasks/gitlab/uploads/sanitize.rake

namespace :gitlab do
  namespace :uploads do
    namespace :sanitize do
      desc 'GitLab | Uploads | Remove EXIF from images.'
      task :remove_exif, [:start_id:stop_id:dry_run:sleep_time:uploader:since] => :environment do |task, args|
        args.with_defaults(dry_run: 'true')
        args.with_defaults(sleep_time: 0.3)

        logger = Logger.new(STDOUT)

        sanitizer = Gitlab::Sanitizers::Exif.new(logger: logger)
        sanitizer.batch_clean(start_id: args.start_id, stop_id: args.stop_id,
                              dry_run: args.dry_run != 'false',
                              sleep_time: args.sleep_time.to_f,
                              uploader: args.uploader,
                              since: args.since)
      end
    end
  end
end

Rake是一门构建语言,和make和ant很像。Rake是用Ruby写的,它支持它自己的DSL用来处理和维护 Ruby应用程序。Rails用rake的扩展来完成多种不同的任务。


0x08 漏洞复现分析


网上最开始流传的方式为通过 后台上传 [14] 恶意JPG格式文件触发代码执行。从之后流出的在野利用分析来看,上传接口 /uploads/user 其实并不需要认证,也就是未授权的RCE,只需要获取到CSRF-Token和未登录session即可。该漏洞的触发流程可大概分为两种,下面将一一介绍。


0x09 漏洞调试环境搭建


本次调试由于本地GitLab Development Kit环境搭建未果,最后选择了两种不同的方式来完成本次漏洞分析的调试,关于workhorse调试环境使用gitlab官方docker配合vscode进行调试,官方docker拉取

docker run -itd  \
 -p 1180:80 \
 -p 1122:22 \
 -v /usr/local/gitlab-test/etc:/etc/gitlab  \
 -v /usr/local/gitlab-test/log:/var/log/gitlab \
 -v /usr/local/gitlab-test/opt:/var/opt/gitlab \
 --restart always \
 --privileged=true \
 --name gitlab-test \
 gitlab/gitlab-ce:13.10.2-ce.0

运行docker后在本地使用命令 ps -aux | grep "workhorse" 可查看workhorse进程ID。

新建目录 /var/cache/omnibus/src/gitlab-rails/workhorse/ 将workhorse源码复制到其下。安装vscode后打开上述目录按提示安装go全部的相关插件,然后添加调试配置,使用dlv attach模式。填入进程PID。下断点开启调试即可正常调试。

"configurations": [
  {
    "name""Attach to Process",
    "type""go",
    "request""attach",
    "mode""local",
    "processId"6257
  }
]

关于rails部分的调试环境使用 gitpod [15] 云端一键搭建的GitLab Development Kit。首先fork仓库后选择指定分支点击gitpod即可进行搭建。rails参考 pry-shell [16] 来进行调试。在gitpod中也可以进行workhorse的调试,同样根据提示安装全部go相关插件

由于gitpod的vscode环境不是root,无法直接在其中Attach to Process进行调试,所以可以本地使用sudo起一个远程调试的环境

sudo /home/gitpod/.asdf/installs/golang/1.17.2/packages/bin/dlv-dap attach 38489 --headless --api-version=2 --log --listen=:2345

相关调试配置

"configurations": [
  {
    "name""Connect to server",
    "type""go",
    "request""attach",
    "mode""remote",
    "remotePath""${workspaceFolder}",
    "port"2345,
    "host""127.0.0.1"
  }
]


0x10 漏洞代码分析-触发流程一

01

workhorse路由匹配

在workhorse的更新中涉及函数有 NewCleaner ,在存在漏洞的版本13.10.2中跟踪到该函数,其中调用到 startProcessing 来执行exiftool命令,具体内容可以看之前贴的代码

func NewCleaner(ctx context.Context, stdin io.Reader) (io.ReadCloser, error) {
 c := &cleaner{ctx: ctx}

 if err := c.startProcessing(stdin); err != nil {
  return nil, err
 }

 return c, nil
}

右键该方法浏览调用结构

从上图中除去带test字样的测试函数,可以看出最终调用点只有两个,upload包下的Handler函数 Accelerate ,和artifacts包下的Handler函数 UploadArtifacts 。现在还暂时不确定是哪个函数,根据前面的漏洞描述信息我们知道对接口 /uploads/user 的处理是整个调用链的开始,所以直接在源码中全局搜索该接口

由于请求会先经过GitLab Workhorse,我们可以直接在上图中确定位于 workhorse/internal/upstream/routes.go 路由文件中的常量 userUploadPattern ,下面搜索一下对该常量的引用







请到「今天看啥」查看全文