Phân tích CVE-2022-2185 - Gitlab Remote Command Execution via Project Import
Brief CVE description
Lỗ hổng CVE-2022-2185 cho phép authenticated user có quyền import project thực hiện import một project bị chỉnh sửa có yếu tố độc hại có thể dẫn đến thực thi các command nguy hiểm trên hệ thống chạy Gitlab sử dụng các phiên bản bị ảnh hưởng.
Danh sách các phiên bản bị ảnh hưởng:
- >=14.0, <14.10.5
- >=15.0, <15.0.4
- >=15.1, <15.1.1
Điểm CVSS 3.1:
9.9
(
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
).
Advisory:
https://about.gitlab.com/releases/2022/06/30/critical-security-release-gitlab-15-1-1-released/#remote-command-execution-via-project-imports
Commit:
https://gitlab.com/gitlab-org/gitlab/-/commit/fae2720ffd7ec5ce3eb88e3b68b2879f4f664cf4
Mitigation
- Upgrade lên phiên bản mới nhất của Gitlab.
- Phân quyền người dùng chặt chẽ, hạn chế tối đa quyền hạn của người dùng có thể sử dụng các tính năng không cần thiết.
- Yêu cầu người dùng sử dụng mật khẩu mạnh.
Analysis
Sau khi đọc advisory và commit, ta nhận biết được rằng đây là lỗi Command Injection và đoạn code lỗi nằm ở class
ImportExport::DecompressedArchiveSizeValidator.

Chúng ta có thể thấy rằng Gitlab đã thêm hàm
validate_archive_path
(dùng để validate
@archive_path
có phải là một đường dẫn trỏ đến 1 tập tin hợp lệ hay không) ngay trước khi chạy vào đoạn code thực thi command.
Biến
@archive_path
được đưa trực tiếp vào giá trị của
command
như hình dưới đây:

Từ đó, ta thấy được rằng chúng ta cần phải tìm cách control được
@archive_path
thành input của ta để có thể execute arbitrary command.
Class
DecompressedArchiveSizeValidator
được sử dụng trong 2 modules là:
Sau quá trình review code, ta thấy rằng để control được
@archive_path
, ta cần phải control được
project.import_source
từ
ImportExport::Importer. Code flow của chúng ta như sau:

Theo như commit thì lỗi có liên quan đến phần xử lí cho bulk import nên ta sẽ tập trung vào module này nhiều hơn.
Diff commit review
Sau khi nhìn qua commit, có vài điểm đáng lưu ý sau đây:

Tại 2 file này, chúng ta có thể thấy Gitlab fix cứng việc truyền data thông qua GraphQL, ngăn chặn việc thêm bớt key bằng cách:
- Tạo 1 hash mới tên
project
- Thêm từng attribute có dạng symbol vào
project
- Trả về biến
project
thay vì trả về trực tiếp dữ liệu từ biến data
=> Chúng ta có thể control được dữ liệu này để làm những bước tiếp theo.
Debugging
Để thuận tiện cho việc debug, chúng ta cần cho phép Gitlab truy cập vào các địa chỉ IP trong local bằng cách vào Admin Panel -> Settings -> Network, tại “Outbound requests” section, chọn vào “Allow requests to the local network from web hooks and services”.

Chúng ta hãy bắt đầu bằng việc khởi tạo 1 bulk import session.

Chọn bất kì repo nào cần import và bấm
Import
. Request có dạng như sau:
POST /import/bulk_imports.json HTTP/1.1
Host: gdk.test:3000
Cookie: [REDACTED]
Content-Type: application/json
Content-Length: 155
Connection: close
{"bulk_import":[{"source_type":"group_entity","source_full_path":"gitlab-org","destination_namespace":"gitlab-org","destination_name":"aaa"}]}
Tại file
app/models/bulk_imports/entity.rb, ta thấy chức năng bulk import có hỗ trợ import project bằng cách đổi
source_type
thành
project_entity
.

Với POST data như hình trên, ta đang thực hiện import project
gitlab-org/gitlab-test
để tạo ra project
gitlab-org/aaa
.
Đặt breakpoint tại hàm
create
trong
app/controllers/import/bulk_imports_controller.rb. Khi request vào endpoint trên thì chương trình sẽ dừng tại breakpoint mình vừa đặt tại đây.
BulkImports::CreateService.execute
thực hiện chức năng import project bằng cách thực thi
BulkImportWorker.perform_async
.

Class method
perform_async
gọi đến instance method
perform
của class
BulkImportWorker.

Trong phương thức này, có 1 dòng gọi tới class
BulkImports::CreatePipelineTrackersService
, class này lặp qua các pipelines được định nghĩa trong
lib/bulk_imports/projects/stage.rb.

Mục đích dùng để kiểm tra xem pipeline nào phù hợp để thêm vào tracker nhằm thực hiện chức năng import.

Những pipeline này được thực hiện tuần tự bởi
BulkImports::Pipeline::Runner. Cụ thể như hình dưới đây:
Nhiệm vụ của Runner là lặp qua từng extractor, transformer và loader của pipeline để xử lí. Dữ liệu được lấy từ extractor sẽ được truyền vào cho các transformers xử lí và dữ liệu sau khi qua transformer sẽ được đưa vào loader.
Digging into the root cause
ProjectPipeline là pipeline đầu tiên được đi vào. Lần lượt đi vào các extractor và transformer như đã khai báo:
- GraphqlExtractor được gọi với tham số
query
- là kết quả của Graphql::GetProjectQuery dùng để lấy dữ liệu từ GraphQL endpoint của Gitlab.

- ProhibitedAttributesTransformer lặp qua các attribute trong dữ liệu để bỏ đi những attribute không cho phép.

- ProjectAttributesTransformer cũng chính là transformer đã xuất hiện trong commit sửa lỗi của Gitlab. Nhiệm vụ chính của transformer này là để tạo một dữ liệu mới chứa những thông tin cần thiết, phục vụ cho các pipeline tiếp theo.
Ở đây, biến data chính là dữ liệu được lấy từ GraphqlExtractor
ở trên.
Sau khi khởi tạo các attribute cần thiết cho biến data, data.transform_keys!(&:to_sym)
sẽ tiến hành transform các keys trong biến thành kiểu Symbol.
Như ta thấy, biến data đã được thêm các attributes mới như name
, path
, import_type
, visibility_level
và namespace_id
.
Sau khi hoàn thành xong các extractor và transformer, chúng ta sẽ được nhảy vào hàm
load
để tiếp tục.
Projects::CreateService.execute sẽ được thực thi với tham số
data
ở trên.

Để có thể từ
Projects::CreateService
với
import_type
là
gitlab_project_migration
chạy đến
ImportExport::Importer
, ta search class này trong codebase để xem class này được dùng ở đâu.

Vậy là
ImportExport::Importer
được dùng với type là
gitlab_project
.

Tại phương thức
import_schedule
,
@project.import_state.schedule
được chạy để thực hiện tạo schedule cho import job khi thỏa mãn các điều kiện, có một điều kiện là attribute
import_type
không phải là
gitlab_project_migration
.
Để có thể làm được điều này, đầu phương thức
execute
của có sử dụng class
Projects::CreateFromTemplateService để thực hiện chức năng tạo project dựa trên một built-in template được mô tả tại
đây.

Đi sâu vào class
Projects::CreateFromTemplateService
, chúng ta có thể thấy được phương thức thực hiện thay đổi attribute
import_type
tại
Projects::GitlabProjectsImportService.prepare_import_params
.

Sau khi hoàn thành
prepare_import_params
, nó sẽ tiến tục chạy
Projects::CreateService
một lần nữa với tham số
params
mới có attribute
import_type
là
gitlab_project
.
Sơ đồ của flow như sau:

Quay trở lại lúc lần đầu tiên chạy vào
Projects::CreateService
, điều kiện
create_from_template?
được định nghĩa như sau

Kiểm tra nếu 1 trong 2 attributes là
:template_name
và
:template_project_id
có xuất hiện trong dữ liệu truyền vào
Project::CreateService
.
Như vậy tổng kết lại cả bài viết, những bước exploit của chúng ta như sau:
- Tạo 1 web proxy đứng trước 1 Gitlab server.
- Cho Gitlab connect vào web proxy của ta.
- Thực hiện request import project qua tính năng Bulk import.
- Sửa response (đúng) trả về từ Gitlab của ta để server nhận được response (đã sửa đổi) với attribute mà ta truyền vào.
- RCE!
Nhưng còn đó một điều đặc biệt ở lỗi này, nằm ở
ImportExport::FileImporter.

Trước khi đi vào phương thức
validate_decompressed_archive_size
để thực thi command, nó đi vào phương thức
wait_for_archived_file
trước.

Phương thức này sẽ tiếp tục chương trình nếu
@archive_file
là một đường dẫn file hợp lệ và tồn tại trên hệ thống, ngược lại thì sẽ thử lại 8 lần, mỗi lần sleep 2
n giây, với
n là số lần đã thử.
Sau đó, phương thức này không thực hiện kết thúc mà dùng yield
để thực hiện các đoạn code trong block do..end
nên chúng ta vẫn thực hiện được phương thức validate_decompressed_archive_size
như bình thường.
Khi exploiting,
@archive_file
của ta lúc này không phải là một file hợp lệ nữa nên ta cần đợi
28 - 1 = 255 giây thì chương trình mới tiếp tục.
Results
Kết quả sau khi chờ 0xff giây:
Phân tích CVE-2022-2185 - Gitlab Remote Command Execution via Project Import
Brief CVE description
Lỗ hổng CVE-2022-2185 cho phép authenticated user có quyền import project thực hiện import một project bị chỉnh sửa có yếu tố độc hại có thể dẫn đến thực thi các command nguy hiểm trên hệ thống chạy Gitlab sử dụng các phiên bản bị ảnh hưởng. Danh sách các phiên bản bị ảnh hưởng:9.9
(CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
). Advisory: https://about.gitlab.com/releases/2022/06/30/critical-security-release-gitlab-15-1-1-released/#remote-command-execution-via-project-imports Commit: https://gitlab.com/gitlab-org/gitlab/-/commit/fae2720ffd7ec5ce3eb88e3b68b2879f4f664cf4Mitigation
Analysis
Sau khi đọc advisory và commit, ta nhận biết được rằng đây là lỗi Command Injection và đoạn code lỗi nằm ở class ImportExport::DecompressedArchiveSizeValidator.validate_archive_path
(dùng để validate@archive_path
có phải là một đường dẫn trỏ đến 1 tập tin hợp lệ hay không) ngay trước khi chạy vào đoạn code thực thi command. Biến@archive_path
được đưa trực tiếp vào giá trị củacommand
như hình dưới đây:@archive_path
thành input của ta để có thể execute arbitrary command. ClassDecompressedArchiveSizeValidator
được sử dụng trong 2 modules là:@archive_path
, ta cần phải control đượcproject.import_source
từ ImportExport::Importer. Code flow của chúng ta như sau:Diff commit review
Sau khi nhìn qua commit, có vài điểm đáng lưu ý sau đây:project
project
project
thay vì trả về trực tiếp dữ liệu từ biếndata
Debugging
Để thuận tiện cho việc debug, chúng ta cần cho phép Gitlab truy cập vào các địa chỉ IP trong local bằng cách vào Admin Panel -> Settings -> Network, tại “Outbound requests” section, chọn vào “Allow requests to the local network from web hooks and services”.Import
. Request có dạng như sau: Tại file app/models/bulk_imports/entity.rb, ta thấy chức năng bulk import có hỗ trợ import project bằng cách đổisource_type
thànhproject_entity
.gitlab-org/gitlab-test
để tạo ra projectgitlab-org/aaa
. Đặt breakpoint tại hàmcreate
trong app/controllers/import/bulk_imports_controller.rb. Khi request vào endpoint trên thì chương trình sẽ dừng tại breakpoint mình vừa đặt tại đây.BulkImports::CreateService.execute
thực hiện chức năng import project bằng cách thực thiBulkImportWorker.perform_async
.perform_async
gọi đến instance methodperform
của class BulkImportWorker.BulkImports::CreatePipelineTrackersService
, class này lặp qua các pipelines được định nghĩa trong lib/bulk_imports/projects/stage.rb.Nhiệm vụ của Runner là lặp qua từng extractor, transformer và loader của pipeline để xử lí. Dữ liệu được lấy từ extractor sẽ được truyền vào cho các transformers xử lí và dữ liệu sau khi qua transformer sẽ được đưa vào loader.
Digging into the root cause
ProjectPipeline là pipeline đầu tiên được đi vào. Lần lượt đi vào các extractor và transformer như đã khai báo:query
- là kết quả của Graphql::GetProjectQuery dùng để lấy dữ liệu từ GraphQL endpoint của Gitlab.GraphqlExtractor
ở trên.data.transform_keys!(&:to_sym)
sẽ tiến hành transform các keys trong biến thành kiểu Symbol.name
,path
,import_type
,visibility_level
vànamespace_id
.load
để tiếp tục. Projects::CreateService.execute sẽ được thực thi với tham sốdata
ở trên.Projects::CreateService
vớiimport_type
làgitlab_project_migration
chạy đếnImportExport::Importer
, ta search class này trong codebase để xem class này được dùng ở đâu.ImportExport::Importer
được dùng với type làgitlab_project
.import_schedule
,@project.import_state.schedule
được chạy để thực hiện tạo schedule cho import job khi thỏa mãn các điều kiện, có một điều kiện là attributeimport_type
không phải làgitlab_project_migration
. Để có thể làm được điều này, đầu phương thứcexecute
của có sử dụng class Projects::CreateFromTemplateService để thực hiện chức năng tạo project dựa trên một built-in template được mô tả tại đây.Projects::CreateFromTemplateService
, chúng ta có thể thấy được phương thức thực hiện thay đổi attributeimport_type
tạiProjects::GitlabProjectsImportService.prepare_import_params
.prepare_import_params
, nó sẽ tiến tục chạyProjects::CreateService
một lần nữa với tham sốparams
mới có attributeimport_type
làgitlab_project
. Sơ đồ của flow như sau:Projects::CreateService
, điều kiệncreate_from_template?
được định nghĩa như sau:template_name
và:template_project_id
có xuất hiện trong dữ liệu truyền vàoProject::CreateService
. Như vậy tổng kết lại cả bài viết, những bước exploit của chúng ta như sau:validate_decompressed_archive_size
để thực thi command, nó đi vào phương thứcwait_for_archived_file
trước.@archive_file
là một đường dẫn file hợp lệ và tồn tại trên hệ thống, ngược lại thì sẽ thử lại 8 lần, mỗi lần sleep 2n giây, với n là số lần đã thử.Sau đó, phương thức này không thực hiện kết thúc mà dùng
Khi exploiting,yield
để thực hiện các đoạn code trong blockdo..end
nên chúng ta vẫn thực hiện được phương thứcvalidate_decompressed_archive_size
như bình thường.@archive_file
của ta lúc này không phải là một file hợp lệ nữa nên ta cần đợi 28 - 1 = 255 giây thì chương trình mới tiếp tục.Results
Kết quả sau khi chờ 0xff giây: