-
[Study] CODEGATE2022 babyfirst WriteupCTF/CODEGATE2022 2022. 3. 1. 15:00
주어진 페이지에 접근해보자. 사용자명을 입력하면 메모를 남길 수 있는 서비스가 제공되고 있다.
Dokerfile을 보면 플래그는 /flag 에 저장되어있음을 알 수 있다.
### Dockerfile FROM ubuntu:20.04 RUN apt-get -y update && apt-get -y install software-properties-common RUN apt-get install -y openjdk-11-jdk RUN apt-get -y install wget RUN mkdir /usr/local/tomcat RUN wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.75/bin/apache-tomcat-8.5.75.tar.gz -O /tmp/tomcat.tar.gz RUN cd /tmp && tar xvfz tomcat.tar.gz RUN cp -Rv /tmp/apache-tomcat-8.5.75/* /usr/local/tomcat/ RUN rm -rf /tmp/* && rm -rf /usr/local/tomcat/webapps/ COPY src/ROOT/ /usr/local/tomcat/webapps/ROOT/ COPY flag /flag COPY start.sh /start.sh RUN chmod +x /start.sh CMD ["/start.sh"]
핵심인 MemoServlet.class 파일을 디컴파일 해보자.
package controller; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.URL; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Base64; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class MemoServlet extends HttpServlet { private Connection conn = null; private void alert(HttpServletRequest req, HttpServletResponse res, String msg, String back) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter pw = res.getWriter(); pw.println("<script>"); pw.println("alert('" + msg + "')"); if (back != null && back.length() > 0) { pw.println(";location.href='" + back + "';"); } pw.println("</script>"); pw.close(); } private boolean isLogin(HttpServletRequest req) { if (req.getSession().getAttribute("name") == null) { return false; } return true; } private String lookupPage(String uri) { String[] array = uri.split("\\/"); if (array.length != 3) { return "error"; } return array[2].trim(); } private void doLogin(HttpServletRequest req) throws ServletException, IOException { String name = req.getParameter("name"); if (name == null || name.length() <= 0 || name.length() > 100) { name = "noname"; } req.getSession().setAttribute("name", name); } private void doWrite(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException, SQLException { String name = (String) req.getSession().getAttribute("name"); String memo = req.getParameter("memo"); if (memo == null || memo.length() <= 0) { memo = "no memo"; } if (memo.length() > 2000) { memo = "too long"; } PreparedStatement pstmt = null; try { PreparedStatement pstmt2 = this.conn.prepareStatement("INSERT INTO memos (`name`, `memo`) VALUES (?,?)"); pstmt2.setString(1, name); pstmt2.setString(2, memo); if (pstmt2.executeUpdate() > 0) { alert(req, res, "write", "/memo/list"); } else { alert(req, res, "error", "/memo/list"); } if (pstmt2 != null) { pstmt2.close(); } alert(req, res, "error", "/memo/list"); } catch (Exception e) { if (0 != 0) { pstmt.close(); } } catch (Throwable th) { if (0 != 0) { pstmt.close(); } throw th; } } private HashMap<Integer, String> getList(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException, SQLException { String name = (String) req.getSession().getAttribute("name"); PreparedStatement pstmt = null; try { PreparedStatement pstmt2 = this.conn.prepareStatement("SELECT * FROM memos WHERE `name`=? ORDER BY idx DESC"); pstmt2.setString(1, name); HashMap<Integer, String> result = new HashMap<>(); ResultSet rs = pstmt2.executeQuery(); while (rs.next()) { result.put(Integer.valueOf(rs.getInt(1)), rs.getString(3)); } if (pstmt2 == null) { return result; } pstmt2.close(); return result; } catch (Exception e) { System.out.println(e.getMessage()); if (0 != 0) { pstmt.close(); } return null; } catch (Throwable th) { if (0 != 0) { pstmt.close(); } throw th; } } private static String lookupImg(String memo) { Matcher matcher = Pattern.compile("(\\[[^\\]]+\\])").matcher(memo); if (!matcher.find()) { return ""; } String img = matcher.group(); String tmp = img.substring(1, img.length() - 1).trim().toLowerCase(); Matcher matcher2 = Pattern.compile("^[a-z]+:").matcher(tmp); if (!matcher2.find() || matcher2.group().startsWith("file")) { return ""; } String urlContent = ""; try { BufferedReader in = new BufferedReader(new InputStreamReader(new URL(tmp).openStream())); while (true) { String inputLine = in.readLine(); if (inputLine != null) { urlContent = urlContent + inputLine + "\n"; } else { in.close(); try { return memo.replace(img, "<img src='data:image/jpeg;charset=utf-8;base64," + new String(Base64.getEncoder().encode(urlContent.getBytes("utf-8"))) + "'><br/>"); } catch (Exception e) { return ""; } } } } catch (Exception e2) { return ""; } } private String getMemo(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException, SQLException { String name = (String) req.getSession().getAttribute("name"); try { int idx = Integer.parseInt(req.getParameter("idx")); PreparedStatement pstmt = null; try { PreparedStatement pstmt2 = this.conn.prepareStatement("SELECT * FROM memos WHERE name=? AND idx=?"); pstmt2.setString(1, name); pstmt2.setInt(2, idx); ResultSet rs = pstmt2.executeQuery(); if (rs.next()) { String memo = rs.getString(3); String tmp = lookupImg(memo); ##### ......................... (1) if (!"".equals(tmp)) { if (pstmt2 != null) { pstmt2.close(); } return tmp; } else if (pstmt2 == null) { return memo; } else { pstmt2.close(); return memo; } } else if (pstmt2 == null) { return ""; } else { pstmt2.close(); return ""; } } catch (Exception e) { alert(req, res, "error", "/memo/list"); if (0 != 0) { pstmt.close(); } alert(req, res, "error", "/memo/list"); return ""; } catch (Throwable th) { if (0 != 0) { pstmt.close(); } throw th; } } catch (Exception e2) { return ""; } } public void init(ServletConfig config) { try { String dbUser = config.getInitParameter("dbUser"); String dbPass = config.getInitParameter("dbPass"); Class.forName("com.mysql.cj.jdbc.Driver"); this.conn = DriverManager.getConnection("jdbc:mysql://mysql:3306/memo?serverTimezone=UTC", dbUser, dbPass); } catch (Exception e) { e.printStackTrace(); } } public void destroy() { try { this.conn.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { if (!isLogin(req)) { alert(req, res, "login first", "/"); return; } String page = lookupPage(req.getRequestURI()); if ("list".equals(page)) { try { req.setAttribute("list", getList(req, res)); } catch (SQLException throwables) { throwables.printStackTrace(); } } else if ("read".equals(page)) { try { req.setAttribute("memo", getMemo(req, res)); } catch (SQLException throwables2) { System.out.println(throwables2.getMessage()); } } req.getRequestDispatcher("/WEB-INF/jsp/" + page + ".jsp").forward(req, res); } public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String page = lookupPage(req.getRequestURI()); if (page.equals("login") || isLogin(req)) { char c = 65535; switch (page.hashCode()) { case 103149417: if (page.equals("login")) { c = 0; break; } break; case 113399775: if (page.equals("write")) { c = 1; break; } break; } switch (c) { case 0: doLogin(req); alert(req, res, "welcome", "/memo/list"); return; case 1: try { doWrite(req, res); return; } catch (SQLException throwables) { alert(req, res, "error", "/memo/list"); System.out.println(throwables.getMessage()); return; } default: alert(req, res, "error", "/memo/list"); return; } } else { alert(req, res, "login first", "/"); } } }
http://3.39.72.134/memo/write 에 접근하여 글을 작성 할 수 있고,
http://3.39.72.134/memo/read?idx=14446 와 같이 작성한 글을 읽을 수 있다.
그 외 소스를 분석하다가 처음에는 SQL 관련 로직에서 취약점이 있을까 살펴봤는데, prepareStatement 처리가 잘 되어있었다.
어쩌면 취약점이 존재할수있는 거의 유일한 로직은 (1)의 memo를 읽는 과정에서의 lookupImg 로직이었다. 해당 로직을 자세하게 분석해보자.
private static String lookupImg(String memo) { Matcher matcher = Pattern.compile("(\\[[^\\]]+\\])").matcher(memo); ### ... (2) if (!matcher.find()) { return ""; } String img = matcher.group(); String tmp = img.substring(1, img.length() - 1).trim().toLowerCase(); Matcher matcher2 = Pattern.compile("^[a-z]+:").matcher(tmp); ### .......... (3) if (!matcher2.find() || matcher2.group().startsWith("file")) { return ""; } String urlContent = ""; try { BufferedReader in = new BufferedReader(new InputStreamReader(new URL(tmp).openStream())); ##### ................................................................. (4) while (true) { String inputLine = in.readLine(); if (inputLine != null) { urlContent = urlContent + inputLine + "\n"; } else { in.close(); try { return memo.replace(img, "<img src='data:image/jpeg;charset=utf-8;base64," + new String(Base64.getEncoder().encode(urlContent.getBytes("utf-8"))) + "'><br/>"); ##### ..................................................... (5) } catch (Exception e) { return ""; } } } } catch (Exception e2) { return ""; } }
(2)와 같이 memo의 컨텐츠 [ ] 안에 내용을 tmp 로 읽는다.
그 tmp의 내용을 (3)과 같이 URL 의 protocol 부분 처럼 알파벳:(ex http:) 형식이되, file protocol이 아니면,
(4)처럼 해당 URL을 openStream() 으로 접근하여 결과 값을 (5)와 같이 base64화 하여 이미지로 출력해준다.
로컬에서 테스트 환경을 구축하여 테스트해보니 실제로 (3)이하의 if문이 존재하지 않다면 로컬 파일을 읽을 수 있음을 확인했다.
하지만, file protocol을 활용 할 수 없기 때문에 처음에는 URL.openStream() 이 작동하는데 어떤 프로토콜들을 사용할 수 있는지 조사해보았다.
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URL.html
URL (Java SE 11 & JDK 11 )
Creates a URL object from the specified protocol, host, port number, and file. host can be expressed as a host name or a literal IP address. If IPv6 literal address is used, it should be enclosed in square brackets ('[' and ']'), as specified by RFC 2732;
docs.oracle.com
다음과 같은 프토콜을 사용 할 수 있다고 한다. 위에 언급된 프로토콜 외에 ftp 프로토콜도 사용할 수는 있음을 확인했다.
(jdk 7 에는 ftp프로토콜 언급되어있음 + 실제 테스트)
하지만 file 프로토콜을 제외하고는 로컬 파일에 접근할수 있는 방법을 찾지 못했다.
그래서 다른 방법을 찾기 위해 doc외 실제 소스코드로써 어떻게 정의되어있는지 확인해보았다.
GitHub - AdoptOpenJDK/openjdk-jdk11: Mirror of the jdk/jdk11 Mercurial forest at OpenJDK
Mirror of the jdk/jdk11 Mercurial forest at OpenJDK - GitHub - AdoptOpenJDK/openjdk-jdk11: Mirror of the jdk/jdk11 Mercurial forest at OpenJDK
github.com
... public URL(URL context, String spec, URLStreamHandler handler) throws MalformedURLException { String original = spec; int i, limit, c; int start = 0; String newProtocol = null; boolean aRef=false; boolean isRelative = false; // Check for permission to specify a handler if (handler != null) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkSpecifyHandler(sm); } } try { limit = spec.length(); while ((limit > 0) && (spec.charAt(limit - 1) <= ' ')) { limit--; //eliminate trailing whitespace } while ((start < limit) && (spec.charAt(start) <= ' ')) { start++; // eliminate leading whitespace } if (spec.regionMatches(true, start, "url:", 0, 4)) { ##### ........ (6) start += 4; } ...
575 Line에 (6) 부분을 보면 url: 로 시작 할 시, 그 이후 값들부터 인식하도록 하는 로직이 있음을 확인했고,
이를 활용하여 아래와 같이 검증 로직을 우회 할 수 있었다.
[url:file:///flag]
FLAG = codegate2022{8953bf834fdde34ae51937975c78a895863de1e1}
반응형'CTF > CODEGATE2022' 카테고리의 다른 글
[Study] CODEGATE2022 myblog Writeup (0) 2022.03.01 [Clear] CODEGATE2022 superbee Writeup (0) 2022.03.01 [Clear] CODEGATE2022 CAFE Writeup (0) 2022.03.01