Thoát khỏi AppleScript: Ngay cả tên thư mục cũng có thể là đầu vào từ người dùng
OpenInTerminal là một tiện ích phổ biến trên macOS được viết bằng Swift, cải thiện trải nghiệm Finder bằng cách cho phép người dùng nhanh chóng mở thư mục hiện tại trong ứng dụng terminal ưa thích. Ứng dụng đạt được điều này bằng cách tích hợp vào thanh công cụ và menu ngữ cảnh của Finder.
Khi kiểm tra mã nguồn, chúng tôi ghi nhận nhiều nhà phát triển dùng AppleScript như một phương án thay thế cho hàm eval() còn thiếu trong Swift. Thói quen tạo và thực thi script động này đưa các rủi ro điển hình của ngôn ngữ động vào một ngôn ngữ được biên dịch.
Bài viết này trình bày chi tiết kỹ thuật về lỗ hổng AppleScript Injection được phát hiện trong OpenInTerminal, do SonarQube phát hiện. Chúng tôi sẽ lần lượt mô tả cách một thư mục được chế tạo độc hại có thể dùng để thực thi mã tùy ý trên máy nạn nhân. Cuối cùng, bài sẽ chỉ ra cách tránh những sai sót tương tự trong mã của bạn.
Tác động
Kẻ tấn công có thể lợi dụng lỗ hổng này để thực thi mã tùy ý trên máy tính của nạn nhân. Để tấn công thành công, nạn nhân phải tải về hoặc nhận được một cấu trúc thư mục độc hại (ví dụ: clone một repository Git không tin cậy hoặc giải nén một tệp ZIP) và sau đó cố mở một trong các thư mục lồng bên trong bằng tính năng OpenInTerminal sử dụng terminal hoặc trình soạn thảo không chuẩn.
Cuộc tấn công có thể được ngụy trang bằng cách sử dụng symbolic link để làm cho thư mục kích hoạt trở nên dễ tiếp cận hơn, giảm thiểu tương tác cần thiết từ người dùng.
Chi tiết kỹ thuật
Sau khi quét mã nguồn OpenInTerminal bằng SonarQube Cloud, chúng tôi nhận được một cảnh báo lỗ hổng:

Dòng mã được tô sáng sẽ chạy các đoạn AppleScript tùy ý truyền vào hàm execute(). Để xác minh đây có thực sự là lỗ hổng hay không, chúng ta cần kiểm tra nguồn gốc của các script đó. Ví dụ, OpenInTerminal dùng AppleScript để khởi chạy ứng dụng bên ngoài. Để mở một đường dẫn hệ thống trong terminal, ứng dụng xây dựng và thực thi lệnh AppleScript do shell script như sau:
var openCommand = DefaultsManager.shared.getOpenCommand(self, escapeCount: 2)
openCommand += " " + path.specialCharEscaped(2)
let source = """
do shell script "\(openCommand)"
"""
try excute(source)
Vấn đề cốt lõi nằm ở cách các ký tự đặc biệt trong đường dẫn thư mục bị escape. Ứng dụng cố escape ký tự đặc biệt cho ngữ cảnh shell, nhưng việc escape này không đủ cho ngữ cảnh bao ngoài là AppleScript.
Hàm specialCharEscaped(2) escape các ký tự như dấu ngoặc kép (") bằng hai dấu gạch chéo ngược (\\"). Mặc dù điều này escape đúng cho phần shell script, nhưng nó để lại dấu ngoặc kép không được escape từ góc nhìn của trình thông dịch AppleScript bên ngoài.
Ví dụ, một thư mục tên foo"bar sẽ được escape thành mã AppleScript sau:
do shell script "open -a iTerm /Users/paul/foo\\"bar"
Điều này phá vỡ cú pháp AppleScript, vì dấu " đơn lẻ kết thúc chuỗi sớm, dẫn đến lỗi. Kẻ tấn công có thể tận dụng sai sót escape này để thoát khỏi ngữ cảnh chuỗi và chèn các lệnh AppleScript độc hại của riêng họ.
Hành trình khai thác
Thực thi payload không đơn giản vì nhiều ký tự đặc biệt cần thiết cho các lệnh đơn giản (như khoảng trắng và dấu ngoặc kép) đã bị escape. Để xây dựng một khai thác hoạt động, kẻ tấn công phải vượt qua một số hạn chế:
Vượt rào các giới hạn ký tự: Để tạo chuỗi lệnh mà không dùng dấu ngoặc kép, kẻ tấn công có thể ghép chuỗi bằng cách chuyển mã ASCII thành ký tự (ví dụ ASCII character 65 thành "A"). Vì ký tự nối & cũng bị escape, một mảng ký tự có thể được tạo và sau đó ép kiểu thành chuỗi đơn bằng từ khóa as text. Thêm nữa, vì ký tự khoảng trắng (0x20) có thể bị escape, kẻ tấn công có thể thay thế bằng ký tự tab.
-- create a char list: ['i', 'd']
set charlist to {ASCII character 105,ASCII character 100}
-- cast to text, joining chars to a single string
set cmd to charlist as text
Vượt giới hạn độ dài đường dẫn: macOS giới hạn tên tệp hoặc thư mục đơn lẻ ở 255 ký tự, không đủ cho payload phức tạp. Payload có thể được chia nhỏ qua nhiều thư mục lồng nhau. Ký tự phân cách đường dẫn (/) vốn sẽ phá vỡ script được giấu bên trong chú thích dòng của AppleScript (--), khiến trình thông dịch bỏ qua và coi nội dung của các tên thư mục lồng nhau như phần tiếp nối của script.
set a to {ASCII character 111,ASCII character 112,ASCII character 101,ASCIIcharacter 110,ASCII character 32,ASCII character 45,ASCII character 97,ASCIIcharacter 32,ASCII character 67,ASCII character 97}--/
set b to {ASCII character 108,ASCII character 99,ASCII character 117,ASCIIcharacter 108,ASCII character 97,ASCII character 116,ASCII character 111,ASCIIcharacter 114}--/
set cmd to {a,b} as text
do shell script cmd --/interesting
Hoàn thiện payload: Phần cuối của payload cũng phải bắt đầu bằng chú thích dòng (--) để đảm bảo dấu ngoặc kép cuối cùng từ lệnh do shell script ban đầu bị bỏ qua, ngăn lỗi cú pháp. Để làm cho cuộc tấn công thuyết phục hơn, kẻ tấn công có thể thêm một symbolic link cấp cao trỏ tới thư mục sâu nhất để nạn nhân có thể đến một đường dẫn chứa payload mà không phải nhìn thấy các thư mục trung gian đáng ngờ.
Khi người dùng có phiên bản OpenInTerminal dễ bị tấn công cố mở thư mục sâu nhất có tên interesting, chuỗi payload AppleScript sẽ chạy, trong ví dụ này khởi chạy ứng dụng Calculator.
Bản vá
Cách giảm thiểu lỗ hổng là tránh sử dụng AppleScript để chạy các lệnh shell. Cách tiếp cận được khuyến nghị là dùng giao diện bản địa Process để thực thi lệnh trực tiếp. Cách này tránh việc truyền lệnh qua một trình thông dịch phụ như AppleScript, loại bỏ rủi ro injection do quy tắc escape không khớp giữa các lớp.
Người duy trì OpenInTerminal đã sửa lỗ hổng này bằng cách thay thế việc xây dựng chuỗi lệnh bằng tay bằng cách truyền đối số một cách cấu trúc. Trước đó, tên ứng dụng và đường dẫn được nối thành một chuỗi lệnh duy nhất, escape thủ công một denylist các metacharacter rồi truyền vào do shell script của AppleScript. Cách làm này dễ vỡ: tên tệp hoặc tên ứng dụng được chế tạo khéo có thể vượt ranh giới dữ liệu/mã và bị hiểu là cú pháp shell.
Triển khai đã sửa gửi /usr/bin/open, các tùy chọn của nó và từng đường dẫn như các tham số AppleEvent riêng biệt. AppleScript sau đó áp dụng POSIX quoted form of cho mỗi phần tử trước khi thực thi, đảm bảo các giá trị không tin cậy vẫn là các tham số thuần túy. Cách này tương tự các API như execve() hoặc subprocess.run([...], shell=False): giữ nguyên ranh giới đối số thay vì cố gắng lọc chuỗi lệnh.
Mốc thời gian
Tóm tắt
Lỗ hổng trong OpenInTerminal là lời nhắc hữu ích về rủi ro khi động cơ tạo và thực thi script từ dữ liệu do người dùng kiểm soát — thậm chí là thứ có vẻ vô hại như một đường dẫn tệp. Khi một ngữ cảnh lập trình (Swift) tạo mã cho ngữ cảnh khác (AppleScript) và ngược lại chạy một shell thứ ba, các quy tắc escape và lọc phải khớp hoàn hảo giữa các lớp. Sự không khớp như trường hợp này có thể tạo ra các lỗ hổng injection tinh vi nhưng nghiêm trọng. Nhà phát triển nên ưu tiên API bản địa an toàn hơn cho việc thực thi tiến trình thay vì tạo script động khi có thể.
Các bài viết liên quan
Cần trợ giúp chuyên sâu hoặc muốn triển khai giải pháp bảo mật tương tự cho sản phẩm của bạn? Liên hệ Softribution để được tư vấn kỹ thuật và nhận giải pháp phù hợp—từ audit mã nguồn, vá lỗ hổng đến triển khai cơ chế thực thi tiến trình an toàn. Chúng tôi sẵn sàng hỗ trợ bạn đánh giá rủi ro và triển khai cập nhật nhằm giảm thiểu nguy cơ injection trong ứng dụng.
