Hacking/Web

python deserialize vulnerability in pickle module

rootable 2020. 7. 26. 16:41
반응형

0. 목적

 - python의 pickle 모듈에서 발생되는 deserialize 취약점에 대해서 분석




1. 정의

1.1 pickle 모듈

python의 pickle 모듈은 객체 구조의 직렬화와 역 직렬화를 위한 바이너리 프로토콜을 구현하기 위해 사용한다. 


그렇다면 직렬화와 역 직렬화는 무엇을 의미하는 것일까? 이에 대한 것을 간단하게 표현하자면 다음과 같다.    

 - pickling ( = Serialization ) : 파이썬 객체 계층 구조 → 바이트 스트림

 - unpickling ( = Deserialization ) : 바이트 스트림 → 파이썬 객체 계층 구조

바로 파이썬 객체 계층 구조를 바이트 스트림으로 변환하는 것이 직렬화 혹은 pickling이라 하고, 이에 대한 역 연산이 바로 역 직렬화 혹은 unpickling인 것이다.

     

그렇다면 이러한 직렬화를 하는 이유는 무엇일까?

그 이유는 직렬화된 데이터를 파일/데이터베이스에 저장하거나 세션에 걸쳐 프로그램 상태를 유지하거나, 네트워크를 통해 데이터를 전송하기 위해서이다. pickle된 바이트 스트림은 unpickling을 통해 원래 객체 계층을 다시 만드는데 사용할 수 있다.



1.2 모듈 인터페이스

pickle 모듈의 인터페이스 중 대표되는 4개의 인터페이스에 대해서 설명하겠다.


1) pickle.dump : 객체 obj의 피클 된 표현을 열린 파일 객체 file에 쓴다.

2) pickle.dumps :  객체 obj의 피클 된 표현을 파일에 쓰는 대신 bytes 객체로 반환한다.


3) pickle.load : 열린 파일 객체 file에서 객체의 피클 된 표현을 읽고, 그 안에 지정된 객체 계층 구조를 재구성하여 반환

4) pickle.loads : 객체의 피클 된 표현 data의 재구성된 객체 계층 구조를 반환


아래는 pickle.dump을 이용하여 "Rootable"이라는 String 객체를 직렬화하고, pickle.loads를 이용하여 다시 역직렬화 예시이다.

python3에서 TEST한 코드



1.3 pickle 동작 분석

- pickletools.dis를 통해 disassemble 한 것을 분석하여 pickle이 어떻게 동작하는지를 분석해보자.

- 아래 예제는 info라는 딕셔너리에 name, userid, password라는 3개의 쌍을 추가하고 이를 직렬화(pickling)한 뒤 이를 pickletools.dis한 것이다.


각각의 문장에 대해 간단히 설명을 추가해보자면 다음과 같다.

 0: PROTO    3        → protocol의 버전을 나타내는 것으로 python의 버전이라고 생각하면 된다.

 2: EMPTY_DICT    → 빈 Python list를 만들고 이를 stack에 올린다.

 3: BINPUT  [index]      → 이전의 item(빈 리스트)를 스택의 0번째에 올린다. 뒤의 숫자는 index를 나타낸다고 생각하면 된다.

 5: MARK            → 스택에서 리스트의 시작을 의미한다.

 6: BINUNICODE '[문자열]' → binary로 표현된 것을 unicode로 변환하여 스택에 넣는다.

 78 : SETITEMS (MARK at 5) → 5번째에 있는 MARK를 기준으로 stack에 있는 key와 value를 list에 넣는다.



(참고)pickle.py에 SETITEMS는 다음과 같이 구현이 되어있다.

1
2
3
4
5
def load_setitems(self):
    items = self.pop_mark()
    dict = self.stack[-1]
    for i in range(0len(items), 2):
        dict[items[i]] = items[i + 1]
cs





2. deserialize 취약점 동작 원리

- pickle 모듈은 다양한 메소드를 지원하고 있다. 이 중 object.__reduce__() 메소드에서 취약점이 발생한다. 


2.1 __reduce__() 메소드

__reduce__() 메소드는 파이썬 객체 계층 구조를 unpickling 할 때 객체를 재구성하는 것에 대한 tuple을 반환해주는 메소드이다.


이렇게 말을 한다면 무슨 말인지 이해가 잘 안가니 조금 더 풀어서 설명을 하겠다.


바이트 스트림을 unpickle할 때, pickle 모듈은 먼저 original object의 인스턴스를 만들고 나서 그 인스턴스를 올바른 데이터로 채운다. 이를 위해서 바이트 스트림에는 original object 인스턴스에 특정된 데이터만 포함한다. 

그러나 데이터만을 가지고 있는 것으로는 충분하지 않을 수 있다. object를 성공적으로 unpickle하기 위해, 그 pickle된 바이트 스트림에는 unpicker에 대한 명령 피연산자와 함께 원래 객체 구조를 재구성하는 명령이 포함되어 있어 객체 구조를 채울 수 있다.


여기서 unpicker에 대한 명령 피연산자와 원래 객체 구조를 재구성하는 명령을 __reduce__() 메소드를 통해 선언하는 것이다. 이를 통해 object가 unpickle될 때 어떻게 재구성될지를 알려주는 것이다.


__reduce__ 메소드는 보통 리턴 값은 2개의 인자를 가지고 있으며 다음의 구성을 가지고 있다.

⦁ 호출가능한 객체 (보통 호출할 클래스의 이름이다)

⦁ 호출가능한 객체에 대한 인자. 호출가능한 객체가 인자를 받아들이지 않으면 빈 튜플을 제공해야 한다.


이 때 호출가능한 객체에 eval 혹은 os와 같이 명령어를 실행할 수 있는 클래스를 임의로 지정할 수 있다면, 이로 인해 RCE와 같은 보안 취약점이 발생할 수 있다.



2.2 취약점 예시

 - 아래 예시는 eval 함수를 이용하여 rootable이라는 문자열을 print 하는 예시이다. 아래와 같이 해당 payload를 loads하게 되면 print 함수가 실행되는 것을 볼 수 있다.

 - stack에 함수로 [ eval ]을 입력하고, 그 인자로 [ print('rootable') ]를 입력해서 실행되는 것을 알 수 있다.



 - payload는 __builtin__.eval(print('rootable'))를 의미함.


 - b : byte 형식임을 표현

 - c : 모듈의 이름 앞에 붙는 문자

 - ( : stack 의 시작지점

 - S : 문자열 표현

 - tR : 코드의 끝에 붙는 문자



 - Exploit1 클래스를 보면 print(123) 문장을 실행시키는 exploit 코드이다. 

eval은 식을 실행 시키는 것이고 exec는 문장을 실행시키는 것이기 때문에 print(123)을 실행시키기 위해서는 exec를 이용해야 한다. 하지만, exec는 return 값 자체가 없기 때문에 compile 함수를 통해 컴파일 코드로 변환시킨 후 eval로 실행시켜주는 것으로 exploit할 수 있다.


 - Exploit2 클래스를 보면 os 모듈을 이용하여 whoami라는 명령어를 실행시킨 exploit코드이다.

해당 exploit을 활용하여 RCE가 가능하다.


./flag.txt 파일 읽기


class Exploit(object):

   def __reduce__(self):

      p = "open('./flag.txt').read()"

      return (eval,(p,))



(참고)

https://stackoverflow.com/questions/19855156/whats-the-exact-usage-of-reduce-in-pickler

https://rushter.com/blog/pickle-serialization-internals/

https://as3617.tistory.com/34?category=866748

https://whitesnake1004.tistory.com/704

https://www.synopsys.com/blogs/software-security/python-pickling/

반응형