File upload vulnerabilities
Cách học tốt nhất là thực hành
Last updated
Cách học tốt nhất là thực hành
Last updated
Xây dựng lab thực hành để hiểu rõ vấn đề:
Giả sử, xây dựng 1 trang upload file như hình với phần xử lí tệp tải lên như sau:
Phân tích:
Đối với đoạn code trên. Tệp đã tải lên được lưu trữ trong thư mục được tạo /upload theo sau là session_id của người dùng hiện tại. Nếu thư mục không tồn tại, nó sẽ được tạo bằng hàm mkdir().
Dòng :
move_uploaded_file($_FILES["file"]["tmp_name"], $file);
có chức năng, kiểm tra xem có tệp nào được tải lên trong yêu cầu hay không. Nếu có, nó sẽ cố gắng di chuyển tệp đã tải lên vào thư mục được tạo ở bước 1 bằng hàm move_uploaded_file()
$file = $dir . "/" . $_FILES["file"]["name"];
Dòng code này nối đường dẫn thư mục $dir
với tên tệp đã tải lên $_FILES["file"]["name"]
để tạo đường dẫn đầy đủ với tệp đã tải lên máy chủ.
=> Như vậy ở đây có thể đưa ra nhận định:
File đã tải lên có thể truy cập từ đường dẫn /upload/session_id/{tên_file}
Server không có biện pháp sàng lọc nào đối với bất kì file nào được upload lên.
=> Kiểm chứng nhận định:
Upload một tệp bất kì. Ở đây upload thử một file test.txt với nội dung file là test.
Upload một tệp file php với nội dung <?php phpinfo(); ?>
Đoạn script trong file test.php được thực thi. Lưu ý khi thực hiện đánh giá lỗ hổng File upload: Đừng bao giờ chèn luôn payload khai thác ngay từ đầu. Hãy thử với những payload vô hại trước tiên như <?php phpinfo(); ?>
vì có thể bạn bị chặn bởi tường lửa và chương trình antivirus được cài đặt để bảo vệ trang web.
Tiến hành gửi lại file chèn payload sau để RCE: <? php echo system($_GET['cmd']); ?>
Như vậy, ta đã RCE được đến server web. Tiến hay khai thác thêm để tìm flag.
Đến đây bạn có thể tạo kết nối về máy để có shell nhìn đẹp và thuận tiện hơn. Nhưng vì bài đơn giản nên mình bỏ qua bước này mà làm luôn trên Browser:
Sau khi báo với người người quản lí vấn đề này. Sau một thời gian, họ gửi lại cho tôi kiểm tra lại xem liệu đó đã được giải quyết hay chưa.
Phân tích:
Dường như không có gì thay đổi ở phần lưu trữ file, vẫn là /upload/session_id/{tên_file}
Điểm khác duy nhất ở đây đó là, dev đã sử dụng hàm explode() để filter extension của file tải lên.
Hàm explode trong PHP cho phép bạn chuyển một chuỗi sang một mảng dựa trên các ký tự phân cách
Ví dụ: ta upload 1 file tên là test.php. Thì bây giờ biến hàm explode() sẽ tách tên file thành 2 phần tử [test, php] và biến $extension sẽ lấy phần tử có index = 1 đem so sánh "===" với "php" . Như vậy giá trị phần tử có index[1] ở đây là php===php => Alert "Hack detected" được đưa ra và những đoạn code sau không được thực thi .
Đặt giả thuyết :
1. Ta biết rằng HTTPd chỉ lấy đuôi file (extension) cuối cùng để quyết định xem có xử lý như code PHP không. Liệu có cách nào tạo ra file name mà đuôi file cuối cùng vẫn là *.php
nhưng khi vào explode
thì đuôi file php
không nằm ở vị trí 1 --> để né đoạn filter.
2. HTTPD có nhất thiết chỉ nhận diện extension file php
để đưa cho mod-php xử lý hay không? Tức là liệu còn đuôi (extension) nào ngoài php
mà HTTPD vẫn thực thi, mà ta chưa biết không?
Kiểm chứng giả thuyết:
Giả thuyết 1:
Bây giờ ta upload 1 file có tên là : test.txt.php. Như vậy hàm explode() bây giờ sẽ tách tên file thành 3 phần tử [test, txt, php] và biến $extension sẽ thấy phần tử có index[1] là txt đi so sánh "===" với php. Như vậy là khác nhau => Nó filter.
Thực hiện: 1. Đây là khi upload 1 file test.php như bình thường. Đúng như phân tích ở trên.
2.Upload file test.txt.php với nội dung <?php echo system($_GET['cmd']); ?>
Như đã đặt giả thuyết, ta bypass thành công filter và thực hiện RCE thành công:
Đối với giả thuyết 2, sẽ giải thích ở level 3.
Ok. Sau khi báo cáo với dev giải quyết vấn đề trên. Anh dev đã gửi lại cho tôi để kiểm tra lại một lần nữa:
Phân tích:
Đường dẫn file sau khi upload vẫn được truy cập tại /upload/session_id/{tên_file}
Anh dev đã sửa lỗi lấy giá trị có index=1 sau khi sử dụng hàm explode() bằng cách lồng vào hàm end(). Ở đây, hàm end có chức năng chỉ lấy giá trị của phần tử cuối cùng, đi so sánh "===' với php. Điều này có thể giải quyết được 1 phần của level 2. Ví dụ: Attacker upload lên file: test.txt.php => hàm explode() xử lí, tách tên file thành 1 mảng gồm 3 phần tử [test, txt, php] và ở đây giá trị cuối cùng luôn là php vì vậy filter thành công. Sẽ có bạn thắc mắc là thế mình để php ở vị trí thứ 2 thì sao (test.php.txt) thì đơn giản HTTPd thực thi lệnh với đuôi .php, thì câu lệnh trong nội dung của file mới được thực hiện.
Vậy, sẽ ra sao nếu extension của file đó không phải là .php mà là biến thể khác của nó ví dụ như php5, phtml, phar
Đặt giả thuyết:
HTTPD có nhất thiết chỉ nhận diện extension file php
để đưa cho mod-php xử lý hay không? Tức là liệu còn đuôi (extension) nào ngoài php
mà HTTPD vẫn thực thi, mà ta chưa biết không?
Chứng minh giả thuyết:
Upload file test.phtml với nội dung : <?php echo system($_GET['cmd']); ?>
Như vậy, như giả định. Đã bypass thành công filter và nội dung của file vẫn được thực thi:
Lần này, anh dev đã khẳng định rằng mọi thứ đã được filter rất cẩn thận. Hãy cũng xem anh dev đã làm gì:
Phân tích:
Ở level này, anh dev đã sử dụng một danh sách đen các extension php, pthml, phar và kết hợp với sử dụng hàm end() lồng explode () để giải quyết 2 level ở trên
Đặt vấn đề:
Ta biết rằng cấu hình sẽ quyết định hành vi của server Apache. Vậy sẽ ra sao nếu chúng ta có thể thay đổi được cấu hình ấy. Liệu ta có thể tao túng hành vi của Apache được hay không ? Nhờ vào file config mà mình đã yêu cầu anh dev gửi cho mình thì ta thấy rằng, với cấu hình này, ta có thể upload file .htaccess . " .htaccess" được phân bổ ở các thư mục và có hiệu lực ở thư mục hiện tại và các thư mục con.
Kiểm chứng vấn đề:
Tập tin .htaccess (hypertext access) là một file có ở thư mục gốc của các hostting và do apache quản lý, cấp quyền. File .htaccess có thể điều khiển, cấu hình được nhiều thứ với đa dạng các thông số, nó có thể thay đổi được các giá trị được set mặc định của apache.
Như vậy, với định nghĩa ở trên thì ta sẽ upload file .htaccess với nội dung file:
AddType application/x-httpd-php .misa
Giải thích: Với nội dung trên, Apache sẽ cho upload file có extension .misa được thực thi chạy như .php
Bây giờ, upload 1 file test.misa với nội dung file :
<?php echo system($_GET['cmd']); ?>
RCE :
Có vẻ như anh dev đã hơi không hài lòng về sự bất cẩn của bản thân khi config Apache lỗi như vậy. Anh ấy gửi lại mình 1 lần nữa để kiểm tra:
Phân tích:
Thử lại các trường hợp trên thì anh dev đã khắc phục thành công.
Ở đoạn code này, anh dev có sử dụng một danh sách trắng, chỉ cho upload những file có MIME dạng : image/jpeg, image/png, image/gif. Như vậy ở đây có nghĩa là : ngoài những file này ra thì không thể upload file nào khác
Đặt vấn đề:
Vậy sẽ ra sao nếu ta có thể thay đổi MIME-type bằng một công cụ thứ 3 - Burp suite ?
Chứng minh vấn đề:
Upload 1 file php bình thường. Sử dụng Burp suite để chặn request và sửa đổi Content-type thành image/png
RCE:
Lần này anh dev đã khẳng định với tôi rằng anh ta đã filter mọi trường hợp. Và nhờ tôi kiểm tra lại:
Phân tích:
Hàm finfo_file()
được sử dụng để lấy loại MIME của tệp đã tải lên. Hàm này yêu cầu một tài nguyên được tạo bởi finfo_open()
, trả về một tài nguyên thông tin của tệp. Tài nguyên này được sử dụng để phân tích nội dung của tệp và xác định loại MIME của nó.
Biến $whitelist
là một mảng chứa các loại MIME được phép cho các tệp đã tải lên. Ở đây, chỉ cho phép tải lên các tệp : JPEG, PNG, GIF
Hàm in_array()
kiểm tra xem mime_type
có trong $whitelist
hay không. Nếu không có trong danh sách, quá trính thực thi mã sẽ dừng lại và in ra thông báo. Điều này giúp ngăn chặn việc thực thi mã độc hại nếu tệp tải lên không thuộc loại dự kiến
Đặt vấn đề:
Liệu có cách nào có thể giả mạo MIME tệp tải lên để đánh lừa hệ thống ?
Giải quyết vấn đề:
Bằng một số công cụ, ta có thể sửa signature của file. Ví dụ như exiftool,... Hoặc đơn giản thì chỉ cần thêm kí tự ISO 8859-1 vào đầu nội dung file PHP.
ví dụ: GIF89a<?php echo system($_GET['cmd]); ?>
Upload file PHP test.php với nội dung GIF89a<?php echo system($_GET['cmd]); ?>
RCE