문제 분석

A group of students who don’t like to do things the “conventional” way decided to come up with a CyberSecurity Blog post.

You’ve been hired to perform an in-depth whitebox test on their web application.

처음 접속 시 Bad Gateway만 출력되는 것을 확인하였습니다. 문제에서 제공되는 소스 코드를 통해 Flag가 나오는 조건을 확인하였습니다.

flag = os.getenv("flag")

@app.route("/get_flag")
@login_required
def get_flag():
    if flag == "true":
        return "DUCTF{NOT_THE_REAL_FLAG}"
    else:
        return "Nope"

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect("/dashboard")
    user = User.query.filter_by(username=request.form.get("username")).first()
    if user and check_password_hash(user.password, request.form.get("password")):
        login_user(user)
        return redirect("/")
    return render_template("login.html")

로그인 이후 os.getenv OS의 환경변수 flag를 True로 변경하여 /get_flag에 접속하면 Flag를 Return 하는 것을 볼 수 있습니다.

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect("/dashboard")
    if request.method == "POST":
        hashed_password = generate_password_hash(request.form.get("password"), method='pbkdf2:sha256')
        new_user = User(username=request.form.get("username"), password=hashed_password)
        db.session.add(new_user)
        db.session.commit()
        return redirect("/login")
    return render_template("register.html")

로그인을 위한 Register를 사용하는데 비밀번호는 SHA256으로 암호화되며 DB에 저장되는 것을 확인할 수 있습니다.

@app.route("/dashboard")
def dashboard():
    posts = BlogPost.query.filter_by(author=current_user.id).all()
    return render_template("dashboard.html", posts=posts)


@app.route("/blog/<blog_id>")
def blog(blog_id):
    post = BlogPost.query.filter_by(id=int(blog_id)).first()
    if not post:
        flash("Blog post does not exist!")
        return redirect("/")
    return render_template("blog.html", post=post)

로그인 이후 Dashboard를 게시물을 작성할 수 있는 기능을 볼 수 있습니다. 작성한 게시물은 수정이 가능하며 Home과 Dashboard를 통해 확인이 가능합니다.

@app.route("/feedback")
@login_required
def feedback():
    return render_template("feedback.html")

@app.route("/save_feedback", methods=["POST"])
@login_required
def save_feedback():
    data = json.loads(request.data)
    feedback = Feedback()
    # Because we want to dynamically grab the data and save it attributes we can merge it and it *should* create those attribs for the object.
    merge(data, feedback)
    save_feedback_to_disk(feedback)
    return jsonify({"success": "true"}), 200

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

Feedback 기능을 통해 게시물에 대한 피드백을 수정할 수 있다. 해당 피드백을 저장하기 위해서 merge 함수를 사용하게 되는데 이 함수는 python의 class pollution를 유발하는 함수로 os.getEnv('flag')의 값을 직접적으로 변경할 수 있게 된다.

따라서, 로그인 이후 대시보드를 통한 게시물 작성 이후 피드백을 통해 flag의 값을 merge를 통해 true로 변경하여 /get_flag에 접근하면 해결이 된다.

각 TextArea에 값을 넣고 Feedback 전송 시 {"title":"1234","content":"1234","rating":"1","referred":""} 이와 같이 Json 형태로 넘어가는 것을 알 수 있다.

파이썬 내부에 선언되어 있는 변수의 값을 변경하기 위해 {"__class__":{"__init__":{"__globals__":{"flag":"true"}}}}로 전송할 경우 정상적으로 처리된 것을 확인할 수 있다.

/get_flag에 접근 시 save_feedback을 통해 flag=True가 되었기에 플래그 추출이 가능하였습니다.