
이전 xss-1과 전체적으로 코드가 유사하지만 한가지 다른점은 xss 필터와 /admin/notice_flag 경로가 추가되었다는 점이다.
@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
xss 필터는 파라미터에 frame, script, on이 들어가면 *로 변환한다. 이 필터로 js 코드를 실행할 수 있는 방법이 거의다 막히기 때문에 js 코드는사용이 불가능해진다.
@app.route("/admin/notice_flag")
def admin_notice_flag():
global memo_text
if request.remote_addr != "127.0.0.1":
return "Access Denied"
if request.args.get("userid", "") != "admin":
return "Access Denied 2"
memo_text += f"[Notice] flag is {FLAG}\n"
return "Ok"
/admin/notice_flag 경로는 /memo 경로에서 사용되는 memo_text를 전역변수로 선언하고, 2가지 조건을 통과하면 flag 값을 memo_text에 저장한다.
조건은 로컬이면서 get으로 받아온 파라미터 userid가 admin이어야 한다는 것. 즉, get 요청을 내가 보내면 안되고 서버에서 보내게끔 만들어야 한다.
@app.route("/memo")
def memo():
global memo_text
text = request.args.get("memo", None)
if text:
memo_text += text
return render_template("memo.html", memo=memo_text)
memo_text는 /memo 페이지에서 memo 파라미터의 기본값으로 사용되는데, memo_text는 전역변수이기 때문에 /admin/notice_flag에서 성공적으로 조건문을 통과했다면 프로세스가 유효한 이상 flag 값을 담고 있다. 그래서 한 프로세스에서 /admin/notice_flag에 대한 get 요청과 /memo에 대한 get 요청이 둘 다 성공할 경우, flag 값이 페이지에 출력되게 된다.
흐름으로 보면
/flag -> check_csrf() -> read_url() -> /vuln -> /admin/notice_flag -> /memo

JS 없이 GET 요청을 전송하는 방법은 <img src="url">이 있다. <img> 태그의 src 속성은 작성한 url에 get 요청을 전송해서 이미지 파일을 받아온다.(POST는 안됌)
테스트를 위해 /vuln 경로에서 <img> 태그의 src 속성으로 /admin/notice_flag와 /memo 경로에 순서대로 get 요청을 보낸다. 코드는 아래와 같다.
<img src="/admin/notice_flag?userid=admin"><img src="/memo?memo=aaaaaa">

/memo 페이지로 이동해보면 제대로 실행된 것을 볼 수 있다.
이제 해당 코드를 /flag 페이지에서 입력.

성공적으로 flag를 받아왔다.