BatBadBut: Lỗ hổng khiến bạn không thể thực thi lệnh một cách an toàn trên Windows

Giới thiệu Gần đây, RyotaK ( @ryotkak ) đã báo cáo chùm lỗ hổng ở một số ngôn ngữ lập trình cho phép kẻ tấn công khai thác lỗi Command Injection trên Windows thỏa mãn một số điều kiện cụ thể (ví dụ đối với Rust ). Các nhà cung cấp bị ảnh hưởng đã công bố khuyến cáo [1] về các lỗ hổng nói trên. Bài viết của RyotaK được dịch lại nhằm cung cấp thêm thông tin chi tiết và giải thích về thang điểm CVSS cao của lỗ hổng. TL;DR BatBadBut là lỗ hổng cho phép kẻ tấn công khai thác Command Injection trên các ứng dụng Windows gián tiếp phụ thuộc vào hàm CreateProcess khi một số điều kiện cụ thể được đáp ứng. CreateProcess() ngầm sinh ra cmd.exe khi thực thi các tệp batch (như .bat , .cmd , v.v.), ngay cả khi ứng dụng không đề cập đến chúng trong dòng lệnh. Do cmd.exe có các quy tắc phân tích cú pháp phức tạp cho các đối số trong lệnh hệ thống, kéo theo việc runtime của các ngôn ngữ lập trình không thể escape các đối số này một cách chính xác. Bởi vậy, có thể tấn công Command Injection một khi kiểm soát được phần đối số lệnh của tệp batch. Ví dụ, đoạn mã Node.js đơn giản dưới đây có thể bật calc.exe trên máy chủ: const { spawn } = require('child_process');
const child = spawn('./test.bat', ['<your-input-here>']);
Điều này chỉ xảy ra nếu một tệp batch được chỉ định rõ ràng trong dòng lệnh được truyền cho CreateProcess() , chứ không xảy ra khi tệp .exe được chỉ định. Tuy nhiên, do Windows mặc định bao gồm các tệp .bat và .cmd trong biến môi trường PATHEXT , nên một số runtime sẽ thực thi các tệp batch kể cả khi có sẵn lệnh cùng tên mà nhà phát triển dự kiến thực thi. Vì vậy, ngay cả đoạn mã dưới đây cũng có thể dẫn đến việc thực thi lệnh tùy ý trong khi rõ ràng nó không hề đề cập đến .bat hay .cmd : cmd := exec.Command("test", "<your-input-here>")
cmd.Run()
Có thể khai thác các hành vi trên khi các điều kiện sau được thỏa mãn: Ứng dụng thực thi lệnh trên Windows Ứng dụng không chỉ định rõ extension của lệnh hoặc extension là .bat hoặc .cmd Lệnh thực thi có chứa đầu vào do người dùng kiểm soát là một phần của các đối số lệnh Runtime của ngôn ngữ lập trình không thể escape các đối số lệnh cho cmd.exe một cách chính xác [2] Lợi dụng những hành vi trên, việc thực thi lệnh tùy ý là hoàn toàn khả thi. RyotaK cũng cung cấp một sơ đồ để xác định một ứng dụng có bị ảnh hưởng bởi lỗ hổng này hay không. Hãy tham khảo Phụ lục A nếu bạn không chắc liệu mình có bị ảnh hưởng hay không, và tham chiếu Phụ lục B để biết trạng thái khắc phục của các ngôn ngữ lập trình bị ảnh hưởng. Điểm CVSS Trước hết, không nên áp dụng trực tiếp điểm CVSS của các lỗ hổng thư viện cho ứng dụng của bạn. Hướng dẫn sử dụng CVSS v3.1 nêu rõ rằng điểm CVSS của thư viện phải được tính toán dựa trên tình huống xấu nhất, và đây là lý do tại sao các lỗ hổng gần đây đối với ngôn ngữ lập trình đạt điểm cao bất chấp yêu cầu về các điều kiện cụ thể. Thay vì áp dụng trực tiếp điểm CVSS, bạn nên tính lại điểm dựa trên cách triển khai cụ thể, tham khảo tại: ** https://www.first.org/cvss/v3.1/user-guide#3-7-Scoring-Vulnerabilities-in-Software-Libraries-and-Similar** Chi tiết kỹ thuật RyotaK cho biết lỗ hổng được gọi là BatBadBut vì nó liên quan đến các tệp bat ch và tệ hại ( bad ), nhưng ( but ) không phải là tệ nhất. Đề mục này sẽ giải thích khía cạnh kỹ thuật của BatBadBut và lý do tại sao có thể thực hiện Command Injection. Cần lưu ý rằng một số đoạn mã sẽ không hoạt động trên các phiên bản runtime mới nhất, bởi một số nhà cung cấp bị ảnh hưởng đã vá lỗ hổng. Nguyên nhân Nguyên nhân cốt lõi của BatBadBut là hành vi bị bỏ qua của hàm CreateProcess trên Windows. Khi thực thi các tệp batch bằng hàm CreateProcess , Windows sẽ ngầm sinh ra cmd.exe bởi Windows không thể thực thi các tệp batch mà không có nó. Ví dụ, đoạn mã sau sinh ra C:\Windows\System32\cmd.exe /c .\test.bat để thực thi tệp batch test.bat : wchar_t arguments[] = L".\\test.bat";
STARTUPINFO si{};
PROCESS_INFORMATION pi{};
CreateProcessW(nullptr, arguments, nullptr, nullptr, false, 0, nullptr, nullptr, &si, &pi);
Mặc dù bản thân nó không phải vấn đề, nhưng vấn đề phát sinh khi ngôn ngữ lập trình bọc hàm CreateProcess và thêm cơ chế escape cho các đối số lệnh. Việc bọc hàm CreateProcess Hầu hết các ngôn ngữ lập trình đều cung cấp hàm để thực thi lệnh, và bản chất là bọc lại hàm CreateProcess để cung cấp một interface thân thiện hơn với người dùng. Ví dụ, module child_process trong Node.js [3] bọc lại CreateProcess và cung cấp cách thực thi lệnh có các đối số như sau: const { spawn } = require('child_process');
const child = spawn('echo', ['hello', 'world']);
Như bạn có thể thấy trong đoạn mã trên, hàm spawn coi lệnh và các đối số của nó là các tham số riêng biệt. Sau đó, các đối số sẽ được escape rồi chuyển tới hàm CreateProcess . src/win/process.c line 444-518 /*
* Quotes command line arguments
* Returns a pointer to the end (next char to be written) of the buffer
*/
WCHAR* quote_cmd_arg(const WCHAR *source, WCHAR *target) {
[...]

/*
* Expected input/output:
* input : hello"world
* output: "hello\"world"
* input : hello""world
* output: "hello\"\"world"
* input : hello\world
* output: hello\world
* input : hello\\world
* output: hello\\world
* input : hello\"world
* output: "hello\\\"world"
* input : hello\\"world
* output: "hello\\\\\"world"
* input : hello world\
* output: "hello world\\"
*/

*(target++) = L'"';
start = target;
quote_hit = 1;

for (i = len; i > 0; --i) {
*(target++) = source[i - 1];

if (quote_hit && source[i - 1] == L'\\') {
*(target++) = L'\\';
} else if(source[i - 1] == L'"') {
quote_hit = 1;
*(target++) = L'\\';
} else {
quote_hit = 0;
}
}
target[0] = L'\0';
_wcsrev(start);
*(target++) = L'"';
return target;
}
Hầu hết các nhà phát triển đều cho rằng hàm spawn đã escape các đối số lệnh một cách chuẩn chỉnh, và điều này đúng trong hầu hết các trường hợp [4]. Tuy nhiên, như đã đề cập trước đó, hàm CreateProcess ngầm sinh ra cmd.exe khi thực thi các tệp batch. Và thật không may, cmd.exe có các quy tắc escape khác so với cơ chế escape thông thường. Quy tắc phân tích cú pháp của cmd.exe Hầu hết shell ở các hệ điều hành Unix đều có quy tắc escape tương đồng (hoặc giống hệt nhau) với dấu backslash (, dấu gạch chéo ngược) được sử dụng làm ký tự escape. Khi đó, nếu bạn muốn dùng dấu ngoặc kép (”) bên trong chuỗi trích dẫn kép, bạn có thể sử dụng dấu backslash để escape: echo "Hello \"World\""
Sử dụng dấu backslash làm ký tự escape dường như là một tiêu chuẩn trên thực tế, ngay cả những thứ khác như JSON hoặc YAML cũng sử dụng nó. Tuy nhiên, khi bạn thực hiện lệnh sau trên command prompt, calc.exe sẽ được thực thi: echo "\"&calc.exe"
Điều này là do command prompt không sử dụng dấu backslash làm ký tự escape mà thay vào đó sử dụng dấu caret (^, dấu mũ). Quay lại ví dụ child_process , dấu ngoặc kép (”) trong đối số lệnh được escape bằng dấu backslash (). Do các quy tắc escape của cmd.exe đề cập trên, việc escape này không đủ chặt khi thực thi tệp batch, vì thế nên đoạn mã sau vẫn sinh ra calc.exe mặc cho đối số đã được phân tách chính xác và tùy chọn shell [5] cũng không sử dụng: const { spawn } = require('child_process');
const child = spawn('./test.bat', ['"&calc.exe']);
Với hành vi này, một đối số dòng lệnh độc hại có thể thực hiện Command Injection, và đây chính là vấn đề của BatBadBut . Biện pháp giảm thiểu Escape các dấu ngoặc kép? Vấn đề ở đây là chuỗi trích dẫn kép bị ngắt bởi dấu ngoặc kép (”) bên trong chuỗi. Vì vậy, có vẻ như việc escape dấu ngoặc kép (”) bằng dấu caret (^) là đủ để ngăn Command Injection [6]. Tuy nhiên, trên thực tế, điều đó là chưa đủ để ngăn cản Command Injection. Command prompt phân tích cú pháp và truyền các biến (ví dụ %PATH% ) trước bất kỳ cú pháp nào khác. Điều này có nghĩa là lệnh sau vẫn thực thi được calc.exe , mặc dù &calc.exe đã nằm trong chuỗi trích dẫn kép: SET VAR=^"
echo "%VAR%&calc.exe"
Tuy các biến môi trường mặc định của Windows không chứa dấu ngoặc kép (”) trong giá trị của chúng, nhưng có một biến đặc biệt gọi là CMDCMDLINE . Biến này chứa dòng lệnh được sử dụng để bắt đầu phiên command prompt hiện tại. "C:\WINDOWS\system32\cmd.exe" /c "echo %CMDCMDLINE%" sẽ được in ra khi lệnh sau được thực thi trên PowerShell: cmd.exe /c "echo %CMDCMDLINE%"
Và bằng cách sử dụng tính năng trích xuất chuỗi con của biến trong command prompt, hoàn toàn có thể trích xuất dấu ngoặc kép (”) từ CMDCMDLINE . Bởi vậy, lệnh sau sẽ sinh ra calc.exe khi thực thi trên PowerShell: cmd.exe /c 'echo "%CMDCMDLINE:~-1%&calc.exe"'
Với hành vi này, việc escape dấu ngoặc kép (”) bằng dấu caret (^) là không đủ để ngăn việc chèn lệnh khi thực thi tệp batch và cần phải escape thêm. Vấn đề này sẽ được giải thích trong phần tiếp theo. Ở vai trò nhà phát triển Vì không phải tất cả ngôn ngữ lập trình đều vá lỗi này [2], nên bạn cần cẩn thận khi thực thi các lệnh trên Windows. Là nhà phát triển, nếu bạn muốn thực thi các lệnh trên Windows nhưng không muốn thực thi các tệp batch, bạn nên luôn ghi rõ extension của lệnh. Ví dụ, đoạn mã sau có thể thực thi test.bat thay vì test.exe nếu người dùng đặt test.bat vào thư mục có trong biến môi trường PATH : cmd := exec.Command("test", "arg1", "arg2")
Để ngăn chặn điều này, bạn phải chỉ định extension của lệnh như sau: cmd := exec.Command("test.exe", "arg1", "arg2")
Nếu bạn muốn thực thi các tệp batch và runtime của bạn không escape các đối số lệnh một cách chính xác cho tệp batch, bạn phải tự escape đầu vào mà người dùng kiểm soát được trước khi sử dụng chúng làm đối số lệnh. Vì dấu cách (space) không thể escape đúng bên ngoài chuỗi trích dẫn kép [7], nên bạn cần sử dụng dấu ngoặc kép để bọc các đối số lệnh. Tuy nhiên, bên trong chuỗi trích dẫn kép, dấu phần trăm (%) lại không thể escape đúng cách [8]. Để giải quyết tình huống trên, quy trình escape cần thực hiện như sau [9]: Tắt tính năng escape tự động sử dụng dấu backslash () do runtime cung cấp. Áp dụng các bước sau cho mỗi đối số: Thay thế dấu phần trăm (%) bằng %%cd:~,% . Thay thế dấu backslash () trước dấu ngoặc kép (”) bằng hai dấu backslash (\) Thay thế dấu ngoặc kép (”) bằng hai dấu ngoặc kép (””) Xóa ký tự dòng mới (
) Bọc toàn bộ đối số bằng cặp dấu ngoặc kép (”). Bằng cách thay thế % bằng %%cd:~ , %%cd:~ sẽ được mở rộng thành một chuỗi trống, và command prompt không thể mở rộng biến thực tế, vì thế nên % sẽ được coi là một ký tự bình thường. Cần lưu ý rằng nếu việc mở rộng trì hoãn được bật thông qua registry DelayedExpansion , thì phải tắt tính năng này bằng cách gọi cmd.exe kèm tùy chọn /V:OFF . Ngoài ra, cần lưu ý rằng, để escape %, phải có extension của lệnh. Nếu extension bị tắt thông qua registry EnableExtensions thì phải bật tính năng này với tùy chọn /E:ON . Ở vai trò người dùng Để ngăn việc thực thi các tệp batch không mong muốn, nên cân nhắc việc di chuyển các tệp batch sang một thư mục không có trong biến môi trường PATH. Trong trường hợp này, các tệp batch sẽ không được thực thi trừ khi đường dẫn đầy đủ được chỉ định, nhờ đó có thể ngăn chặn việc thực thi các tệp batch không mong muốn. Ở vai trò maintainer của runtime Maintainer một runtime của ngôn ngữ lập trình nên triển khai cơ chế escape bổ sung cho các tệp batch. Ngay cả khi không muốn khắc phục vấn đề này trên lớp runtime, thì ít nhất nó cũng nên được ghi lại sự cố và có các cảnh báo phù hợp cho người dùng, bởi vấn đề này chưa được nhiều người biết đến. Kết luận Bài viết này đã giải thích chi tiết kỹ thuật của BatBadBut , một lỗ hổng cho phép kẻ tấn công thực hiện Command Injection trên Windows khi đáp ứng một số điều kiện cụ thể. Như đã đề cập nhiều lần trong bài viết, lỗi này không ảnh hưởng đến hầu hết các ứng dụng, nhưng trong trường hợp ứng dụng bị ảnh hưởng, cần escape các đối số lệnh một cách thủ công. Hy vọng bài viết này sẽ giúp bạn hiểu được mức độ nghiêm trọng của lỗ hổng này và giảm thiểu khắc phục nó một cách hợp lý. Theo Flatt Security. Phụ lục Phụ lục A: Sơ đồ để xác định xem ứng dụng của bạn có bị ảnh hưởng hay không Phụ lục B: Trạng thái của các ngôn ngữ lập trình bị ảnh hưởng