Java審計之檔案操作漏洞

nice_0e3發表於2020-09-19

Java審計之檔案操作漏洞篇

0x00 前言

本篇內容打算把Java審計中會遇到的一些檔案操作的漏洞,都給敘述一遍。比如一些任意檔案上傳,檔案下載,檔案讀取,檔案刪除,這些操作檔案的漏洞。

0x01 檔案上傳漏洞

RandomAccessFile類上傳檔案案例:

package com.test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/FileUploadServlet")
public class domain extends HttpServlet {
    private static final long serialVersionUID = 1L;
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        InputStream inputStream = request.getInputStream();
        String realPath = request.getServletContext().getRealPath("/upload");

        System.out.println(realPath);
        File tempFile = new File(realPath,"temp.tmp");
        if (!tempFile.exists()){
            tempFile.createNewFile();
        }
        FileOutputStream fos = new FileOutputStream(tempFile);
        byte[] buffer = new byte[1024];
        int len = 0;
        while(-1 != (len = inputStream.read(buffer))){
            fos.write(buffer, 0, len);
        }
        RandomAccessFile randomFile = new RandomAccessFile(tempFile, "r");
        randomFile.readLine();
        String contentDisposition = randomFile.readLine();
        String filename = contentDisposition.substring(contentDisposition.indexOf("filename=\""), contentDisposition.lastIndexOf("\""));
        filename = filename.replace("filename=\"", "");
// 防止中文亂碼
        filename = new String(filename.getBytes("ISO-8859-1"),"UTF-8");
        System.out.println(filename);
        randomFile.seek(0);
        long start = 0;
        int forth = 1;
        while(-1 != (len = randomFile.readByte()) && (forth<=4)){
            if(len == '\n'){
                start = randomFile.getFilePointer();
                forth++;
            }
        }
        fos.close();
        inputStream.close();
        File saveFile = new File(realPath,filename);
        RandomAccessFile randomAccessFile = new RandomAccessFile(saveFile, "rw");
        randomFile.seek(randomFile.length());
        long endPosition = randomFile.getFilePointer();
        int j = 1;
        while((endPosition >= 0) && j <= 2){
            endPosition --;
            randomFile.seek(endPosition);
            if(randomFile.readByte() =='\n'){
                j++;
            }
        }
        randomFile.seek(start);
        long startPoint = randomFile.getFilePointer();
        while(startPoint < endPosition-1){
            randomAccessFile.write(randomFile.readByte());
            startPoint = randomFile.getFilePointer();
        }
        randomAccessFile.close();
        randomFile.close();
        tempFile.delete();
        System.out.println("檔案上傳成功");
    }
}

這裡並沒有校驗任何的檔案型別,進行了上傳。

commons-fileupload類上傳案例:

package com.test;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/FileUploadServlet")


public class domain extends HttpServlet{

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //得到上傳檔案的儲存目錄,將上傳的檔案存放於WEB-INF目錄下,不允許外界直接訪問,保證上傳檔案的安全
        String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        File file = new File(savePath);
        if(!file.exists()&&!file.isDirectory()){
            System.out.println("目錄或檔案不存在!");
            file.mkdir();
        }
        //訊息提示
        String message = "";
        try {
            //使用Apache檔案上傳元件處理檔案上傳步驟:
            //1、建立一個DiskFileItemFactory工廠
            DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
            //2、建立一個檔案上傳解析器
            ServletFileUpload fileUpload = new ServletFileUpload(diskFileItemFactory);
            //解決上傳檔名的中文亂碼
            fileUpload.setHeaderEncoding("UTF-8");
            //3、判斷提交上來的資料是否是上傳表單的資料
            if(!fileUpload.isMultipartContent(request)){
                //按照傳統方式獲取資料
                return;
            }
            //4、使用ServletFileUpload解析器解析上傳資料,解析結果返回的是一個List<FileItem>集合,每一個FileItem對應一個Form表單的輸入項
            List<FileItem> list = fileUpload.parseRequest(request);
            for (FileItem item : list) {
                //如果fileitem中封裝的是普通輸入項的資料
                if(item.isFormField()){
                    String name = item.getFieldName();
                    //解決普通輸入項的資料的中文亂碼問題
                    String value = item.getString("UTF-8");
                    String value1 = new String(name.getBytes("iso8859-1"),"UTF-8");
                    System.out.println(name+"  "+value);
                    System.out.println(name+"  "+value1);
                }else{
                    //如果fileitem中封裝的是上傳檔案,得到上傳的檔名稱,
                    String fileName = item.getName();
                    System.out.println(fileName);
                    if(fileName==null||fileName.trim().equals("")){
                        continue;
                    }
                    //注意:不同的瀏覽器提交的檔名是不一樣的,有些瀏覽器提交上來的檔名是帶有路徑的,如:  c:\a\b\1.txt,而有些只是單純的檔名,如:1.txt
                    //處理獲取到的上傳檔案的檔名的路徑部分,只保留檔名部分
                    fileName = fileName.substring(fileName.lastIndexOf(File.separator)+1);
                    //獲取item中的上傳檔案的輸入流
                    InputStream is = item.getInputStream();
                    //建立一個檔案輸出流
                    FileOutputStream fos = new FileOutputStream(savePath+File.separator+fileName);
                    //建立一個緩衝區
                    byte buffer[] = new byte[1024];
                    //判斷輸入流中的資料是否已經讀完的標識
                    int length = 0;
                    //迴圈將輸入流讀入到緩衝區當中,(len=in.read(buffer))>0就表示in裡面還有資料
                    while((length = is.read(buffer))>0){
                        //使用FileOutputStream輸出流將緩衝區的資料寫入到指定的目錄(savePath + "\\" + filename)當中
                        fos.write(buffer, 0, length);
                    }
                    //關閉輸入流
                    is.close();
                    //關閉輸出流
                    fos.close();
                    //刪除處理檔案上傳時生成的臨時檔案
                    item.delete();
                    message = "檔案上傳成功";
                }
            }
        } catch (FileUploadException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            message = "檔案上傳失敗";
        }
        request.setAttribute("message",message);
        request.getRequestDispatcher("/message.jsp").forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }

}

這裡判斷了檔案是否為空,但是沒有判斷檔案的型別。

public class UploadHandleServlet1 extends HttpServlet{

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //得到上傳檔案的儲存目錄,將上傳的檔案存放於WEB-INF目錄下,不允許外界直接訪問,保證上傳檔案的安全
        String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        //上傳時生成的臨時檔案儲存目錄
        String tempPath = this.getServletContext().getRealPath("/WEB-INF/temp");
        File file = new File(tempPath);
        if(!file.exists()&&!file.isDirectory()){
            System.out.println("目錄或檔案不存在!");
            file.mkdir();
        }
        //訊息提示
        String message = "";
        try {
            //使用Apache檔案上傳元件處理檔案上傳步驟:
            //1、建立一個DiskFileItemFactory工廠
            DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
            //設定工廠的緩衝區的大小,當上傳的檔案大小超過緩衝區的大小時,就會生成一個臨時檔案存放到指定的臨時目錄當中。
            diskFileItemFactory.setSizeThreshold(1024*100);
            //設定上傳時生成的臨時檔案的儲存目錄
            diskFileItemFactory.setRepository(file);
            //2、建立一個檔案上傳解析器
            ServletFileUpload fileUpload = new ServletFileUpload(diskFileItemFactory);
            //解決上傳檔名的中文亂碼
            fileUpload.setHeaderEncoding("UTF-8");
            //監聽檔案上傳進度
            fileUpload.setProgressListener(new ProgressListener(){
                public void update(long pBytesRead, long pContentLength, int arg2) {
                    System.out.println("檔案大小為:" + pContentLength + ",當前已處理:" + pBytesRead);
                }
            });
            //3、判斷提交上來的資料是否是上傳表單的資料
            if(!fileUpload.isMultipartContent(request)){
                //按照傳統方式獲取資料
                return;
            }
            //設定上傳單個檔案的大小的最大值,目前是設定為1024*1024位元組,也就是1MB
            fileUpload.setFileSizeMax(1024*1024);
            //設定上傳檔案總量的最大值,最大值=同時上傳的多個檔案的大小的最大值的和,目前設定為10MB
            fileUpload.setSizeMax(1024*1024*10);
            //4、使用ServletFileUpload解析器解析上傳資料,解析結果返回的是一個List<FileItem>集合,每一個FileItem對應一個Form表單的輸入項
            List<FileItem> list = fileUpload.parseRequest(request);
            for (FileItem item : list) {
                //如果fileitem中封裝的是普通輸入項的資料
                if(item.isFormField()){
                    String name = item.getFieldName();
                    //解決普通輸入項的資料的中文亂碼問題
                    String value = item.getString("UTF-8");
                    String value1 = new String(name.getBytes("iso8859-1"),"UTF-8");
                    System.out.println(name+"  "+value);
                    System.out.println(name+"  "+value1);
                }else{
                    //如果fileitem中封裝的是上傳檔案,得到上傳的檔名稱,
                    String fileName = item.getName();
                    System.out.println(fileName);
                    if(fileName==null||fileName.trim().equals("")){
                        continue;
                    }
                    //注意:不同的瀏覽器提交的檔名是不一樣的,有些瀏覽器提交上來的檔名是帶有路徑的,如:  c:\a\b\1.txt,而有些只是單純的檔名,如:1.txt
                    //處理獲取到的上傳檔案的檔名的路徑部分,只保留檔名部分
                    fileName = fileName.substring(fileName.lastIndexOf(File.separator)+1);
                    //得到上傳檔案的副檔名
                    String fileExtName = fileName.substring(fileName.lastIndexOf(".")+1);
                    if("jsp".equals(fileExtName)||"rar".equals(fileExtName)||"tar".equals(fileExtName)||"jar".equals(fileExtName)){
                        request.setAttribute("message", "上傳檔案的型別不符合!!!");
                        request.getRequestDispatcher("/message.jsp").forward(request, response);
                        return;
                    }
                    //如果需要限制上傳的檔案型別,那麼可以通過檔案的副檔名來判斷上傳的檔案型別是否合法
                    System.out.println("上傳檔案的副檔名為:"+fileExtName);
                    //獲取item中的上傳檔案的輸入流
                    InputStream is = item.getInputStream();
                    //得到檔案儲存的名稱
                    fileName = mkFileName(fileName);
                    //得到檔案儲存的路徑
                    String savePathStr = mkFilePath(savePath, fileName);
                    System.out.println("儲存路徑為:"+savePathStr);
                    //建立一個檔案輸出流
                    FileOutputStream fos = new FileOutputStream(savePathStr+File.separator+fileName);
                    //建立一個緩衝區
                    byte buffer[] = new byte[1024];
                    //判斷輸入流中的資料是否已經讀完的標識
                    int length = 0;
                    //迴圈將輸入流讀入到緩衝區當中,(len=in.read(buffer))>0就表示in裡面還有資料
                    while((length = is.read(buffer))>0){
                        //使用FileOutputStream輸出流將緩衝區的資料寫入到指定的目錄(savePath + "\\" + filename)當中
                        fos.write(buffer, 0, length);
                    }
                    //關閉輸入流
                    is.close();
                    //關閉輸出流
                    fos.close();
                    //刪除處理檔案上傳時生成的臨時檔案
                    item.delete();
                    message = "檔案上傳成功";
                }
            }
        } catch (FileUploadBase.FileSizeLimitExceededException e) {
            e.printStackTrace();
            request.setAttribute("message", "單個檔案超出最大值!!!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }catch (FileUploadBase.SizeLimitExceededException e) {
            e.printStackTrace();
            request.setAttribute("message", "上傳檔案的總的大小超出限制的最大值!!!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }catch (FileUploadException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            message = "檔案上傳失敗";
        }
        request.setAttribute("message",message);
        request.getRequestDispatcher("/message.jsp").forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
    //生成上傳檔案的檔名,檔名以:uuid+"_"+檔案的原始名稱
    public String mkFileName(String fileName){
        return UUID.randomUUID().toString()+"_"+fileName;
    }
    public String mkFilePath(String savePath,String fileName){
        //得到檔名的hashCode的值,得到的就是filename這個字串物件在記憶體中的地址
        int hashcode = fileName.hashCode();
        int dir1 = hashcode&0xf;
        int dir2 = (hashcode&0xf0)>>4;
        //構造新的儲存目錄
        String dir = savePath + "\\" + dir1 + "\\" + dir2;
        //File既可以代表檔案也可以代表目錄
        File file = new File(dir);
        if(!file.exists()){
            file.mkdirs();
        }
        return dir;
    }
}

這段程式碼和上面不同的是新增多了一個黑名單,多了一個判斷條件, if("jsp".equals(fileExtName)||"rar".equals(fileExtName)||"tar".equals(fileExtName)||"jar".equals(fileExtName),但是這樣的黑名單還是能過去繞過的。

主要的審計要是看上傳地方是不是黑名單,如果是黑名單,該怎麼去繞過。如果是白名單,在jdk低版本中也可以使用%00截斷。

驗證Mime型別檔案上傳案例

public class mimetype {
    public static String main(String fileUrl) throws IOException {
        String type = null;
        

        URL u = new URL(fileUrl);

        URLConnection uc = u.openConnection();
        type = uc.getContentType();
        return type;
    }
}

0x01 任意檔案讀取

任意檔案讀取漏洞其實比較簡單,基本上就2種方法,一個是位元組輸入流InputStream,一個是FileReader字元輸入流。

InputStream:

@WebServlet("/readServlet")
public class readServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String filename = request.getParameter("filename");
        File file = new File(filename);
        OutputStream outputStream = null;
        InputStream inputStream = new FileInputStream(file);
        
        int len;
        byte[] bytes = new byte[1024];
        while(-1 != (len = inputStream.read())) {
            outputStream.write(bytes,0,len);
        }
}}

FileReader:

@WebServlet("/downServlet")
public class readServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String filename = request.getParameter("filename");
        String fileContent = "";
        FileReader fileReader = new FileReader(filename);
        
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        String line = "";
        while (null != (line = bufferedReader.readLine())) {
            fileContent += (line + "\n");
        }
        }
}

這兩種方法除了讀寫方式不一樣外,其餘的都是一樣的。

0x02 任意檔案下載

在前面的ssrf中其實提到了這個檔案讀取和下載,但是ssrf中是進行了遠端請求的時候獲取的輸入流,然後進行輸出。而在任意檔案讀取或下載中,是直接去使用io流進行讀寫,顯示出來給我們。

@WebServlet("/downServlet")
public class readServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        String filename = request.getParameter("filename");
        String fileContent = "";
        FileReader fileReader = new FileReader(filename);
        response.setHeader("content-disposition", "attachment;fileName=" + filename);
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        String line = "";
        while (null != (line = bufferedReader.readLine())) {
            fileContent += (line + "\n");
        }
        }
}

和前面的檔案讀取也差不多,只是多了設定了一個響應體。

0x03 任意檔案刪除

@WebServlet("/downServlet")
public class readServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String filename = request.getParameter("filename");
        File file = new File(filename);
        PrintWriter writer = response.getWriter();
        if(file != null && file.exists() && file.delete()) {
            writer.println("刪除成功");
        } else {
            writer.println("刪除失敗");
        }
}
}

參考文章

https://www.cnblogs.com/lcngu/p/5471610.html
https://xz.aliyun.com/t/6986

0x04 結尾

本文的一些程式碼其實比較簡單,但是如果實際中還是需要注意一些可能產生漏洞的點。

相關文章