CVE-2022–43781 Bitbucket command injection

Petrus Viet
7 min readNov 25, 2022

--

There is a command injection vulnerability using environment variables in Bitbucket Server and Data Center.

I) Setup

Ở đây mình chỉ hướng dẫn setup và debug ở trên môi trường windows, các bạn linux cũng có thể làm tương tự nhé ~!

  1. Đầu tiên các bạn cần tải file cài đặt (dạng zip) ở đây.
  2. Giải nén file vừa tải về, mình ví dụ các bạn giải nén ra thư mục D:\\atlassian-bitbucket-7.6.17\ nhé.
  3. Tạo folder home, ta cần một folder home để chứa database, etc,… . Lưu ý là folder này không được nằm trong D:\\atlassian-bitbucket-7.6.17\ nhé vì nó mà mình mất thêm một ngày dể setup :(. Mình ví dụ chúng ta lấy thư mục D:\\home\ làm thư mục home.
  4. Bây giờ chúng ta cần sửa một vài file trong D:\\atlassian-bitbucket-7.6.17\bin:
  • Sửa file set-bitbucket-home.bat
set BITBUCKET_HOME=D:\\home
  • Sửa file _start-webapp.bat (để debug):
set JVM_SUPPORT_RECOMMENDED_ARGS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

5. Mở cmd hoặc powershell ở thư mục D:\\atlassian-bitbucket-7.6.17\ và chạy:

.\bin\start-bitbucket.bat /no-search

6. Các bạn tiếp tục config theo hướng dẫn ở link dưới.
LƯU Ý: Do Atlassian công bố các Bitbucket server sử dụng PostgreSQL không bị ảnh hưởng bởi lỗ hổng nên các bạn lưu ý không sử dụng PostgreSQL nhé (Mặc định sẽ sử dụng H2 database).

Ref: https://confluence.atlassian.com/bitbucketserver073/install-bitbucket-server-on-windows-from-a-zip-file-1013851214.html

II) Phân tích

Trước khi bắt tay vào diff, ta cần nghiên cứu kỹ các thông tin đã có trước đã. Atlassian đã công bố lỗ hổng CVE-2022–43781Command injection vulnerability using environment variables.

Ở bảng Affected Versions ta thấy có một điều đặc biệt ở đây:

Điều đó có nghĩa là các phiên bản 8.x sẽ bị ảnh hưởng nếu không bật tính năng mesh :D Vậy mesh nó là gì và tại sao không có trên phiên bản 7.x?

Research một lúc ta có thể thấy rằng: mesh là một hệ thống lưu trữ dùng để thay thế NFS xuất hiện như một optional từ phiên bản 8.x. Vậy có nghĩa là server sẽ bị ảnh hưởng nếu sử dụng NFS hay nói cách khác nó đâu đó phải liên quan tới git :D :D :D

Dừng lại và suy nghĩ. Những thông tin trên khiên mình liên tưởng tới một kịch bản:

Attacker input payload vào username rồi bằng một cách nào đó nó nằm trong environment variables để thực thi câu lệnh git nào đó.

Nếu đã đặt kịch bản xoay quanh git, mình quyết định sử dụng Process Monitor, filter các process git được tạo ra khi tạo repo và push code lên server.

Mò mẫm trong đống process mới được tạo, mình nhận ra không hề có câu lệnh nào có chứa parameter liên quan tới username. Như thế là đúng với suy luận ở trên: payload nằm ở trong environment variables chứ không có ở trên parameter.

Mình đi vào diff bản patch ~

  • Tại com.atlassian.bitbucket.internal.process.RemoteUserNioProcessConfigurer!configure():

Ta thấy tại vulnerability version cặp (“REMOTE_USER”, UserName) được đưa vào map environment rồi lấy environment đưa vào hàm NioProcessParameters.updateEnvironment(). Còn ở phía fix version đã có sự thay đổi nhỏ: Cặp ("REMOTE_USER", UserName) trực tiếp đưa vào hàm NioProcessParameters.environmentPut()

  • Tiếp tục xem xét tại com.atlassian.bitbucket.internal.process.NioProcessParameters:

Ta thấy ngay ở phiên bản lỗi, hàm updateEnvironment chỉ check input đưa vào có phải là null hay không. Còn ở phía phiên bản fix tại hàm environmentPut chương trình đã có thêm thao tác kiểm tra blank và kiểm tra xem key và val có chứa byte null hay không =)))

Như thế, bây giờ ta có thể đặt break point tại đấy rồi tiến hành debug nào!

  • Khi ta truy cập vào một repo bất kỳ thì chương trình dừng lại tại NuNioProcessHelper.run()
  • Chương trình đi vào NuNioProcessHelper.applyConfigurers()
  • Tại RemoteUserNioProcessConfigurer.configure(), ta thấy chương trình đã put cặp ("REMOTE_USER", UserName) vào environment
  • Tiếp tới là NuProcessBuilder.run()
  • Tại NuProcessBuilder.prepareEnvironment(), chương trình chuyển environment từ dạng Map sang dạng String[]
  • Tiếp tới là WinProcessFactory.runProcess()
  • Rồi vào WindowsProcess.run()
  • Tiến tới WindowsProcess.getEnvironment(), chương trình chuyển environment đang ở dạng String[] về lại dạng Map bằng cách cắt chuỗi ở vì trí ký tự = đầu tiên:
  • Tiếp tới chương trình đi vào WindowsProcess.getEnvironmentBlock(), ở đây Map environment lại được chuyển về một chuỗi string với công thức:
out =  "KEY1=VAL1\0KEY2=VAL2\0\0"

=> Như thế, nếu như chúng ta tạo một tài khoản mới với username là: ahihi\0KEY=VAL thì có phải là khi thực thi các câu lệnh git sẽ có thêm một giá trị environment được chèn thêm vào là KEY=VAL hay không nhỉ???

Cho rằng chúng ta đã có thể chèn thêm các giá trị environment tùy ý, vấn đề mới là chúng ta nên chèn như thế nào để có thể gây nên RCE???

Lang thang google một lúc thì mình có tìm thấy một danh sách các environment được git sử dụng. Mình chú ý tới GIT_EXTERNAL_DIFF

Như đã được nêu trong docs, nôm na là GIT_EXTERNAL_DIFF dùng để chỉ định một tập lệnh thực thi khi git diff được gọi :D

  • Payload đầu tiên mình test như sau:
  • Để chương trình gọi git diff, ta đơn giản chỉ cần tạo một repo chỉnh sửa 1 file text trong đó rồi truy cập phần commit để diff :)

CALC đã được gọi thành công, nhưng đây chỉ là thực thi file calc.exe, nếu mình gọi cmd.exe và thêm parameter thì sao?

  • Như các bạn thấy, mình không thể thêm parameter vào được :(, mình tiếp tục thử với payload sau:

Mặc dù có thông báo lỗi, nhưng server có tạo request ra ngoài. Thiết nghĩ mình có thể tạo một file server trên windows và tiếp tục test, nhưng vì kịch bản này cần yêu cầu outbound ra internet nên mình để đó và tìm kiếm một cách khác :D

  • Lăn lộn 15–30 phút gì đó mình nghĩ ra cái này:

và nó đã thực sự hoạt động!

Stack Trace:

NioCommand.call()
-> NuNioProcessHelper.run()
-> NuNioProcessHelper.applyConfigurers()
-> RemoteUserNioProcessConfigurer.configure() (# put username to environment)

-> NuNioProcessHelper.createProcessBuilder()
-> NuProcessBuilder.run()
-> NuProcessBuilder.prepareEnvironment() (Map to String[] - "Key=Value")
-> WinProcessFactory.runProcess()
-> WindowsProcess.run()
-> WindowsProcess.prepareProcess()
-> WindowsProcess.getEnvironment()
-> WindowsProcess.getEnvironmentBlock() (String[] to Char[] - "key1=var1\0key2=var2")
-> NuKernel32.ResumeThread(this.processInfo.hThread) (RCE)

--

--