(web) Curlove

안녕 오늘은 드림핵 ‘curl’ + ‘ove’ 문제를 풀어보자. 난이도가 5로 상당히 높게 책정되었는데, 난이도에 걸맞게 크게 두가지 파트로 나누어진 문제였다.

이 문제에서 주는 인사이트는 정규식 우회와 curl의 메타기호와 url의 구조이다.

먼저 문제에서 다운로드 받은 app.py의 코드를 살펴보면,

try:
    FLAG = open("./flag.txt", "r").read()
except:
    FLAG = "DH{{This_is_flag}}"

플래그의 위치는 현재 디렉터리인 것을 알 수 있다.

@app.route("/admin", methods=["GET", "POST"])
def admin():
    if not session:
        return redirect("/login")
    
    if session["isAdmin"] == False:
        return redirect("/guest")

    if request.method == "GET":
        return render_template("admin.html")

    if request.method == "POST":
        url = request.form["url"].strip()

        if (url[0:4] != "http") or (url[7:20] != "dreamhack.io/"):
            return render_template("admin.html", msg="Not allowed URL")

        if (".." in url) or ("%" in url):
            return render_template("admin.html", msg="Not allowed path traversal")
        
        if url.endswith("flag") or ("," in url):
            return render_template("admin.html", msg="Not allowed string or character")
        try:
            response = subprocess.run(
                ["curl", f"{url}"], capture_output=True, text=True, timeout=1
            )
            return render_template("admin.html", response=response.stdout)

        except subprocess.TimeoutExpired:
            return render_template("admin.html", msg="Timeout !!!")

위의 코드를 보면, admin 권한이 있어야, 들어갈 수 있는 관리자 페이지가 존재한다.

일단 admin 권한을 얻을 수 있는 방법을 보면,

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "GET":
        return render_template("login.html")

    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")

        if username == "" or password == "":
            return render_template("login.html", msg="Enter username and password")

        sha256_password = sha256((password).encode()).hexdigest()
        try:
            user = get_user(username, sha256_password)

            if user:
                if user[1].startswith("admin"):
                    session["username"] = user[1]
                    session["isAdmin"] = True
                    session["login"] = True
                    return redirect("/admin")
                else:
                    session["username"] = user[1]
                    session["isAdmin"] = False
                    session["login"] = True
                    return redirect("/guest")
            else:
                return render_template("login.html", msg="Login Failed..."), 401
        except Exception as e:
            abort(500)


@app.route("/signup", methods=["GET", "POST"])
def signup():
    if request.method == "GET":
        return render_template("signup.html")

    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")

        if username == "" or password == "":
            return render_template("login.html", msg="Enter username and password")

        m = search(r".*", username)

        if username or m:
            if m.group().strip().find("admin") != 0:
                return render_template("signup.html", msg="Not allowed username"), 403
            else:
                username = username.strip()
                sha256_password = sha256((password).encode()).hexdigest()
                register_user(username, sha256_password)
                return redirect("/login")

코드처럼 admin으로 시작하는 아이디의 경우 자동으로 관리자 권한을 할당 받게 되는데, sign up 시에, admin이 포함된 단어를 필터링하는 정규식이 존재한다.

하지만, 정규식에 “.”은 개행 문자를 제외한 문자를 타겟으로하는데 취약점이 있다.

그렇다면 아이디의 처음을 개행 문자로 하는 아이디를 만든다면 필터링을 우회할 수 있다!

URL 인코딩으로 특수 문자를 변환하므로, Burp Suite를 이용해서 직접 개행 문자를 넣어준다.

이 방법으로 Admin page에 접근할 수 있었다.

접근한 Admin page에서는 url을 입력할 수 있는데, 이때 입력 값을 체크하는 기능이 있다.

@app.route("/admin", methods=["GET", "POST"])
def admin():
    if not session:
        return redirect("/login")
    
    if session["isAdmin"] == False:
        return redirect("/guest")

    if request.method == "GET":
        return render_template("admin.html")

    if request.method == "POST":
        url = request.form["url"].strip()

        if (url[0:4] != "http") or (url[7:20] != "dreamhack.io/"):
            return render_template("admin.html", msg="Not allowed URL")

        if (".." in url) or ("%" in url):
            return render_template("admin.html", msg="Not allowed path traversal")
        
        if url.endswith("flag") or ("," in url):
            return render_template("admin.html", msg="Not allowed string or character")
        try:
            response = subprocess.run(
                ["curl", f"{url}"], capture_output=True, text=True, timeout=1
            )
            return render_template("admin.html", response=response.stdout)

        except subprocess.TimeoutExpired:
            return render_template("admin.html", msg="Timeout !!!")

Url 입력 값을 체크하는 부분을 정리하면 다음과 같다.

  1. http 프로토콜이 [0:4]에서 나와야하고, dreamhack.io/가 [7:20]까지 나와야 한다.
  2. Path traversal을 막기 위해서 “..” “%”필터링이 있다.
  3. flag나 , 같은 문자로 끝나는 것을 막는다.

이런 조건들을 통과하면 비로소 입력한 url을 curl 명령어를 통해서 실행하고 페이지에 띄우게 된다.

curl 실행 예

해당 조건을 만족하는 url을 좀 생각해보면, 일단 [4:6] 까지가 우리가 사용할 수 있는 url 공간이다.. 3글자 밖에 없는데, 이 3자리를 이용하거나 자리를 늘릴 방안을 생각해야 한다.

curl은 프로토콜을 추측해서 적용해주는 기능이 있다 생략하면 http를 적용해줄 듯하다.

좋은 힌트를 참고한다.

ip주소 0.0.0.0은 인트 값으로 계산하면 결국 0이다. 즉 내가 가진 모든 ‘로컬주소’를 지칭하는 url 0.0.0.0은 0으로 써도 되는 것이다.

flag.txt를 필터링을 우회하며 불러내기 위해서 중괄호를 씌웠다. 이 코드가 왜 작동할 수 있냐하면 바로 Crul의 글로빙 기능 덕분이다.

이렇게 해서 완성된 url을 입력하면 flag가 나온다

http@0/dreamhack.io/.{.flag.txt}

게시됨

카테고리

작성자

태그: