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:

  1. Upload File
  2. View File
  3. Delete File

image

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.

image

Đi vào phân tích code

image

Ở đâ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.

image

Đ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
2
3
4
5
6
7
8
    if (!uploadDir.exists()) uploadDir.mkdir();

- Kiểm tra nếu thư mục upload chưa tồn tại thì tạo thư mục mới.

```java
String deleteFile = request.getParameter("delete");

- Lấy giá trị của query parameter delete từ URL.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    if (deleteFile != null) {
File fileToDelete = new File(uploadPath, deleteFile);
if (fileToDelete.exists()) {
fileToDelete.delete();
response.sendRedirect("hello-file-upload");
return;
} else {
out.println("<p style='color:red;'>File not found.</p>");
}
}

- Xoá file nếu tồn tại.
- ![image](https://hackmd.io/_uploads/ryCj6z-Pex.png) Đoạn HTML để
render ra được các chức năng.

![image](https://hackmd.io/_uploads/Sk6gAMbPxl.png) Hiển thị tất cả file
trong thư mục upload dưới dạng danh sách HTML. Mỗi file có 2 tùy chọn: -
View: mở file trong tab mới. - Delete: gửi request để xóa file.

File[] files = uploadDir.listFiles();

- Lấy toàn bộ file trong thư mục upload (đã được tạo và gán ở phần
trước).
1
2
3
4
5
6
7
8
    if (files != null && files.length > 0) {
out.println("<ul>");
for (File f : files) {
String fname = f.getName();

- Nếu thư mục không rỗng, duyệt từng file để in ra dưới dạng danh sách
`(<ul> và <li>)`

1
2
3
4
5
6
    out.println("<li>" + fname +
" [<a href='" + request.getContextPath() + "/upload/" + fname + "' target='_blank'>View</a>] " +
"[<a href='?delete=" + fname + "' onclick='return confirm(\"Delete " + fname + "?\")'>Delete</a>]</li>");

- Tạo link View và Delete.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    } else {
out.println("No uploaded files.");
}

- Nếu thư mục rỗng (không có file), in ra thông báo "No uploaded
files."

![image](https://hackmd.io/_uploads/S1zhNmbDel.png)

String uploadPath = getUploadPath(request);
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) uploadDir.mkdir();

- Tạo thư mục upload nếu chưa có
- getUploadPath(request) trả về đường dẫn thư mục upload trên server.
- File uploadDir = new File(uploadPath) tạo đối tượng File để thao
tác.
- mkdir() tạo thư mục nếu chưa tồn tại.
1
2
3
4
    String selectedCase = request.getParameter("case");

- Lấy giá trị của tham số case trong form upload.

1
2
3
4
    Part filePart = request.getPart("file");

- filePart là đối tượng chứa toàn bộ dữ liệu file được upload.
- request.getPart("file") dựa vào tên input trong HTML form:
1
2
3
4
    String filename = filePart.getSubmittedFileName();

- Lấy tên file gốc.

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

image

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.
image
Chọn Lv1 là no
filter.

image

Thử Upload
lên một file .txt

image

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.

image

Viết một File
shell.jsp với nội dung như trên.

image

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

image

Case 2 : First Dot Split

image

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ên shell.jpg.jsp thì ở đâ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. image Chọn case
    2 và Upload thử file shell.jsp và đã bị dính filter.
    image Thay đổi tên
    file bằng cách thêm 1 extension là .jpg phía trước là file đã
    thành shell.jpg.jsp và response trả về là 302 chứng tỏ file đã
    được upload thành công.
    image File shell thực
    thi đã xuất hiện trong /upload.
    image 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

image

Chọn case 3, lúc này
file shell trước đã được xóa để tránh nhầm lẫn.

image

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 file jspx vì lớp filter chỉ bắt mỗi
    jsp.

    image

    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ạng jspx là jsp xml vậy nên ta
    cần sửa lại một chút trong file shell.jspx

    image

    image

    Thành công RCE
    được.

    image

Case 4 : JSP Block Only

image

Đế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ó.

image

Test thử
shell.jsp. và thành công upload lên.

image

Trong danh sách đã
hiển thị các thư mục được upload và có file shell nằm trong đó.

image

Thành công lợi dụng
config để upload RCE.

Case 5 : Content-Type Filter

image

Ở đâ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.

image

Mod lại content type
thành image/png

image

Thành công bypass qua lớp filter bằng cách lừa đây là một file image.

image

File shell.jsp đã có
giờ ta chỉ cần RCE như các case trên.

Case 6 : Magic Bytes Check

image

  • 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 \x89PNG và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.

image

Tạo một file
shell.jsp bằng linux.

image

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.

image

Kiểm tra lại nội
dung fake.jsp.

image

Tiến
hành upload và upload thành công.

image

image

Thành công RCE.

Case 7 : Path Traversal + FileUpload To RCE

image

Ở 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.

image

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.

image

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.

image

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
2
3
<script>
alert(1);
</script>

Tiến hành Upload thử file lên.
image

image

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.

image

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.