Java 원본 체인을 통한 역직렬화 악용 기법

자바 시리얼라이제이션 취약점에서 외부 라이브러리 없이도 악성 코드를 실행할 수 있는 원본 체인을 활용하는 방법에 대해 살펴봅니다. 이 기법은 특정 버전의 JDK 7u21에서만 작동하며, 내장 클래스만을 사용하여 명령어 실행을 유도합니다.

역직렬화 공격의 핵심 목적은 원하는 동작을 수행하는 것입니다. 일반적으로는 명령어 실행이 목적이지만, 파일 읽기, 네트워크 연결 등 다양한 행동도 가능합니다. 하지만 대부분의 사례에서는 명령어 실행으로 최종 목표를 달성합니다. 이 과정에서 가장 중요한 것은 특정 메서드를 트리거해 원하는 동작을 유도하는 것입니다.

핵심 원리: AnnotationInvocationHandler의 equalsImpl 메서드

이 체인의 중심은 sun.reflect.annotation.AnnotationInvocationHandler 클래스의 equalsImpl 메서드입니다. 해당 메서드는 다음과 같은 조건에서 반복문을 통해 인터페이스의 메서드를 호출하게 됩니다:

  • this.type.isInstance(var1)false일 경우 (즉, 전달된 객체가 해당 인터페이스의 인스턴스가 아님)
  • var1Templates.class와 같은 AnnotationInvocationHandler를 구현하지 않은 인터페이스일 때

이 경우, getDeclaredMethods()로 인터페이스의 모든 메서드를 가져오며, 여기서 newTransformer()getOutputProperties()가 포함됩니다. 이 두 메서드는 TemplatesImpl에서 오버라이드되어 있으며, 악성 바이트코드를 로드하는 데 사용됩니다.

동적 프록시를 통한 메서드 전달

이 메서드를 트리거하려면 equalsImpl가 호출되도록 해야 합니다. 이를 위해 AnnotationInvocationHandler.invoke 메서드를 이용합니다. 이 메서드는 프록시 객체가 호출되는 모든 메서드를 포워딩합니다.

다음과 같이 동적 프록시를 생성하고, Map 인터페이스를 구현하도록 설정하면, equals() 호출 시 자동으로 invokeequalsImpl 흐름이 발생합니다:

Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

InvocationHandler handler = (InvocationHandler) constructor.newInstance(Templates.class, map);
Map proxy = (Map) Proxy.newProxyInstance(jdk7u21.class.getClassLoader(), new Class[]{Map.class}, handler);

hashcode 충돌을 통한 equals 자동 트리거

직렬화 과정에서 equals()가 자동으로 호출되기 위해서는 컬렉션(예: HashSet, LinkedHashSet)의 중복 검사 단계에서 발생해야 합니다. 이때 map.put()에서 equals()가 호출됩니다.

이 조건을 만족하기 위해선 두 객체의 hashCode() 값이 일치해야 하며, 이는 hash collision을 유도해야 합니다.

AnnotationInvocationHandler.hashCodeImpl() 메서드는 내부 맵의 키와 값을 기반으로 해시값을 계산합니다. 이 메서드는 다음과 같습니다:

private int hashCodeImpl() {
    int result = 0;
    for (Map.Entry<?> entry : memberValues.entrySet()) {
        result += 127 * entry.getKey().hashCode() ^ memberValueHashCode(entry.getValue());
    }
    return result;
}

이 메서드에서 키의 해시값이 0이 되도록 하면, resultmemberValueHashCode(value)에 의해 결정됩니다. 따라서 valueTemplatesImpl 인스턴스라면, 전체 해시값은 TemplatesImpl.hashCode()와 같아집니다.

결국, key의 해시값이 0인 문자열을 찾아야 합니다. 이를 위해 간단한 브루트포스를 수행하면, f5a5a608이라는 문자열이 해시값이 0임을 확인할 수 있습니다.

완전한 패로드 구성

최종 패로드는 다음과 같습니다:

  1. TemplatesImpl 인스턴스를 생성하고, 악성 바이트코드를 설정합니다.
  2. HashMapf5a5a608을 키로, 임의의 값으로 설정합니다.
  3. AnnotationInvocationHandler를 생성하여 Templates.class와 위 맵을 인자로 전달합니다.
  4. 동적 프록시를 생성하여 Map 인터페이스를 구현합니다.
  5. LinkedHashSettemplatesproxy를 추가합니다.
  6. map.put(f5a5a608, templates)를 호출하여 해시값을 일치시킵니다.
  7. 최종적으로 ObjectOutputStream.writeObject(set)를 통해 직렬화하고, ObjectInputStream.readObject()로 역직렬화하면 equalsImpl가 트리거되어 악성 코드가 실행됩니다.

이 과정에서 HashSet의 중복 검사가 proxy.equals(templates)를 자동으로 호출하며, 그 결과 AnnotationInvocationHandlerinvokeequalsImpl 흐름이 시작됩니다. 이후 TemplatesImpl.newTransformer()가 호출되어 악성 바이트코드가 로드되고 실행됩니다.

결론적으로, 이 체인은 내장 클래스만을 사용해 역직렬화 공격을 수행하는 대표적인 예이며, 특히 외부 의존성이 없는 환경에서 매우 유용합니다. 다만, 해당 기술은 고유한 조건(특정 JDK 버전, 특수한 해시 충돌)에 의존하므로 실제 환경 적용 시 반드시 환경을 검증해야 합니다.

태그: JDK7u21 Java serialization AnnotationInvocationHandler TemplatesImpl Dynamic proxy

5월 23일 19:57에 게시됨