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:- >=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/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_pathcó 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ủacommandnhư hình dưới đây:@archive_paththành input của ta để có thể execute arbitrary command. ClassDecompressedArchiveSizeValidatorđược sử dụng trong 2 modules là:- BulkImports::FileDecompressionService
- ImportExport::FileImporter
Sau quá trình review code, ta thấy rằng để control được@archive_path, ta cần phải control đượcproject.import_sourcetừ 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:- Tạo 1 hash mới tên
- Thêm từng attribute có dạng symbol vào
- Trả về biến
=> Chúng ta có thể control được dữ liệu này để làm những bước tiếp theo.projectprojectprojectthay vì trả về trực tiếp dữ liệu từ biếndataDebugging
Để 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_typethànhproject_entity.gitlab-org/gitlab-testđể tạo ra projectgitlab-org/aaa. Đặt breakpoint tại hàmcreatetrong 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.executethực hiện chức năng import project bằng cách thực thiBulkImportWorker.perform_async.perform_asyncgọi đến instance methodperformcủ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:- GraphqlExtractor được gọi với tham số

- 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ừ
Sau khi khởi tạo các attribute cần thiết cho biến data,
Như ta thấy, biến data đã được thêm các attributes mới như
Sau khi hoàn thành xong các extractor và transformer, chúng ta sẽ được nhảy vào hàmquery- 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_levelvànamespace_id.loadđể tiếp tục. Projects::CreateService.execute sẽ được thực thi với tham sốdataở trên.Projects::CreateServicevớiimport_typelàgitlab_project_migrationchạ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_typekhông phải làgitlab_project_migration. Để có thể làm được điều này, đầu phương thứcexecutecủ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_typetạiProjects::GitlabProjectsImportService.prepare_import_params.prepare_import_params, nó sẽ tiến tục chạyProjects::CreateServicemột lần nữa với tham sốparamsmới có attributeimport_typelàgitlab_project. Sơ đồ của flow như sau:Projects::CreateService, điều kiệncreate_from_template?được định nghĩa như sau:template_namevà:template_project_idcó 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:- 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.validate_decompressed_archive_sizeđể thực thi command, nó đi vào phương thứcwait_for_archived_filetrước.@archive_filelà 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..endnên chúng ta vẫn thực hiện được phương thứcvalidate_decompressed_archive_sizenhư bình thường.@archive_filecủ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: