CTF/웹해킹

[HackPack CTF] Most Cookies - 웹해킹 / 쿠키변조 / Flask

SecurityMan 2022. 4. 12. 21:00

 

이번 대회에서 가장 어려웠던 웹해킹 문제

 

확실히 쉬움/중간 난이도 문제보다 난이도 차이가 크게 난다.

 

제목부터 알 수 있듯이 쿠키값을 변조해서 세션을 탈취하는 문제이다.

 

반응형

 

문제 설명을 읽어보면 Flask session cookie는 아주 안전하다고 한다.

 

CTF에서 이렇게 무언가가 안전하다고 말하는것은 100% 안전하지 않다.

 

그 취약점을 이용해서 해킹을 하면 된다.

 

이번 문제는 소스 파일이 주어지고, 문제페이지 주소도 별도로 제공된다.

 

 

문제페이지에 들어가보면 이런 화면이 나오낟.

 

Cookie를 검색하는 사이트라고 한다.

 

일단 저기 예시로 나와있는 snickerdoodle을 입력해본다.

 

 

snickerdoodle을 입력하면 초록색 창에서

 

That is a cookie! Not very special thought.. 라는 문구가 뜨고,

 

밑에는 I love snickerdoodle cookies! 라는 문구가 출력된다.

 

 

입력창에 쿠키 이름이 아닌 다른걸 입력하면

 

빨간색 창과 함께 That doesn't appear to be a valid cookie 라는 문구가 뜬다.

 

 

이쯤에서 주어진 소스파일인 server.py 파일을 열어본다.

 

from flask import Flask, render_template, request, url_for, redirect, make_response, flash, session
import random
app = Flask(__name__)
flag_value = open("./flag").read().rstrip()
title = "Most Cookies"
cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
app.secret_key = random.choice(cookie_names)

@app.route("/")
def main():
	if session.get("very_auth"):
		check = session["very_auth"]
		if check == "blank":
			return render_template("index.html", title=title)
		else:
			return make_response(redirect("/display"))
	else:
		resp = make_response(redirect("/"))
		session["very_auth"] = "blank"
		return resp

@app.route("/search", methods=["GET", "POST"])
def search():
	if "name" in request.form and request.form["name"] in cookie_names:
		resp = make_response(redirect("/display"))
		session["very_auth"] = request.form["name"]
		return resp
	else:
		message = "That doesn't appear to be a valid cookie."
		category = "danger"
		flash(message, category)
		resp = make_response(redirect("/"))
		session["very_auth"] = "blank"
		return resp

@app.route("/reset")
def reset():
	resp = make_response(redirect("/"))
	session.pop("very_auth", None)
	return resp

@app.route("/display", methods=["GET"])
def flag():
	if session.get("very_auth"):
		check = session["very_auth"]
		if check == "admin":
			resp = make_response(render_template("flag.html", value=flag_value, title=title))
			return resp
		flash("That is a cookie! Not very special though...", "success")
		return render_template("not-flag.html", title=title, cookie_name=session["very_auth"])
	else:
		resp = make_response(redirect("/"))
		session["very_auth"] = "blank"
		return resp

if __name__ == "__main__":
	app.run()

 

코드가 꽤 길다.

 

import flask를 통해 flask로 만든 웹 페이지인걸 알 수 있다.

 

flask는 파이썬으로 작성된 웹 프레임워크이다. 파이썬 코딩으로 웹 페이지를 만들수 있도록 해준다.

 

cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
app.secret_key = random.choice(cookie_names)

 

이 부분이 중요하다.

 

cookie_names 라는 배열안에 쿠키 이름들이 들어가있다.

 

맨 처음봤던 snickerdoodle도 있는걸 볼 수 있다.

 

여기서 random.choice로 하나를 골라서 secret_key 로 사용하고 있다.

 

def search():
	if "name" in request.form and request.form["name"] in cookie_names:
		resp = make_response(redirect("/display"))
		session["very_auth"] = request.form["name"]
		return resp

 

두번째로 확인할 부분은 이곳이다.

 

사용자의 입력을 받는 부분인데 form에서 name 변수에 사용자의 입력을 저장해서

 

very_auth 라는 세션값에 저장하고 있다.

 

아까처럼 sinckerdoodle을 입력하면 very_auth에 sinckerdoodle이라는 값이 저장되는 것이다.

 

def flag():
	if session.get("very_auth"):
		check = session["very_auth"]
		if check == "admin":
			resp = make_response(render_template("flag.html", value=flag_value, title=title))
			return resp

 

마지막으로 볼 곳은 이 부분이다.

 

very_auth 세션값이 admin일 경우 플래그를 출력하도록 만들어 놓았다.

 

 

일단 쿠키를 사용하는 문제이니 쿠키값을 먼저 확인해본다.

 

쿠키라는것은 클라이언트의 상태 정보를 저장하는 특정한 문자열을 말한다.

 

특히나 네이버나 구글같은 웹사이트에 로그인을 하면

 

로그인한 정보를 저장하기 위해서 쿠키라는 값을 생성하게 된다.

 

 

참고로 쿠키값을 확인한 저 프로그램은 크롬 확장프로그램 중 하나로

 

https://chrome.google.com/webstore/search/editthiscookie?hl=ko 이곳에서 설치할 수 있다.

 

문제페이지에서 쿠키값은 seesion에 eyJ2ZX ~~ 로 시작하는 인코딩된 문자열이 저장된 것을 볼 수 있다.

 

base64로 인코딩 되어있는것 같다.

 

 

바로 Cyberchef에서 base64로 디코딩 해봤는데 무언가 이상했다.

 

앞부분은 제대로 디코딩이 되는데, 뒤쪽은 전혀 이상한 문자들이 나왔다.

 

이렇게 일반적인 방법으로 디코딩을 하는것은 아닌가보다.

 

 

flask에서 쿠키값을 만들때는 특별한 방법으로 만든다.

 

flask-unsign 이라는 도구를 이용해서 해볼 수 있다.

 

pip3 install flask-unsign 이라고 입력하면 다운로드가 가능하다.

 

 

다운받고나서 flask-unsign 이라고 입력하면 실행이 된다.

 

 

flask-unsign 을 실행시키면서 -d -c 옵션을 준다.

 

-d는 decode로 base64로 인코딩 된 쿠키값을 디코딩해준다.

 

-c는 디코딩할 쿠키값을 지정해주는 옵션이다.

 

이렇게 명령어를 수행하면 아래쪽에 디코딩된 쿠키값이 출력되는 것을 볼 수 있다.

 

 

flask의 경우 session cookie 값을 만들때

 

이까 소스코드에서 봤던것처럼 secret key를 같이 입력해서 만든다.

 

그걸 알아내기 위해서 -u(unsign) 옵션을 줘서 flask-unsign을 실행시킨다.

 

에러가 뜨는데 wordlist 파일이 선택되지 않았다고 한다.

 

flask-unsign이 brute force 공격을 하기위해서 사전파일이 필요한듯 하다.

 

사전파일은 보통 어마어마한 크기의 txt 파일을 사용하지만, 여기선 그럴 필요가 없다.

 

왜냐면 secret_key에 어떤 값들이 들어가는지 이미 알고있기 때문이다.

 

 

아까 소스코드에서 봤던 쿠키 이름들 중 하나가 sercet_key로 들어가기 때문에

 

txt파일을 하나 만들어서 쿠키 이름들을 써준다.

 

 

아까랑 똑같이 flask-unsign을 실행시키면서 --wordlist 옵션으로 txt 파일을 지정해준다.

 

그럼 아까랑 다르게 secret_key가 butter 라고 알려준다.

 

다음으로 할 일은 조작된 쿠키를 만드는 것이다.

 

아까 소스코드에서 확인했던 것처럼 very_auth에 admin이라는 값이 들어가 있어야 플래그를 볼 수 있다.

 

일반적인 방법으로는 very_auth에 admin이라는 값을 못넣는다.

 

very_auth에는 지정된 쿠키 이름만 들어갈 수 있도록 해놨기 때문..

 

 

하지만 secret key를 알아냈으니 flask-unsign을 이용해서 가짜 쿠키를 만들 수 있다.

 

--sign 옵션을 줘서 쿠키를 생성하고, -c 옵션으로 쿠키로 만들 내용을 지정한다.

 

--secret로 key를 지정해주면 아래쪽에 base64인코딩된 쿠키값이 생성된다.

 

 

이제 쿠키값을 수정하면 된다.

 

쿠키값을 수정할 때는 ModHeader 라는 크롬 확장프로그램을 사용했다.

 

https://chrome.google.com/webstore/search/modheader?hl=ko 이곳에서 다운로드 받을 수 있다.

 

 

Modheader에서  Request header의 Cookie를 session=만들어낸 쿠키값으로 입력해주면

 

 

문제페이지 화면이 바뀌면서 플래그가 출력된다.

반응형