본문 바로가기
CS/시큐어 코딩

[시큐어 코딩] 파일 업로드/다운로드 취약점

by J-rain 2024. 6. 2.

 

 

파일 업로드/다운로드 취약점

파일을 업로드하고 다운로드하는 기능들이 적절히 제어되지 않을 때 웹쉘이나 악성코드가 업로드되어 서버에서 실행될 수 있으며, 악성코드가 삽입된 파일을 사용자들이 다운로드받아서 실행하여 사용자 PC가 침해사고를 당할 수 있다.

 

 

발생 원인

업로드기능 취약점 원인

  • 업로드되는 파일의 타입을 제한하지 않는 경우
  • 업로드되는 파일의 크기나 개수를 제한하지 않는 경우
  • 업로드된 파일을 외부에서 직접적으로 접근 가능한 경우
  • 업로드된 파일의 이름과 저장된 파일의 이름이 동일해 공격자가 파일을 인식 가능한 경우
  • 업로드된 파일이 실행권한을 가지는 경우

다운로드기능 취약점 원인

  • 파일에 대한 접근권한이 없는 사용자가 직접적인 경로를 통해 파일을 다운로드할 수 있는 경우
  • 악성코드에 감염된 파일이 다운로드 허용되는 경우

 

 

공격기법

1. 업로드된 파일에 직접 접근 가능한지 확인하고 웹쉘을 업로드하여 실행 가능한지 체크

 

>> 업로드 경로 확인

파일 업로드 기능이 있는 페이지에서 정상적으로 이미지파일을 업로드해 해당 이미지의 업로드 경로를 확인한다.

 

 

>> 업로드된 이미지 파일 직접 호출

업로드한 파일에 직접 접근 가능한지 확인한다. 파일이 저장된 경로(URL)을 복사하여 다른 종류의 브라우저에서 요청하여 파일에 접근 가능한지를 확인한다.

 

 

>> 웹쉘을 업로드하여 호출

업로드되는 파일의 타입에 제약이 없고, 업로드된 파일에 직접 접근 가능하다면 웹쉘파일 업로드를 시도한다. 파일이 정상적으로 업로드되면 업로드된 웹쉘을 호출하여 실행한다.
웹쉘이 정상적으로 실행되면 시스템 명령어를 입력하여 정상적으로 작동되는지 확인한다.

 

 

2. 업로드되는 파일의 개수나 크기에 대한 제약이 있는지 체크

 

>> 큰 파일 업로드 시도

 

 

3. 업로드 파일명과 같은 이름으로 파일이 저장되는지 체크

1번에서 실행한 lion.png와 다르게 1717311696730lion.png로 저정됨을 확인

 

방어기법

 

BoardController.java

	@RequestMapping(value="/write.do", method=RequestMethod.POST)
	public String boardWriteProc(@ModelAttribute("BoardModel") BoardModel boardModel, MultipartHttpServletRequest request, HttpSession session){
		
		// 외부에서 직접 접근 가능한 경로에 파일이 업로드되도록 설정됨
		String uploadPath = session.getServletContext().getRealPath("/")+"files/";
		System.out.println("uploadPath: "+uploadPath);
 
		MultipartFile file = request.getFile("file"); 
		
        
		// 업르도되는 파일에 대해 파일이름이 비어 있지 않으면 업로드를 허용하도록 설정됨
		if ( file != null && ! "".equals(file.getOriginalFilename())) {
			String fileName = file.getOriginalFilename();
			File uploadFile = new File(uploadPath+ fileName);
			
			// 동일 파일명이 있으면 시간정보를 붙여서 파일명을 생성해서 사용
			if(uploadFile.exists()){
				fileName = new Date().getTime() + fileName;
				uploadFile = new File(uploadPath + fileName);
			}

			try {
				// 파일 저장
				file.transferTo(uploadFile);
			} catch (Exception e) {
				System.out.println("upload error");
			}
			// 저장되는 파일명 DB에 저장
			boardModel.setFileName(fileName);
		}

		String content =  boardModel.getContent().replaceAll("\r\n", "<br />");		
		boardModel.setContent(content);
		// DB에 내용저장
		service.writeArticle(boardModel);		
		
		return "redirect:list.do";
	}
현재 코드는 업로드 기능에서 가질 수 있는 다양한 취약점을 가지고 있는 코드이다.

 

 

첫 번째로 파일의 저장되는 경로가 외부에서 직접적으로 접근 가능한 경로를 사용하면 업로드된 웹쉘을 공격자가 사용할 수 있다.

 

String uploadPath = session.getServletContext().getRealPath("/")+"WEB-INF/files/";

or

String uploadPath = "/files";

// session.getServletContext().getRealPath("/")는 애플리케이션 루트의 실제 경로를 반환함
이 코드를 통해 외부에서 직접 접근 불가능한 경로에 파일이 업로드되도록 설정하여 방어한다.

 

 

두 번째로 업로드 되는 파일에 대해 파일의 타입을 제한하지 않아 웹쉘과 같은 위험한 파일이 업로드될 수 있다.
if ( file.getContentType().contains("image") && fileName.toLowerCase().endsWith(".jpg") || fileName.toLowerCase().endsWith(".png")){

// 업로드 처리 기능 구현
}
이 코드를 통해 파일명을 toLowerCase()와 같은 메서드를 사용해 소문자로 변환한 후, 지정된 확장자 명으로 끝나는지 endWith() 메서드를 이용해 검사하여 방어한다.

 

 

세 번째로 업로드된 파일명을 이용하여 파일을 저장하는 경우 공격자는 쉽게 자신이 업로드한 파일을 찾아서 사용할 수 있다.
String savedFileName = UUID.randomUUID().toString();
File uploadFile = new File(uploadPath+ savedFileName);
랜덤하게 생성된 문자열을 파일명으로 이용하여 공격자가 업로드한 파일을 찾아서 사용하지 못하도록 방어한다.

 

if ( uploadFile.exists() ){
savedFileName = savedFileName + UUID.randomUUID().toString();
uploadFile = new File(uploadPath+ savedFileName);
}
만약 생성된 파일명이 이미 사용 중이라면 기존 파일이 손상되거나 새로운 파일이 저장되지 못하기 때문에 파일명이 중복되지 않도록 처리하여 방어한다.

 

 

네 번째로 업로드되는 파일의 개수나 크기에 대한 제약이 없는 경우 공격자는 다량의 큰 파일을 업로드하도록 요청하여 서버에 부하가 발생하도록 공격할 수 있다.
MultipartFile file = request.getFile("file");
		if ( file != null ) {
	       if ( file.getSize() > 10240000) {
	    	return "redirect:write.do";
	    }
파일 크기가 10MB보다 작은 경우만 파일 업로드를 허용하여 방어한다.

 

 

수정 BoardController.java

        @RequestMapping(value="/write.do", method=RequestMethod.POST)
	public String boardWriteProc(@ModelAttribute("BoardModel") BoardModel boardModel, MultipartHttpServletRequest request, HttpSession session){

		
		String uploadPath = session.getServletContext().getRealPath("/")+"WEB-INF/files/";
		System.out.println("uploadPath: "+uploadPath);
		MultipartFile file = request.getFile("file");
		if ( file != null ) {
	       if ( file.getSize() > 10240000) {
	    	return "redirect:write.do";
	    }
	    
		String fileName = file.getOriginalFilename();		
		if ( fileName != null && ! "".equals(fileName) ) {
				if ( fileName.toLowerCase().endsWith(".jpg")  || 
						fileName.toLowerCase().endsWith(".png")) {
			String savedFileName=
					 UUID.randomUUID().toString()+new Date().getTime();
			File uploadFile = new File(uploadPath+ savedFileName);
	    	try {
	    		System.out.println("write do");
		    	file.transferTo(uploadFile);
		    } catch (Exception e) {
			   System.out.println("파일 저장 오류 발생");
		    }
		boardModel.setFileName(fileName);
		boardModel.setSavedFileName(savedFileName);
		}
		}
		}
		//
		// new line code change to <br /> tag	
		String content =  boardModel.getContent().replaceAll("\r\n", "<br />");		
		boardModel.setContent(content);
		//
		service.writeArticle(boardModel);		
		
		return "redirect:list.do";
	}
    
    
        @RequestMapping("/get_image.do")
	public void getImage(HttpServletRequest request, HttpSession session, HttpServletResponse response) {
		int idx=TestUtil.getInt(request.getParameter("idx"));
		
		if(session.getAttribute("idx")==null || (Integer)session.getAttribute("idx")!=idx) {
			return;
		}
		
		BoardModel board = service.getOneArticle(idx);
		
		String filePath = session.getServletContext().getRealPath("/")+"WEB-INF/files/"+board.getSavedFileName();
		
		BufferedOutputStream out =null;
		InputStream in =null;
		
		try {
			response.setContentType("image/jpeg");
			response.setHeader("Content-Dispostion", "Inline;filename="+board.getFileName());
			File file = new File(filePath);
			in = new FileInputStream(file);
			out =new BufferedOutputStream(response.getOutputStream());
			int len;
			byte[] buf = new byte[1024];
			while ((len=in.read(buf))>0) {
				out.write(buf,0,len);
			 }
		}catch(Exception e)
			{
			 System.out.println(e);
			}finally {
				if(out!=null)try {out.close();}catch(Exception e) {}
				if(in !=null)try {in.close();}catch(Exception e) {}
		}
	}

 

수정 view.jsp

<c:if test="${board.fileName != null }">
		<tr>
			<td colspan="4" align="left">
			<span class="date">첨부파일:&nbsp;
			<a href="get_image.do?idx=${board.idx}" 
			target="_blank">${board.fileName}</a></span></td>
		</tr>
		</c:if>

 

 

방어결과

 

>> 큰 파일 업로드

게시글은 작성되지만 파일이 업로드 되지 않는다.

 

 

>> png파일 업로드

 

새 브라우저로 http://localhost:8080/openeg/board/get_image.do?idx=25 접속시 로그인 하라는 창이 뜬다.

 

 

 

>> 같은 png파일 재업로드

같은 lion.png 파일이지만 http://localhost:8080/openeg/board/get_image.do?idx=25  idx값이 1증가한것을 확인할 수 있다.

 

 

참고: 해킹 방어를 위한 JAVA 시큐어코딩(실무에 바로 적용하는)(개정판 4판) 김영숙

댓글