Cho phép người dùng tải tệp lên — ảnh đại diện, tài liệu, đính kèm — là một trong những tính năng nguy hiểm nhất một ứng dụng có thể có. Người dùng đưa dữ liệu tuỳ ý vào máy chủ của bạn, và nếu bạn xử lý sai, dữ liệu đó trở thành mã chạy được. Bài này dựng lại bốn cách khai thác upload, và chứng minh cách nguy hiểm nhất bằng một cuộc thực thi mã thật.

Toàn bộ chạy trong container tự dựng, tự dọn. Chỉ tấn công máy chủ do tôi tạo. Không nhắm vào hệ thống của ai.

Bon cach khai thac tai tep len va chung minh RCE bang polyglot

Bốn cách khai thác

  • Đuôi tệp giả: đặt tên tệp php là avatar.png. Nếu máy chủ tin đuôi tên, nó lưu một tệp thực thi vào thư mục web.
  • Polyglot: một tệp vừa là ảnh PNG hợp lệ vừa chứa mã. Nó qua được cả phép kiểm "có phải ảnh không".
  • Path traversal: đặt tên tệp là ../../../etc/cron.d/x để ghi ra ngoài thư mục upload.
  • DoS bằng dung lượng: tải một tệp khổng lồ để làm đầy đĩa hoặc hết RAM.

Chứng minh: polyglot thực thi mã thật

Đây là phần đáng sợ nhất, và tôi đo nó bằng một máy chủ PHP+Apache thật. Tôi tạo một tệp 118 byte: một PNG 1×1 hợp lệ, rồi nối mã PHP vào cuối. Kết quả đo, ba phần:

Kiểm tra Kết quả
Pillow (thư viện ảnh) mở tệp đọc được — PNG 1×1 hợp lệ
Phục vụ với đuôi .png Apache trả nội dung thô, không thực thi
Phục vụ với đuôi .php RCE-THANH-CONG:uid=33(www-data)

Dòng cuối là một cuộc thực thi mã từ xa (RCE) thật: mã PHP nhúng trong "ảnh" chạy lệnh id trên máy chủ và trả về danh tính tiến trình web (www-data). Từ đó kẻ tấn công chạy được lệnh tuỳ ý.

Điều đáng nhớ

"Kiểm xem có phải ảnh không" là không đủ — vì tệp này thật sự là ảnh. Đây là điểm phản trực giác nhất. Nhiều hướng dẫn khuyên "kiểm magic bytes" hoặc "thử mở bằng thư viện ảnh". Polyglot của tôi qua cả hai: Pillow mở nó ra thành một ảnh PNG 1×1 hoàn chỉnh. Nó không phải ảnh giả — nó là ảnh thật và đồng thời là mã PHP thật. Không có phép kiểm nội dung nào phân biệt được, vì không có gì để phân biệt: cả hai cùng đúng.

Cái cứu bạn không phải kiểm nội dung, mà là không bao giờ thực thi thứ được tải lên. Nhìn hai dòng cuối bảng: cùng một tệp, đuôi .png thì vô hại, đuôi .php thì RCE. Khác biệt duy nhất là máy chủ có thực thi nó hay không. Cách chặn thật: lưu tệp ngoài thư mục web (không phục vụ trực tiếp), đặt tên ngẫu nhiên với đuôi cố định do bạn quyết định (không dùng tên hay đuôi client gửi), và cấu hình thư mục upload không bao giờ chạy mã. Làm đúng ba điều đó thì một polyglot dù tinh vi tới đâu cũng chỉ nằm im như một tệp dữ liệu.

basename chặn path traversal — nhưng chỉ theo hệ điều hành đang chạy. Đo được một cái bẫy: os.path.basename('../../../etc/passwd') trên Linux trả về passwd — chặn đúng. Nhưng os.path.basename('..\\..\\windows\\system32\\x') trên Linux trả về nguyên chuỗi ..\..\windows\system32\x, vì Linux không coi \ là dấu phân tách thư mục. Một tên tệp dùng dấu \ lọt qua basename trên máy chủ Linux, và nếu đường dẫn đó được một thành phần khác (hay một hệ Windows) diễn giải, traversal thành công. Bài học: đừng chỉ dựa vào basename; sinh tên tệp hoàn toàn mới thay vì làm sạch tên client.

Đuôi giả không cần polyglot

Trước khi bàn polyglot, đáng nhớ rằng cách đơn giản nhất vẫn hiệu quả nếu máy chủ ngây thơ. Trong phép đo, một tệp chứa <?php system($_GET['cmd']); ?> đặt tên avatar.png: nhận dạng theo tên cho ra .png (tưởng là ảnh, cho lưu vào thư mục web), còn nhận dạng theo byte đầu cho ra .php — mã thực thi. Nếu máy chủ chỉ tin đuôi tên client gửi, nó lưu một web shell vào đúng chỗ nó sẽ chạy.

Đây là lý do nhận dạng phải theo nội dung chứ không theo tên. Polyglot là cấp độ khó hơn — khi kẻ tấn công làm cho cả nhận-dạng-theo-nội-dung cũng bị qua — nhưng phần lớn upload bị khai thác vẫn chỉ vì tin đuôi tên. Con số hai cách nhận dạng cho ra hai kết quả khác nhau trên cùng một tệp là toàn bộ vấn đề, cô đọng.

Vì sao

Bốn cách khai thác chia làm hai nhóm, theo hậu quả:

  • Đuôi giả và polyglot dẫn tới thực thi mã — nghiêm trọng nhất. Gốc chung: máy chủ đối xử với tệp người dùng như thay vì dữ liệu. Cách chặn cũng chung: đảm bảo tệp tải lên không bao giờ chạy.
  • Path traversal và DoS dẫn tới ghi sai chỗcạn tài nguyên. Gốc chung: tin dữ liệu client (tên tệp, dung lượng) mà không kiểm. Cách chặn: sinh tên mới, giới hạn dung lượng trước khi đọc vào bộ nhớ.

Sợi chỉ xuyên suốt giống mọi bài bảo mật khác trong sê-ri: dữ liệu người dùng là dữ liệu, không phải lệnh — và không phải tên đường dẫn, không phải đuôi tệp, không phải kích thước tin được. Mọi thuộc tính của tệp mà client kiểm soát đều phải bị nghi ngờ và thay thế bằng giá trị do máy chủ quyết định.

Nghĩa là gì trong thực tế

  • Sinh tên tệp mới hoàn toàn phía máy chủ (UUID + đuôi cố định), đừng dùng hay "làm sạch" tên client. Điều này chặn cả path traversal lẫn đuôi giả trong một bước.
  • Lưu tệp tải lên ngoài web root, phục vụ chúng qua một handler kiểm quyền, không để web server chạy trực tiếp. Thư mục upload phải được cấu hình không thực thi.
  • Giới hạn dung lượng trước khi đọc. Kiểm Content-Length và dừng đọc khi vượt ngưỡng, đừng nạp cả tệp vào bộ nhớ rồi mới kiểm.
  • Nhận dạng kiểu theo nội dung để hiển thị đúng, nhưng đừng coi đó là phép bảo mật. Magic bytes hữu ích để đặt Content-Type, nhưng như polyglot cho thấy, chúng không đảm bảo tệp chỉ là ảnh.

Chỗ tôi không kết luận được

Tôi chứng minh RCE bằng cách phục vụ tệp với đuôi .php — mô phỏng hai tình huống thật: upload cho phép đuôi thực thi, hoặc web server cấu hình sai chạy mã trong thư mục upload. Tôi không dựng một cuộc tấn công đổi-đuôi hoàn chỉnh qua chính route upload; tôi đo hậu quả (mã chạy được) và điều kiện (đuôi thực thi + thư mục chạy mã), là hai mắt xích quyết định.

Và polyglot của tôi nhắm PHP. Cùng ý tưởng áp dụng cho các môi trường khác (một tệp SVG chứa JavaScript gây XSS khi xem, một tệp nén chứa đường dẫn traversal — "zip slip"), mỗi cái là một biến thể với chi tiết riêng mà tôi không đo hết.

Thử ba mươi giây

Nếu ứng dụng của bạn cho tải tệp lên, tìm hai điều trong mã. Thứ nhất: tên tệp lưu xuống đến từ đâu — từ request (client) hay do bạn sinh mới? Nếu từ client, bạn có thể có path traversal.

Thứ hai: tệp được lưu ở đâu, và web server có phục vụ trực tiếp thư mục đó không? Nếu thư mục upload nằm trong web root và server có thể chạy mã ở đó, một polyglot như trong bài này — một "ảnh" thực thi được — là một đường thẳng tới RCE.