ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Study] CODEGATE2022 babyfirst Writeup
    CTF/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외 실제 소스코드로써 어떻게 정의되어있는지 확인해보았다.

    https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.base/share/classes/java/net/URL.java#L575

     

    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

    댓글

Designed by Tistory.