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 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:
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:
Đ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
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ủacommand
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 đượcproject.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:
project
project
project
thay vì trả về trực tiếp dữ liệu từ biếndata
=> 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: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ànhproject_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 projectgitlab-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 thiBulkImportWorker.perform_async
.Class method
perform_async
gọi đến instance methodperform
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:
query
– là kết quả của Graphql::GetProjectQuery dùng để lấy dữ liệu từ GraphQL endpoint của Gitlab.Ở đâ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ớ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.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à attributeimport_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 attributeimport_type
tạiProjects::GitlabProjectsImportService.prepare_import_params
.Sau khi hoàn thành
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:
Quay trở lại lúc lần đầu tiên chạy vào
Projects::CreateService
, điều kiệncreate_from_template?
được định nghĩa như sauKiể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à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:
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ứcwait_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 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.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: