Java Servlet FileUpload Vulnerability
Java Servlet FileUpload Vulnerability by @Phatmh
Lỗ hổng File Upload
Bản chất của File Upload: File Upload đối với tôi nó đơn giản chỉ là lợi
dụng Unsafe Method để truyền một Untrusted Data vào nhằm thay đổi hành
vi của hệ thống trong trường hợp này là Web App, với FileUpload những gì
User Upload lên sẽ chính là Untrusted Data và với Feature Upload File
như này sẽ thế nào nếu nó không được Validate một cách cẩn thận ta sẽ
đến với DEMO bằng Java Servlet.
Web App Overview
Đây là một Web App được dựng với mục đích như một môi trường test các
case phổ biến về lỗ hổng FileUpload. Feature chính của nó bao gồm:
- Upload File
- View File
- Delete File
Hình ảnh overview
của trang web. Và ở đây mình code theo từng level, mỗi level tương ứng
với mỗi cơ chế validate khác nhau và ở đây ta sẽ phải tìm các Bypass và
đi đên RCE.

Đi vào phân tích code
Ở đây mình dùng@WebServlet để ánh xạ path của web chứa chức năng file upload đến
index.jsp vì ở đây mình làm trang web chứa nhiều lỗ hổng nên việc chia
ra từng alias là một ứng dụng rất cần thiết. Rồi đến với đoạn code đầu
tiên của class thì ta có đoạn getUploadPath đoạn này để define thư mục
mà mình sẽ Upload File lên cụ thể ở đây các file sẽ nằm ở /upload.
Đoạn code doGet()
trong Servlet này dùng để xử lý các request HTTP GET gửi tới endpoint
hello-file-upload. Đây là phần quan trọng của chức năng quản lý file
upload, bao gồm cả việc tạo thư mục upload nếu chưa có và xoá file nếu
có yêu cầu.
response.setContentType("text/html");
PrintWriter out = response.getWriter();
- Dòng này thiết lập định dạng của response là text/html, tức là nội
dung trả về là HTML. - PrintWriter out cho phép bạn ghi dữ liệu HTML vào response để hiển
thị trên trình duyệt.
1 | if (!uploadDir.exists()) uploadDir.mkdir(); |
1 | if (deleteFile != null) { |
1 | if (files != null && files.length > 0) { |
1 | out.println("<li>" + fname + |
1 | } else { |
1 | String selectedCase = request.getParameter("case"); |
1 | Part filePart = request.getPart("file"); |
1 | String filename = filePart.getSubmittedFileName(); |
1 | InputStream fileContent = filePart.getInputStream(); |
- Lấy nội dung của File.
Đi vào phân tích các case lỗi
Case1 : FileUpload Without Validation
Với case đầu tiên
thì nó chỉ đơn giản là một chức năng Upload File nhưng không hề có một
lớp phòng thủ nào vì thế attacker sẽ có thể dễ dàng thực hiện Upload một
file thực thi nguy hiểm để RCE được WebApp.
Chọn Lv1 là no
filter.
Thử Upload
lên một file .txt

Test thử chức năng view file, có thể thấy rằng các file được upload lên
sẽ nằm ở thư mục/upload. Với case này thì rõ ràng là nó không hề có
một lớp filter nào vậy nên việc Upload Shell sẽ khá là đơn giản.
Viết một File
shell.jsp với nội dung như trên.
Tiến hành Upload
shell.jsp lên và nó sẽ nằm ở thư mục /upload.
http://localhost:1337/vulnerability_web_war_exploded/upload/shell.jsp?cmd=whoami
Tiến hành truyền câu lệnh vào query ?cmd ở đây tôi dùng whoami và đã
thành công thực thi câu lệnh RCE

Case 2 : First Dot Split

String[] parts = filename.split("\\.");
Tách tên file bằng dấu “.”. Ví dụ: - “webshell.jsp” → [“webshell”,
“jsp”] - “webshell.jsp.jpg” → [“webshell”, “jsp”, “jpg”] -
split(“\.”) dùng \. vì . là ký tự đặc biệt trong regex.
String ext2 = parts.length > 1 ? parts[1].toLowerCase() : "";
- Lấy phần mở rộng thứ 2, tức là index 1 Và lỗi đã xảy ra ở đây, lớp
filter này chỉ có thể hoạt động trong trường hợp file mình upload
lên chỉ có 1 dấu.trong trường hợp này ta hoàn toàn có thể dễ
dàng Bypass bằng cách lợi dụng hành vi chỉ nhận dấu chấm đầu tiên
bằng cách tạo 1 file có tênshell.jpg.jspthì ở đây sau dấu chấm
đầu tiên nó sẽ nhận định đây là file jpg nên sẽ đi qua lớp filter dễ
dàng.
Chọn case
2 và Upload thử file shell.jsp và đã bị dính filter.
Thay đổi tên
file bằng cách thêm 1 extension là.jpgphía trước là file đã
thànhshell.jpg.jspvà response trả về là 302 chứng tỏ file đã
được upload thành công.
File shell thực
thi đã xuất hiện trong /upload.
Thành công RCE
với câu lệnh whoami trả về kết quả như trên.
Case 3 : Last Dot Check
Chọn case 3, lúc này
file shell trước đã được xóa để tránh nhầm lẫn.

String ext3 = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
lastIndexOf(‘.’): tìm vị trí dấu chấm cuối cùng trong tên file.
substring(…): lấy tất cả ký tự sau dấu chấm đó → chính là đuôi
file thực tế.toLowerCase(): chuẩn hóa chữ thường để không bị bypass bởi JSP. Tại
đây có thể thấy rằng lớp filter đã khá là cứng rồi vì nó sẽ check ở
dấu chấm cuối cùng cho nên nếu ta test theo các case trước sẽ không
còn tác dụng nữa. Vậy mindset ở đây là liệu ngoài jsp ra thì mặc
định nó còn thực thi file nào khác nữa không? Sau một lúc tìm hiểu
thì ta có thể Bypass được bằng filejspxvì lớp filter chỉ bắt mỗijsp.
Thành
công đi qua lớp filter này bằng cách lợi dụng sự bất cẩn của dev ghi
chặn nhưng không hết các đuôi file có thể thực thi. Ở đây sau khi
tìm hiểu thì tomcat sẽ hiểu định dạngjspxlà jsp xml vậy nên ta
cần sửa lại một chút trong file shell.jspx
Thành công RCE
được.
Case 4 : JSP Block Only
Đến với case này thì
nó vẫn là kiểm tra chỉ cần có tồn tại jsp ở cuối filename là sẽ dính
filter nhưng mà cũng như ở case 3 ta có thể tìm kiếm file khác ngoài jsp
có thể thực thi như jspx đã được test ở bên trên. Tính ra case 3 ở đây
khá giống case 4 nhưng nếu như nó chặn hẳn jsp và jspx thì vẫn sẽ có
cách Bypass nhưng với điều kiện là tùy vào config của Web App, với tùy
trường hợp config ta có thể sử dụng. Nhưng ở đây có một case dễ khả thi
là sử dụng dấu . lợi dụng config up một file shell.jsp. lên, dựa
theo tìm hiểu về config của tomcat thì nó vẫn sẽ nhận là file jsp nếu
không được config cẩn thận thì có thể lợi dụng nó.
Test thửshell.jsp. và thành công upload lên.
Trong danh sách đã
hiển thị các thư mục được upload và có file shell nằm trong đó.
Thành công lợi dụng
config để upload RCE.
Case 5 : Content-Type Filter
Ở đây server chỉ
kiểm tra Content-Type trong phần header của file upload, chứ không kiểm
tra extension hoặc nội dung thực tế của file vậy nên có thể Bypass dễ
dàng bằng cách khiến nó hiểu rằng File thực thi là một File hoặc bất kì
file nào mà nó allow.
Mod lại content type
thành image/png

Thành công bypass qua lớp filter bằng cách lừa đây là một file image.
File shell.jsp đã có
giờ ta chỉ cần RCE như các case trên.
Case 6 : Magic Bytes Check
- 89504E47 là magic
bytes chuẩn của file PNG (\x{=tex}89PNG) - Đoạn code này sẽ check 4
bytes đầu để kiểm tra file được đưa lên có phải là file PNG không nếu
không thì sẽ bị chặn. Nhưng ở đây cho dù check được vào trong magic byte
nhưng vẫn chưa đủ để validate hết vì ta hoàn toàn có thể trick được hệ
thống bằng cách bỏ thêm đoạn\x89PNGvào trước các dòng payload để khi
nó đọc sẽ nhận định đây chính là file PNG vì nó chỉ nhận 4 bytes đầu.
Tạo một file
shell.jsp bằng linux.
Tạo một file
fake.jsp đưa magic byte vào đó để nó sẽ nhận là image sau đó ghép với
file shell.jsp bây giờ nội dung shell.jsp sẽ được đưa vào fake.jsp mà
magic byte của fake.jsp được giữ nguyên.
Kiểm tra lại nội
dung fake.jsp.
Tiến
hành upload và upload thành công.

Thành công RCE.
Case 7 : Path Traversal + FileUpload To RCE
Ở case này thì cũng
không có filter vì ở đây mình muốn mô phỏng tình huống là tại thư mục
upload nó sẽ được config là không cho phép run bất kì file thực thi nào,
vậy nếu rơi vào trường hợp đó thì có cách Path Traversal là có thể lợi
dụng được vì ta có thể thử với thư mục khác liệu thư mục đó có thực thi
được các file thực thi hay không.
Tiến hành upload thử../shell.jsp và có thể thấy file đã được upload lên nhưng liệu nó có
đi ra khỏi thư mục /upload không.
Check ở bên trong
cấu trúc thư mục thì có thể thấy rằng file shell.jsp đã thoát ra khỏi
thư mục upload.
Kiểm
tra trong này thì nó kêu chưa có thư mục được upload và củng cố được
rằng file shell đã được upload ra ngoài thư mục cha. Bây giờ chỉ cần
truy cập đến và tiến hành RCE thôi.
Cơm thêm
Có vẻ như lỗi FileUpload ta còn có thể khai thác thêm một lỗi nữa là
Store XSS vì ở các Level có lớp filter check extension có vẻ như nó
không hề chặn file .html. Trình duyệt thực thi được script trong file
.html sau khi upload là vì server không cài đặt Content-Disposition:
attachment, và MIME type của file là text/html, nên trình duyệt xử lý
file như một trang web. Đầu tiên tạo một file xss.html với nội dung:
1 | <script> |
Tiến hành Upload thử file lên.
Thành công Upload
File xss.html lên bây giờ nếu ta là user bình thường bấm thử view thì
nó sẽ trả về như thế nào.
Có thể thấy xss đã
được thực thi tại trường hợp này thì file đã được lưu và nếu user click
vào xem nó sẽ thực thi XSS và nó là store XSS, ở đây web này tôi không
khởi tạo session id nên không thể DEMO được XSS để lấy cắp cookie bằng
fetch và Webhook được.