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:

  1. Tạo 1 hash mới tên project
  2. Thêm từng attribute có dạng symbol vào project
  3. 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_levelnamespace_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_typegitlab_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_typegitlab_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: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:

  1. Tạo 1 web proxy đứng trước 1 Gitlab server.
  2. Cho Gitlab connect vào web proxy của ta.
  3. Thực hiện request import project qua tính năng Bulk import.
  4. 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.
  5. 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 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 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:

1.151 lượt xem