나의 첫 CTF [2024 Layer7 CTF]
소개
2024 Layer7 CTF는 선린 인터넷 고등학교의 Layer7 동아리에서 개최하는 CTF이다.
신청은 Layer7 CTF에서 하고 디스코드도 가입해야 한다.
푼 문제들
솔직히 2 문제 풀었다만 실질적으로 푼 문제는 1 문제이다.
나머지 1 문제는 그냥 답이 나와있기 때문이다.
그렇기 때문에 사실 푼 문제'들'이 아니라 푼 문제라고 하는 게 맞다.
그러나 자존심이 있기 때문에 '들'이라고 했다.
거두절미하고 다음은 푼 문제에 대한 간단한 write-up이다.
Flag 형식은 Layer7{...} 이다.
이번 문제는 File vulnerability와 Path traversal으로 푸는 문제였다.
파일을 올려 다른 경로의 파일을 조회하는 것이 풀이방식이다.
전체 코드
from flask import Flask, render_template_string, request, redirect, url_for, flash, send_from_directory
import os
import markdown
import datetime
app = Flask(__name__, static_folder=None) # 정적 폴더 비활성화
app.secret_key = "helo"
UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = {"md"}
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
if "file" not in request.files:
flash("No file part")
return redirect(request.url)
file = request.files["file"]
if file.filename == "":
flash("No selected file")
return redirect(request.url)
if file and allowed_file(file.filename):
filepath = os.path.join(app.config["UPLOAD_FOLDER"], file.filename)
file.save(filepath)
return redirect(url_for("view_file", filename=file.filename))
else:
flash("Only markdown (.md) files are allowed")
return redirect(request.url)
return '''
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<title>Layer Blog</title>
<style>
body {
background-color: #f8f9fa;
font-family: 'Arial', sans-serif;
}
.container {
margin-top: 50px;
max-width: 600px;
}
.btn {
width: 100%;
}
</style>
</head>
<body>
<div class="container text-center">
<h1 class="mb-4">Welcome to Layer Blog</h1>
<p class="mb-4">Upload a Markdown file to create a new blog post!</p>
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<input type="file" name="file" class="form-control">
</div>
<button type="submit" class="btn btn-primary">Upload</button>
</form>
</div>
</body>
</html>
'''
@app.route("/view/<filename>")
def view_file(filename):
filepath = os.path.join(app.config["UPLOAD_FOLDER"], filename)
if not os.path.exists(filepath):
return "File not found!", 404
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
html_content = markdown.markdown(content)
post_date = datetime.datetime.now().strftime("%B %d, %Y")
return render_template_string(f"""
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{filename} - Layer Blog</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {{
background-color: #f8f9fa;
font-family: 'Georgia', serif;
}}
.navbar {{
background-color: #343a40;
}}
.navbar-brand {{
color: #ffffff !important;
font-weight: bold;
}}
.container {{
margin-top: 2rem;
max-width: 800px;
}}
.post-title {{
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 1rem;
color: #343a40;
}}
.post-meta {{
font-size: 0.9rem;
color: #6c757d;
margin-bottom: 2rem;
}}
.markdown-content {{
font-size: 1.2rem;
line-height: 1.8;
color: #333;
}}
footer {{
margin-top: 3rem;
padding: 1rem 0;
background-color: #343a40;
color: #ffffff;
text-align: center;
}}
</style>
</head>
<body>
<nav class="navbar navbar-dark">
<div class="container">
<a class="navbar-brand" href="/">Layer Blog</a>
</div>
</nav>
<div class="container">
<h1 class="post-title">{filename}</h1>
<p class="post-meta">Posted on {post_date}</p>
<div class="markdown-content">
{html_content}
</div>
<a href="/" class="btn btn-primary mt-4">Upload Another File</a>
</div>
<footer>
<p>© 2024 Layer Blog. All rights reserved.</p>
</footer>
</body>
</html>
""")
@app.route("/image/<filename>")
def serve_image(filename):
return send_from_directory("static", filename)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=9292)
전에 Flask로 게시판을 만들어 본 경험이 도움이 되었다.
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
if "file" not in request.files:
flash("No file part")
return redirect(request.url)
file = request.files["file"]
if file.filename == "":
flash("No selected file")
return redirect(request.url)
if file and allowed_file(file.filename):
filepath = os.path.join(app.config["UPLOAD_FOLDER"], file.filename)
file.save(filepath)
return redirect(url_for("view_file", filename=file.filename))
else:
flash("Only markdown (.md) files are allowed")
return redirect(request.url)
파일을 올리는 창의 코드이다.
순서를 말하자면
1. 파일을 POST 방식으로 전달받는다.
2. allowed_file에서 .md가 들어간 파일만을 받는다.
3. 설정한 UPLOAD 폴더에 파일을 넣는다.
4. view_file 창으로 넘어가 upload한 파일을 본다.
이제 view_file 코드를 보자면
@app.route("/view/<filename>")
def view_file(filename):
filepath = os.path.join(app.config["UPLOAD_FOLDER"], filename)
if not os.path.exists(filepath):
return "File not found!", 404
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
html_content = markdown.markdown(content)
post_date = datetime.datetime.now().strftime("%B %d, %Y")
받은 파일을 기반으로 UPLOAD 폴더에서 파일을 꺼내온다.
마크다운 형식의 내용을 html 형식으로 바꿔서 사용자에게 제공한다.
그러면 이제 풀이를 작성해 보자.
풀이
이 문제를 풀기 전에 알아야 할 정보가 2가지 있다.
.md 파일은 외부 이미지 혹은 내부 이미지를 불러올 수 있다.
static_folder=None 의 의미는 flask 내에서 정적 파일 접근을 막아 커스텀 정적 파일 접근 로직을 만들 수 있어 효율적인 구조를 설정한다는 의미이며, 즉 flask에서 제공하는 /static 경로를 막는다는 뜻이다.
고로 /static 경로를 직접적으로 쓰면 안 된다는 말이다.
.md에서는 이미지를 불러오기 위해

이런 문법을 가진다.
경로는 정확해야 하며 상대 경로 혹은 절대 경로로 할 수 있다. (내부 이미지 불러올 경우)
그리고 문제에서는
이리 말했기 때문에
아무 .md 파일을 만들어 안에

라고 하면 된다.
내가 헷갈렸던 것은 경로 설정이었는데
단순히 /static/flag.png라고 하면 안 되기 때문에 뭐라 해야 하는지 몰라서 한참을 헤맸었다.
@app.route("/image/<filename>")
def serve_image(filename):
return send_from_directory("static", filename)
그냥 대놓고 알려준다.
아래 return 부분에는 static 파일에 filename의 이름을 가진 파일을 보여준다는 의미다.
그래서 그냥 url에 /image/flag.png라고 써도 된다.
그리고 .md 파일에 위에 나온 것처럼 써도 된다.
진짜 코드 좀 잘 봐야겠다.
그리고 왜 image라고 써야 하냐. 다른 asset이나 아무 텍스트는 왜 안 되냐
이렇게 물어볼 수도 있다.
나도 이랬다.
근데 코드를 조금만 더 살펴보면
소감
어제 처음으로 CTF를 해보며 내가 정말 해킹의 겉핥기를 했구나를 알게 되었다.
솔직히 개념을 숙지하고서 내가 그래도 몇 문제는 풀 수 있겠지라고 생각했지만 전혀 그렇지 않았다.
단순히 개념은 개념일 뿐 응용은 사용자의 역량이었고 나는 그걸 간과한 새내기였었다.
그래도 이제야 깨달았고 겨우겨우 한 문제를 풀 때 그 성취감은 성격이 무던한 나도 가슴이 뛰도록 해주었다.
더욱 더 열심히 실력을 갈고닦아서 다음 CTF에서는 그래도 3문제는 풀고 싶다.