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(0, len(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/
'Hacking > Web' 카테고리의 다른 글
XSS In event handler (0) | 2020.08.06 |
---|---|
Content Security Policy(csp) (2) | 2020.07.28 |
Filter bypass Using Multipart form data (0) | 2020.05.08 |
SQL Injection where filter bypass (2) | 2020.04.23 |
get column name in mysql error based sql injection (0) | 2020.04.22 |