XSS는 사용자가 특정 사이트를 신뢰하기 때문에 발생하는 문제라면, CSRF는 특정 사이트가 사용자를 신뢰하기 때문에 발생하는 문제이다. XSS는 클라이언트의 브라우저에서 발생하는 문제, CSRF는 서버에서 발생하는 문제이다.
즉 두가지 조건이 필요하다.
1. 사용자가 사이트에 로그인한 상태
2. 사용가 조작된 페이지에 접속
먼저 문제의 페이지를 들어가보니 로그인을 해달라고 한다. 그럼 해줘야지
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
elif request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
try:
pw = users[username]
except:
return '<script>alert("not found user");history.go(-1);</script>'
if pw == password:
resp = make_response(redirect(url_for('index')) )
session_id = os.urandom(8).hex()
session_storage[session_id] = username
resp.set_cookie('sessionid', session_id)
return resp
return '<script>alert("wrong password");history.go(-1);</script>'
로그인 페이지를 뜯어보니
POST 요청으로 username, password를 받고있다.
비밀번호가 틀리면 알수없는 유저, 맞으면 make_response 함수를 호출한다.
index페이지로 리다이렉트 되는 HTTP응답 객체를 생성한다.
8비트길이의 난수를 생성하고, 16진수 문자열로 변환해 세션id로 사용한다.
이후 딕셔너리 형태로 세션 스토리지에 저장하고 사용자의 브라우저 쿠키에 세션 아이디를 저장한다.
@app.route("/vuln")
def vuln():
param = request.args.get("param", "").lower()
xss_filter = ["frame", "script", "on"]
for _ in xss_filter:
param = param.replace(_, "*")
return param
다음으로 vuln페이지를 보면, request.args.get("param","") 로 URL쿼리에서 param라는 이름의 값을 가져온다.
/vuln?param=value라면 param에 value가 저장되는것이다. (소문자로)
또한 XSS공격을 방지하기위해 frame, script, on이란 문자열을 필터처리해주고 있다.
필터링 된 결과를 반환한다.
이전문제와는 다르게 XSS공격이 방지된것을 볼 수 있다.
@app.route("/flag", methods=["GET", "POST"])
def flag():
if request.method == "GET":
return render_template("flag.html")
elif request.method == "POST":
param = request.form.get("param", "")
session_id = os.urandom(16).hex()
session_storage[session_id] = 'admin'
if not check_csrf(param, {"name":"sessionid", "value": session_id}):
return '<script>alert("wrong??");history.go(-1);</script>'
return '<script>alert("good");history.go(-1);</script>'
FLAG 페이지를 보면 POST요청처리는
POST요청에서 전달된 폼데이터 중 param이란 이름의 값을 가져온다.
session_id = os.urandom(16).hex(): 16바이트의 랜덤한 값을 생성하고, 이를 16진수로 변환하여 세션 ID로 사용한다.
이후 저장소에 admin의 값을 딕셔너리 형태로 저장한다.
CSRF 검증:
• if not check_csrf(param, {"name": "sessionid", "value": session_id}):는 CSRF 검증을 수행하는 부분
def check_csrf(param, cookie={"name": "name", "value": "value"}):
url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
return read_url(url, cookie)
check_csrf함수를 보면 세션아이디를 값으로 하는 쿠키를 설정하고, param값을 url에 맞게 인코딩해준다.
이후 read_url함수를 url과 쿠키를 넣어 실행시킨다
def read_url(url, cookie={"name": "name", "value": "value"}):
cookie.update({"domain": "127.0.0.1"})
try:
service = Service(executable_path="/chromedriver")
options = webdriver.ChromeOptions()
for _ in [
"headless",
"window-size=1920x1080",
"disable-gpu",
"no-sandbox",
"disable-dev-shm-usage",
]:
options.add_argument(_)
driver = webdriver.Chrome(service=service, options=options)
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
driver.get("http://127.0.0.1:8000/")
driver.add_cookie(cookie)
driver.get(url)
except Exception as e:
driver.quit()
print(str(e))
# return str(e)
return False
driver.quit()
return True
쿠키를 추가하고, 기본페이지에 접속한다.
이후 add.cookie로 쿠키를 추가해주고 url로 접속한다.
이때의 url은 앞에 check_CSRF함수에서 인코딩한 vuln 페이지이다.
앞의 XSS-2문제는 vuln이 필터없이 그대로 출력하기 때문에 자바스크립트와 같은 코드를 실행시킬 수 있었는데 이번엔 필터링 처리때문에 그 취약점은 사라졌다.
@app.route("/change_password")
def change_password():
pw = request.args.get("pw", "")
session_id = request.cookies.get('sessionid', None)
try:
username = session_storage[session_id]
except KeyError:
return render_template('index.html', text='please login')
users[username] = pw
return 'Done'
마지막 페이지인 change_password를 살펴보면
쿠키에서 세션아이디를 받아온 뒤 username에 세션아이디를 넣고 username을 pw로 바꿔준다.
여기서 주의해야 할 점은 get요청을 사용하고있다는 점이다. 즉 url에 비밀번호를 넣어라는것이다.
users의 value값은 password에 대응하는 것을 알았으며, 결국 비밀번호를 변경하는 코드인 것이다.
앞 문제에선 vurl에 script코드를 삽입해서 memo를 통해 값을 알아내려했다면, 이번엔 scrtipt는 필터링 되고 있으므로 img태그로 admin의 비밀번호를 바꾸는 코드를 감싸볼것이다.
<img src="/change_password?pw=admin"> 코드를 vurl에 넣어준다.
그럼 admin의 비밀번호가 admin으로 바뀌게된다.