URL: prefix를 이용하여 Deny-list 기반 Protocol 검증 우회하기

phithon_xg가 재미있는 트릭을 트윗에 공개했는데, 실제로 분석애서 유용하게 쓰일 수 있어 간단하게 정리해서 글로 공유드려봅니다.

Bypass protocol check in Java

아래 url: 접두사가 있는 URL들은 Java URL에서 각각 http://, file:// 과 동일하게 동작합니다.

url:http://127.0.0.1:8080
url:file:///etc/passwd

그래서 만약에 deny-list 기반으로 프로토콜을 검증하고 있는 경우, 이러한 url 접두사를 통해 우회할 수 있는 포인트가 됩니다.

// check() 함수가 file://로 시작하는 url을 차단하는 함수라고 가정하고, 
// 만약 inputURL에 url:file:///sdcard/blahblah... 가 들어왔다면 ..
String checkedURL = check(inputURL);

// file://로 시작하지 않기 때문에 통과되고 URL Class로 넘어가서 생성자에 의해 파싱되면
URL url = new URL(checkedURL);
// checkedURL에는 file:///sdcard/blahblah... 가 남게됩니다.

Example

실제로 되는지 테스트를 위해 Java 코드를 작성해봅시다. 단순하게 URL: prefix가 있는 URL을 읽어 출력하는 코드입니다. 만약 Exception이 걸리면 StackTrace가 찍히겠죠.

package main;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;

public class main {
    public static void main(String[] args) {
        try {
            URL url1 = new URL("url:https://www.hahwul.com");
            System.out.println("URL1: " + url1.toString());
        } catch (MalformedURLException mue) {
            mue.printStackTrace(System.err);
        }
    }
}

컴파일 후 실행해보면 prefix는 처리되지 않고 URL만 남는 것을 확인할 수 있습니다 👍

javac main/main.java
java main.main
URL1: https://www.hahwul.com

URL: Prefix

RFC3986에는 이런 부분이 있습니다.

The prefix “URL:” (with or without a trailing space) was formerly recommended as a way to help distinguish a URI from other bracketed designators, though it is not commonly used in practice and is no longer recommended.

URL: prefix는 일반적으로 사용되진 않지만 URI를 괄호로 묶은 다른 URI와 구별하는 데 도움이 될 수 있다는 이야기인데요. 이 prefix에 대해 좀 더 알아보기 위해 RFC1738을 보면, 이렇게 이야기되고 있습니다.

prefix “URL:”, be used to delimit the boundaries of the URL. This wrapper does not form part of the URL and should not be used in contexts in which delimiters are already specified.

즉 URL의 경계를 구분하는데 사용되는 prefix고 URL에 포함되는 wrapper는 아니라고 합니다. 즉 유효한 URL은 아니라는 거죠.

Restrictions

curl로 URL: prefix를 포함한 요청을 발생시켜보면 아래와 같이 URL 포맷 에러가 나타납니다.

curl -i -k url:https://www.hahwul.com
curl: (3) URL using bad/illegal format or missing URL

이는 curl의 url library 또는 코드는 이 prefix를 처리하지 않고 그대로 URL이라고 판단했기 때문인데요. 위에 문제가 됬던 Java URL의 경우 prefix가 포함된 URL을 받으면 prefix를 제거하고 체크하기 때문에 단순히 사용자로부터 들어온 URL의 프로토콜을 Deny-list로 검사하는 경우 이를 우회할 수 있게 된겁니다. 결국 사용하는 언어나 라이브러리, 프레임워크에서 URL을 어떻게 처리하는가가 중요한 제약이 되곘네요.

Attack vector

우선은 확인된건 Java의 URL Class라 Java기반 웹이랑 Android 앱 중 URL을 사용하는 부분은 전반적으로 영향받을 것 같습니다. 아무래도 꼼꼼한 체크가 필요할 것 같구요. 관련있는 공격 기법들은 아래 정도일 것 같네요.

참고로 Android의 경우 Golden technic이라고 해서 URL에 대한 여러가지 우회 방법들이 있습니다. 혹시 들어보신 적이 없다면 “Bypass host validation Technique in Android(Common+Golden+Mythink)” 글을 봐주셔도 될 것 같습니다.

Mitigation

모든 제약은 allow-list 기반의 제약이 훨씬 강합니다. 다만 상황에 따라서 deny-list를 사용해야할 수 밖에 없는 경우가 있는데요. 일단 Java application에서 protocol 체크를 하고 있다면 prefix 또한 체크하여 검증하는 것이 좋을 것 같습니다.

References

  • https://twitter.com/phithon_xg/status/1498153253350961152
  • https://datatracker.ietf.org/doc/html/rfc3986#appendix-C
  • https://datatracker.ietf.org/doc/html/rfc1738#section-7