2017. 5. 7. 18:29 :: 취약점

안녕하세요. Message 입니다.

오늘 포스팅 주제는 Java와 관련된 CVE에서 자주 등장하는 ysoserial 도구에 대한 내용입니다.

CVE-2015-4852 취약점 포스팅에 포함시키려 하였으나, 생각보다 분량이 많아져서 따로 작성하게 되었네요.

현재 이슈되고 있는 내용은 아니지만, 제가 모르는 내용이 많아서 배울것이 많은 주제였습니다. 

주요 골자는 아래 웹사이트에 있는 내용을 참고하였고, 필요한 개념과 디버깅 부분을 추가하여 포스팅을 진행하겠습니다.

 

OSINT :: Understanding the ysoserial's CommonsCollections1 exploit

http://opensources.info/understanding-the-ysoserials-commonscollections1-exploit/

 

 

 

  0x00  Ready

 

 1. Y SO SERIAL? - ysoserial.jar

 ysoserial은 해외 연구원들이 프레임워크상에서의 RCE 발생과 관련된 연구 결과를 입증하기 위해 제작한 개념증명 도구입니다.

 이 툴을 이용하면 Java 프로그램에서 임의의 사용자 명령을 실행할 수 있게 해주는 페이로드를 생성할 수 있습니다.

 Java의 직렬화(Serialization)/역직렬화(Deserialization)에서 발생하는 취약점을 다루다 보니 이름에 serial이 붙은걸로 추정되지만

 Github 대문에 조커와 비슷한 이미지가 있는걸로 보아 우리가 생각하는그 대사를 노린게 분명해보입니다 ㅎㅎ

 URL : https://github.com/frohoff/ysoserial

 

 

 

 2. Gadget Chain

 ysoserial은 직렬화/역직열화 과정에서 Commons-Collections 라이브러리의 InvokerTransformer를 악용합니다.

 만약 서버의 Java Class Path에 InvokerTransformer가 필수 가젯으로 포함된 응용 프로그램이 운영되고 있을 경우 

 공격자는 ysoserial을 이용하여 얻은 가젯체인(gadget chain)을 직렬화하여 서버로 전달할 방법을 고안할 것입니다.

 (여기서 가젯체인이라는 용어가 생소하지만, OSINT에서는 Method Sequence로 설명하고 있습니다.

 시퀀스의 각 메소드를 Gadget이라 하고, 사용자의 명령(ls, wget 등..)이나 InvokerTranformer등을 하나의 Gadget으로 봅니다.)

 데이터가 서버로 성공적으로 전달되어 역직렬화 되는 과정이 진행된다면, 결과적으로 Runtime.exec() 메소드를 호출하게 됩니다.

 아래 그림은 ysoserial의 gadget chain을 표현한 그림입니다. 지금은 이해가 안되더라도 한번 쓰-윽 살펴보고 넘어갑니다.

 

 

 

 3. 필요 배경지식

 ysoserial 도구는 아래의 클래스들과 디자인 패턴등을 이용하여 페이로드를 생성합니다.

 만약 이들에 대한 이해도가 부족하다면 ysoserial 페이로드가 실행되는 매커니즘을 완전히 이해하기 힘들 수 있습니다.

 1) JDK : ① AnnotationInvocationHandler  ② Proxy  ③ Map  ④ Override  ⑤ InvocationHandler  ⑥ Runtime

 2) Commons Collections : ① LazyMap  ② Transformer  ③ ChainedTransformer  ④ InvokerTransformer

 3) ETC.. : ① Java Serialization and Deserialization  ② ObjectInputStream - readObject()

 

 

 

   0x01  Payload only Execute

 

 1. RCE 예제 실행

 OSINT의 첫번째 예제를 실행시켜보겠습니다.

 해당 코드는 CommonsCollection1 옵션으로 생성한 페이로드의 핵심 부분이며, 여기에 20~30%만 덧붙이면 온전한 페이로드가 됩니다.

 CommonsCollection1 옵션은 ysoserial을 실행시키면 가장 앞에 있는 페이로드 생성 옵션입니다. (아래 그림은 ver.0.0.2 기준)

 해당 옵션은 CVE-2015-4852 취약점에서 사용되는 페이로드이기도 합니다.

 

 

 Commons-Collection 3.2.1 버전을 다운받아 라이브러리에 추가시킨 뒤 이클립스로 실행시켜보았습니다.

 아직 동작원리는 파악되지 않지만, String배열에 할당해준 사용자 명령어("calc.exe")가 정상 실행되는군요.

 다시 한번 상기해보면 ysoserial 도구는 Runtime.exec() 메소드 호출을 통해 InvokerTransformer를 악용하여 RCE를 발생시키는 도구이므로,

 아래의 코드 역시 결국 Runtime.exec() 메소드를 호출하는 구조일겁니다.

 

 

 위의 코드를 이해하려면 getMethod() + invoke() 메소드가 어떤 기능을 수행하는지 알아야합니다.

 아래 코드는 String.class에서 getMoethod 메소드로 length() 메소드 객체를 얻어와 invoke()로 실행시키는 간단한 예제입니다.

 invoke() 메소드는 클래스의 정보를 얻어와 동적으로 메소드를 실행시킬 수 있는 자바의 리플렉션(Reflection) 기능의 핵심입니다.

 리플렉션에 대한 자세한 설명은 아래쪽에서 하겠습니다. 일단 예제의 실행 결과는 length 메소드 실행 결과와 동일한 "9" 입니다.

 

 

 

 2. 예제 분석

 아래 사진은 위의 소스코드를 좀더 가독성 있게 정리한 결과입니다. 조금더 보기 수월해졌나요?

 일단 Transformer형 배열에 ConstantTransformer, InvokerTransformer를 할당하고 ChainedTransformer 객체에 인자로 넣어주고 있습니다.

 ConstantTransformer는 말그대로 상수 객체를 반환하며, InvokerTransformer는 invoke() 메소드를 이용한 결과값을 객체로 반환합니다.

 어떻게 invoke() 메소드를 이용하여 객체를 반환하는지는 아래에서 상세하게 보겠습니다.

 

 

 이어지는 코드에서는 LazyMap의 decorate 메소드를 호출하여 ChainedTransformer를 인자로 넣어준 뒤 LazyMap.get("문자열")을 호출합니다.

 LazyMap의 API 설명을 살펴보면, Map의 객체를 decorator 패턴으로 확장/생성할 수 있도록 설계된 메소드입니다.

 get 호출시 넘긴 key 값과 매칭되는 value값이 없으면 ChainedTransformer(factory)가 해당 인자를 이용하여 새로운 객체를 생성합니다.

 이때 ChainedTransformer안에 있는 Transformer들이 가지고 있는 값들(Runtime.class, getMethod, invoke...)을 이용합니다.

 

 

 실제로 get메 소드를 호출하며 넣어준 임의의 key값인 "message"는 매칭되는 value가 없으므로

 ChainedTransformer안에 들어 있는 4개의 Transformer들의 trnasform 메소드가 객체를 생성하기 위해 연쇄적으로 호출됩니다.

 아래 소스코드를 보면 map.cotainsKey(key) == false 일때 factory.transform 메소드가 실행됨을 알 수 있습니다.

 

 

 transform 함수는 메소드의 반환값을 다음 실행되는 transform의 인자로 넘기게 되는데

 Java의 decorator 디자인 패턴과 동일하게 아래와 같이 4단계로 확장되는 방식입니다.

 디자인 패턴까지 상세하게 다룰 수 없으므로, 관련된 블로그를 소개합니다 → 데코레이터 패턴 :: http://jdm.kr/blog/78

 ①  Runtime.class  // → ConstantTransformer

 ②  Runtime.class.getMethod("getRuntime", new Class[0])  // → InvokerTransformer

 ③  Runtime.class.getMethod("getRuntime", new Class[0]).invoke(new Class[]{ Object.class, Object.class},new Object[0]))

 ④  Runtime.class.getMethod("getRuntime", new Class[0]).invoke(new Class[]{ Object.class, Object.class},new Object[0])).exec("calc.exe");

 

 

 실제로 이렇게 일렬로 쭈욱 만들어진 코드를 실행해보면 계산기(calc.exe)가 진짜(?) 실행됨을 알 수 있습니다.

 여기 까지 진햄함으로서 우리는 LazyMap에 ChainedTransformer를 인자로 넣어주고 get 함수를 호출하면

 RCE가 발생할 조건이 성립됨을 알았습니다. 관건은 타겟 서버 LazyMap의 get 함수를 어떻게 호출시키느냐 일겁니다.

 그나마 희소식은 여기까지 잘 이해했다면 ysoserial gadget chain을 핵심부분을 이해했다고 볼 수 있습니다.

 

 

 

 

 

 

  0x02  Putting it all together

 

 1. CommonsCollections1 페이로드

 CommonsCollections1 옵션으로 생성한 페이로드 + 직렬화/역직렬화까지 포함한 풀버전(?)을 살펴보겠습니다.

 생각보다 길지만, 직렬화/역직렬화를 위한 파일입출력을 제외하면 위에서 분석한 예제의 내용이 상당수 포함되어 있습니다.

 

 

 

 

 2. Main

 main에서는 위에서 공부한 내용이 포함된 객체(evilObject)를 생성합니다.

 해당 객체에 대한 자세한 내용은 아래에서 다룹니다. 

 

 

 이후에 serializeToByteArray, derializeFromByteArray 메소드를 이용하여 evilObject 객체의 직렬화/역직렬화를 수행합니다.

 역직렬화를 하기 위한 deserializeFromByteArray 내부의 ObjectInputStream.readObject 메소드에서 RCE가 발생하게됩니다.

 

 

 getEvilObject의 상단 코드는 바로 위에서 본 내용입니다. 다만 RCE를 발생시키는 lazymap.get() 메소드가 없습니다.

 표면적으로는 없어졌지만, 다른 메소드를 타고 타고 넘어가다보면 결국 어디선가는 나올겁니다.

 lazymap.get() 대신 새롭게 추가된 코드를 살펴보면 AnnotationInvocationHandler, Constructor, InvocationHandler, Proxy 등이 보입니다.

 라인수는 얼마 안되지만 각종 디자인 패턴과 캐스팅이 많이 포함되어 있기 때문에 처음에는 무슨 내용인지 파악하기 힘들 수 있습니다.

 

 

 

 

 3. secondInvocationHandler :: 첫번째 Handler

 

 ① 리플렉션(Java Reflection)을 이용한 객체 생성

 새롭게 추가된 코드의 첫부분은 클래스의 이름 "sun.reflect.annotation.AnnotationInvocationHandler"을 이용하여 

 secondInvocationHandler 객체를 생성하는 부분이 나오는데, 이를 이해하기 위해서는 리플렉션(Java Reflection) 개념에 대해 알아야합니다.

 리플렉션은 동적인 방식을 이용하여 Java의 유연성을 높이는 기능이며, 프레임워크에서 자주 사용되는 방식입니다.

 Java는 아래의 java.lang.reflect 패키지를 통해 리플렉션 기능을 제공하고 있습니다.

 

 

 리플렉션에서 제공하는 동적 기능은 클래스의 이름만으로 해당 클래스의 다양한 정보를 얻어오고,

 중간에 보았던 invoke() 메소드 등을 활용하여 상황에 따른 메소드를 호출할 수 있는 기능등을 말합니다.

 이러한 동적 기능을 이용하게 되면, 클래스의 변경이 자주 일어나는 코드에서 일일이 소스코드를 수정해서 배포하지 않아도 되며

 상황에 따라 입력으로 받은 메소드 이름을 이용하여 로직의 변화 없이 새로운 기능을 제공해줄 수도 있습니다.

 보안과는 별개로 사용 방법에 따라 매우 편리한 기능이라 할 수 있습니다.

 

 리플렉션을 이용하면 클래스의 이름/제어자/정보/부모클래스/생성자/메소드/변수의 정보를 얻을 수 있으며

 스프링 프레임워크에서 자주 사용되어 친숙한 Annotation(주석의 일종, @RequestMapping 등) 객체도 얻을 수 있습니다.

 

 

 위에서 설명한 리플렉션의 기능을 원활하게 사용하기 위해서는 클래스의 정보를 얻을 수 있는 API가 필요합니다.

 이를 위해서 자바에서는 java.lang.Class라는 클래스를 제공해주고 있으며,

 일반적으로 아래 예제와 같은 형태로 객체의 정보를 얻어냅니다. (우리가 분석할 evilObject에서도 동일한 패턴이 사용됩니다)

 코드를 살펴보면, forName() 메소드에 String 클래스의 이름을 넣어주고 Class 객체를 얻었습니다.

 이후에 getDeclaredConstructors() 메소드를 이용하여 String 클래스에 정의된 생성자들을 Constructor<?> 객체 형태로 얻습니다.

 

 

 getDeclaredConstructors() 메소드는 배열 형태로 생성자(Constructor)들을 반환해주며,

 배열 인덱스([0], [1], [2]...)를 이용하면 원하는 생성자만 얻어낼 수 있습니다. (오버로딩 때문에 생성자가 여러개일 수 있으므로)

 이때 결과값은 다양한 생성자를 고려하여 모든 타입을 수용하는 <?> 제네릭(Generics) 형태입니다.

 어떤 생성자들이 반환됐는지 출력해보면 아래와 같습니다. 개인적으로 제가 원했던 new String("문자열")의 형태는 [12]에 있었네요.

 

 

 

 ② 본문코드 분석

 이제 evilObject에서 AnnotationInvocationHandler 객체를 리플렉션으로 생성하는 부분을 살펴보겠습니다.

 Class.forName() 메소드로 AnnotationInvocationHandler 객체를 얻고, getDeclaredConstructors()[0] 메소드로 첫번째 생성자를 얻었습니다.

 이후에 newInstance를 이용하여 생성자에 lazyMap을 인자로 전달하면서 취약점을 발생시키는 객체를 생성합니다.

 lazymap을 인자로 전달하는것으로 보아 최종적으로 RCE가 발생하는 부분은 secondInvocatinHandler에서 발생할 것으로 추정됩니다.

 첫번째로 생성됨에도 불구하고 변수명이 second로 시작되는 이유는 해당 핸들러가 다른 객체의 인자로 들어감으로써

 두번째로 생성되는 invoacationHandlerToSerialize 핸들러 보다 RCE 흐름상 뒷부분에 위치하기 때문입니다.

 

 

 lazymap을 인자로 받는 AnnotationInvocationHandler 클래스의 생성자를 살펴보면 Map<String, Object> 타입의

 memberValues 파라미터를 두번째 인자로 받고 있습니다. 이것은 ysoserial에서 굳이 AnnotationInvocationHandler를 타겟으로 하여

 도구를 제작한 이유와 무관하지 않아보입니다. 아마도 LazyMap과 ChainedTransformer의 조합으로 취약점이 발생하는것을

 발견하고 Map을 인자로 받는 클래스를 찾지 않았을까요.

 

 

 

 

 4. invocationHandlerToSerialize :: 두번째 Handler

 

 ① Proxy를 이용한 객체 생성

 두번째로 생성되는 invocationHandlerToSerialize 객체 생성 코드를 분석하려면 프록시(Proxy) 디자인 패턴에 대한 이해가 필요합니다.

 일단 프록시 디자인 패턴에 대한 원리는 쉽게 설명해 놓은 글이 있어서 아래의 블로그 주소를 소개합니다.

 프록시 패턴 :: http://devbox.tistory.com/entry/DesignPattern-%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A8%ED%84%B4

 

 내용을 요약하자면, 가벼운일은 대리인 PrinterProxy에서 처리하고, 무거운일(heavyJob)은 Printer를 호출해서 처리하는 로직입니다.

 이런 디자인패턴을 구현하게 되면 핵심기능의 앞 뒤로 원하는 추가 기능을 넣을 수 있을 뿐만 아니라,

 A/B/C 3개의 클래스가 있다고 가정했을 때, 프록시에서 1번만 구현하면 되기 때문에 코드 관리가 편리해집니다.

 

 하지만 우리가 분석중인 evilObject에서는 정적 프록시 패턴이 아닌, 리플렉션의 다이나믹 프록시(Dynamic Proxy)를 사용합니다.

 사실 기존의 정적 프록시는 A/B/C 클래스가 단일 프록시로 구현되어 있을 때 새로운 D/E/F 클래스가 추가되야 한다고 가정하면 

 매번 추가되는 신규 클래스의 인터페이스를 프록시에 구현해야 하는 번거로움이 있습니다.

 또한 부가기능/접근제어와 같은 코드는 자주 활용되는 것들이 많기 때문에 유사한 중복 코드가 빈번하게 발생하기도 합니다.

 

 다이나믹 프록시는 이러한 정적 프록시의 단점을 극복하고자 리플렉션의 특징을 도입하여 설계되었습니다.

 이때 사용하는 리플렉션의 특징은 바로 위에서 학습했던 InvocationHandler의 invoke() 메소드입니다.

 프록시 객체 생성시 인자로 전달되는 InvocationHandler에 부가기능을 한번만 구현하여 중복코드 문제를 해결합니다.

 또한 A/B/C 클래스의 앞뒤로 원하는 출력이나 기능을 덧붙이는 기능도 그대로 유지할 수 있습니다.

 프록시는 클라이언트에게 요청받은 내용을 핸들러에게 넘기고, 핸들러에서는 invoke()에서 처리한 후 결과값을 반환해줍니다.

 아래에서 디버깅을 진행할 때에도 프록시를 이용한 호출이 있다면, 프록시가 가지고 있는 핸들러의 invoke() 함수를 눈여겨 봐야합니다.

 아래 그림은 InvocationHandler를 이용하여 다이나믹 프록시를 설명하는 다이어그램입니다.

 

 그림출처 : https://jsdom.wordpress.com/2011/09/05/java-reflection-in-action-study-note-5/

 

 

 예제로 우리가 분석중인 다이나믹 프록시에 인자값으로 전달되는 AnnotationInvocationHandler 클래스를 살펴봅시다.

 클래스의 기본 설명은 Annotation의 Dynamic Proxy 구현을 위한 InvocationHandler 라고 되어 있습니다.

 프록시 패턴에 대해 잘 아시는 분이라면 다이나믹 프록시에 등록하여 사용하는 핸들러임을 바로 파악하지 않았을까 싶네요.

 InvocationHandler, Serializable 인터페이스를 구현했기 때문에 invoke/readObject 메소드가 있습니다.

 

 

 프록시를 통해 toString을 실행할 경우 toStringImp() 메소드를 이용하여 annotation의 형태의 문자열을 출력합니다.

 이런식으로 invoke()를 통해 처리하지 않고, 인터페이스를 구현한 메소드는 가벼운일에 속한다고 봐야할것 같습니다.

 ex) @("Original toString()")

 

 

 사실 이부분까지 알필요는 없지만, 취약점의 핵심이 되는 AnnotationInvocationHandler 클래스는 도대체 어디에 쓰이는걸까요.

 검색을 해봐도 별다른 결과가 나오지 않아서 주로 프레임워크에서 사용되지 않을까 싶어 JDK 라이브러리에 검색을 했습니다.

 여러 결과가 나왔으나, AnnotationParser 클래스가 눈에 띕니다.

 

 

 실제로 코드를 살펴보면 annotationForMap을 호출하면서 타입과 Map을 넘겨주면 AnnotationInvocatioanHandler를 리턴해줍니다.

 이렇게 사용하면 사용자는 annotationForMap을 사용하면서 자동으로 프록시의 기능을 이용하게 될것으로 보입니다.

 

 

 실제로 활용 용도나 인자값을 고려하지 않고 실행되는것에 초점을 맞춘 예제를 작성하여 돌려보면 

 마치 프록시처럼 문자열 "@" "(" 등을 덧붙이는 부가기능이나 타입을 반환하는 메소드는 annotationForMap 본인이 처리하고 있습니다.

 

 

 

 그외의 무거운일(HeavyJob)인 equals 메소드는 invoke()를 통해 해결합니다.

 

 

 

 ② 본문코드 분석

 본론으로 돌아와서, Proxy를 어떻게 사용하고 있는지 살펴봅니다.

 newProxyInstance 메소드로 아래의 인자를 넣어주면서 다이나믹 프록시의 객체(evilProxy)를 생성하고 있습니다.

 인자는 프록시 클래스를 정의하기 위한 클래스로더, 클래스가 구현해야할 인터페이스, 실제 작업을 처리하는 핸들러입니다.

 또한 이렇게 얻은 객체(evilProxy)를 InvocationHandler 객체를 생성하면서 인자로 넣어주는것을 볼 수 있습니다.

 

 ① 첫번째 인자 :: 클래스로더(ClassLoader)

 동적으로 클래스의 정보(Class.forName), 생성자(getConstructor)를 얻거나 정의(defineClass)하는데 사용

 ② 두번째 인자 :: 프록시가 구현해야할 인터페이스 리스트

 해당 리스트를 이용하여 generateProxyClass 메소드에서 인터페이스를 구현한 .class 파일을 FOS스트림으로 생성

  

 ③ 세번째 인자 :: 프록시가 호출하는 Handler

 바로 위에서 살펴본 AnnotationInvocationHandler이므로, 어떤 기능을 수행하는지는 생략

 

 프록시의 객체(evilProxy)를 생성하면서 재밌는 점은 구현해야할 인터페이스인 두번째 인자 Map.class 입니다.

 왜하필 Map.class를 상속받았는지 의문점이 있었으나, 바로 아래에 두번째 Invocationhandler 객체를 생성할때 의문이 풀리게됩니다.

 AnnotationInvocationHandler 클래스의 생성자는 Map<String, Object> 형태의 두번째 인자를 받습니다.

 따라서 Map.class를 구현한 evilProxy 객체는 이 타이밍에서 Map.class로 업캐스팅되겠죠.

 Map 인터페이스를 구현하지 않으면 RCE를 발생시키지 못하니 구현한것인지,

 혹은 원래 이렇게 사용하는 핸들러인지는 잘 모르겠으나, 어쨋든 Map 인터페이스를 구현한 것이 이유가 있는것은 확실합니다.

 

 

 핸들러의 invoke 함수 내부를 살펴보면 RCE가 발생하는 주요 포인트인 memberValues.get(member) 코드가 존재합니다.

 여기서 memberVlues는 방금 바로 위에서 본 proxy 객체입니다. 따라서 proxy.get(member) 라고 바꿔도 무방합니다.

 또한 proxy는 Map 인터페이스를 구현하면서 Map.class의 get() 메소드가 이미 존재합니다.

 이후는 우리가 Payload only Execute에서 살펴본대로 가젯 체인이 연쇄 호출되며 사용자 명령어(calc 등..)가 실행될것입니다.

 

 

 

 5. invocationToSerialize의 구조

 디버깅을 하면서 가장 헷갈렸던 부분은 핸들러 안에 핸들러가 들어가고, 중간에 프록시까지 들어가는 복잡한 구조입니다.

 정리하지 않고 무작정 디버깅으로 넘어가면 머릿속에서 핸들러의 미로(?)에 빠질수도 있습니다.

 일단 객체 생성코드 부분을 다시 체크하고 구조를 살펴보겠습니다.

 ① secondInvocationHandler는 Lazymap을 인자로 넣어주면서 생성↓↓

  InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);

 ② invocationHandlerToSerialize는 Map 인터페이스를 구현한 Proxy를 인자로 넣어주면서 생성 ↓↓

  InvocationHandler invocationHandlerToSerialize = (InvocationHandler) constructor.newInstance(Override.class, evilProxy);

 

 이렇게 생성된 invocationToSerialize 객체의 구조를 이클립스로 살펴보면 아래와 같습니다.

 invocationHandlerToSerialize 인스턴스를 생성하면서 인자로 넣어준 ProxymemberValues로 선언되어 있고,

 Proxy 인스턴스를 생성하면서 넣어준 두번째 AIHandlerh(handler)에 선언되어 자리를 잡았습니다.

 두번째 AnnotationInvocationHandler는 인스턴스를 생성시 LazyMap을 인자로 전달했기 때문에 핸들러 변수(h)에 있습니다.

 

 

 이렇게 이해하기 어려운 구조로 객체를 생성해야 하는 이유는 사용자 명령을 서버단에서 실행하기 위해서입니다.

 간단하게 아래처럼 Serializable 인터페이스를 구현한 객체를 만들고 readObject() 내부에 사용자 명령어를 넣어주고 전달하면 끝아닌가?

 라는 생각을 할수도 있지만, 이러한 로직이 성립하려면 서버(or 응용프로그램)에서 동일한 MyObject 객체를 가지고 있어야합니다.

 (실제로 테스트를 해보면 MyObject를 찾을 수 없다면서 java.lang.ClassNotFoundException 오류가 발생할겁니다.)

 객체를 공유할 수 있는 기능이 제공되지 않는 이상 발생하기 희박한 상황이겠지요.

 

 

 하지만 ysoserial 페이로드는 Java Class Path Commons Collection 라이브러리가 포함되어 있고, 역직렬화를 수행하면

 같은 객체를 공유하지 않더라도 서버에서 실행될 수 있습니다.

 

 

 

 

  0x03  Gadget Chain Debugging

 

 이제 마지막으로 가젯체인에 있는 순서대로 RCE가 발생하는 포인트까지 디버깅을 진행하겠습니다.

 

 1. Gadget Chain :: ObjectInputStream.readObject()

 ObjectInputStream의 readObject 메소드가 호출되면 직렬화된 데이터를 읽어들이기 위해 역직렬화 과정을 거치게 됩니다.

 역직렬화가 진행되면 ObjectInputStream이 읽어들인 직렬화된 클래스 내부의 readObject를 호출하게 됩니다.

 이때 readObject 메소드 호출 역시 Reflection 기능을 이용하여 readObject를 invoke 메소드로 호출하는 로직입니다.

 만약 Serializable 인터페이스를 구현한 객체가 readObject 메소드를 가지고 있다면 해당 readObject가 호출됩니다.

 AnnotationInvocationHandler의 경우 Serialize와 Map 인터페이스를 구현했기 떄문에 readObject()를 가지고 있습니다.

 

 

 

 2. Gadget Chain :: readObject() + Map.entrySet()

 참고로 Deserialize가 진행되면서 readObject() 메소드는 second 핸들러 → toSerialize 핸들러 순으로 총 2번 호출됩니다.

 (정확한 이유는 모르겠으나 serialize 인터페이스를 구현한 클래스가 중첩되었을 경우 각각 직렬화/역직렬화 과정을 진행하나봅니다)

 second 핸들러에서는 RCE가 발생하지 않으며, 두번째로 호출되는 toSerialize 핸들러의 AnnotationInvocationHandler에서 RCE가 발생합니다.

 

 proxy를 가지고 있는 toSerialize의 역직렬화를 살펴보겠습니다. 

 AnnotationInvocationHandler는 readObject() 메소드를 실행할때 생성자를 통해 받은 Map의 entrySet() 메소드를 호출합니다.

 그리고 entrySet() 메소드를 실행하는 순간 evilProxy가 가지고 있는 InvocationHandlerinvoke() 메소드로 실행흐름이 넘어갑니다.

 쌩뚱맞게 갑자기 invoke() 메소드로 흐름이 넘어가는 부분에 대해서 나름대로 분석을 해보면..

 ① memberValues = Proxy이므로, 실행되는 메소드는 evilProxy.entrySet으로 볼 수 있음

 ② 요청(entrySet)은 프록시가 가진 핸들러(AnnotationInvocationHandler)가 처리 (프록시 다지안 패턴)

 ③ 이때 Proxy는 Map 인터페이스를 구현(Imp)하였으므로, entrySet 메소드를 호출하여도 에러가 발생하지 않음

 ④ InvocationHandler는 통칭 무거운일(Heavy Job)을 invoke() 메소드로 처리하므로 invoke() 메소드로 흐름이 넘어감

 

 

 

 3. Gadget Chain :: invoke() → Payload only Execute

 proxy를 생성할때 넣어준 AnnotationInvocationHandler의 invoke() 함수로 진입했습니다.

 디버깅을 진행하다보면 중간 부분에 memberValues.get(member) 메소드를 호출하는 부분이 나옵니다.

 

 

 이때 memberValues는 main 에서 첫번째 AnnotationInvocationHandler 객체를 생성할때 넣어준 LazyMap입니다.

 생성할때와 동일하게 factory에 ChinedTransformer를 가지고 있고, map은 HashMap입니다.

 해당 코드를 실행하게 되면 Payload only execute 챕터에서 보았던 RCE가 정상적으로 실행됩니다.

 

 

 단순히 툴을 사용하면 하나의 직렬화된 객체가 짠 하고 나오지만, 그 중간과정은 매우 복잡하네요.

 디버깅을 끝으로 Ysoserial - CommonsCollection1 Exploit 페이로드 분석을 마칩니다.

 

 

 

 

  0x04  Debugging/Error Tip

 

 1. UnsuppoertedOperationException : Serialization support for org.apache.commons..

 InvokerTransformer의 보안 때문에 오류가 난다면 Common-Collections 3.2.1 Ver 이하로 라이브러리로 바꿔주어야 합니다.

 (저의 경우에는 3.2.1 Ver을 설치했다고 착각했지만 확인해보니 3.2.2 Ver을 사용하여 오류 발생....)

 

 

 

 2. IncompleteAnnotationException : java.lang.Override msiing element entrySet

 프로젝트 설정에서 JDK 1.8 → JDK 1.7 Ver 으로 변경하여 실행 (여전히 오류는 발생하지만 RCE는 발생함)

 (미리 1.7 버전을 다운로드 받아서 이클립스 build path에 별도로 추가해주어야 탭으로 선택할 수 있습니다)

 

 

 

 

 

 

  0x05  마치며..

 

 포스팅을 진행하면서 디버깅 할일이 많아지다 보니, 마치 개발(?)을 하는듯한 느낌이 들더군요.

 물론 개념만 알면 그냥 넘어갈 수 있는 부분도 많았고, 응용이 필요하진 않았지만

 프레임워크에서 사용하는 각종 디자인 패턴과 자바의 개념들이 머리속에서 뒤엉키는 경우가 많았습니다.

 내용이 길어지고 내용들이 얽히고 섥히다 보니 오타나 흐름이 이상한 부분이 있을 수 있습니다.

 틀린 내용이나 바로 잡아야할 내용이 있을 시 댓글 남겨주시면 감사하겠습니다.

 긴 글 읽어주셔서 감사합니다.

 

 

posted By Message

Commit your way to the LORD, trust in him and he will do this. [PSALms 37:5]

redScreen.tistory.com Blog :(

 

 

':: 취약점' 카테고리의 다른 글

[CVE-2014-6332] OLE 자동화 배열 취약점 분석  (1) 2016.07.24
FCKeditor 취약점 + 간단실습  (0) 2016.04.20
posted by Red_Message