[Dreamhack] STAGE5. Cross Site Request Forgery
Web Hacking - Cross Site Request Forgery (CSRF)
이용자의 신원 정보가 포함된 쿠키는 서명과 같은 역할을 함 (이용자가 문서의 내용에 동의했다). 따라서, 이용자의 식별 정보가 포함된 쿠키는 클라이언트의 요청이 이용자로부터 왔으며, 이를 이용자가 동의했고, 요청에 이용자의 권한이 부여되어야함을 의미한다.
XXS는 서명을 날조하는 공격에 대응되며, CSRF(교차 사이트 요청 위조)는 이미 서명된 문서를 위조하는 것에 대응된다. 조작된 문서의 내용이 서명으로 인해 효력을 가지게 되는 것을 뜻한다. CSRF는 이러한 방식으로 이용자를 속여서 의도치 않은 요청에 동의하도록 한다.
CSRF
임의 이용자의 권한으로 임의 주소에 HTTP 요청을 보낼 수 있는 취약점으로, 공격자는 임의 이용자의 권한으로 서비스를 이용해 이득을 취할 수 있다. (금전적인 이득, 혼란 야기)
*이용자의 송금 요청
GET /sendmoney?to=dreamhack&amount=1337 HTTP/1.1
Host: bank.dreamhack.io
Cookie: session=IeheighaiToo4eenahw3
*CSRF 취약점이 존재하는 예제 코드
# 이용자가 /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."
위 코드는 이용자로부터 예금주와 금액을 입력받고, 계좌의 비밀번호 또는 OTP 등의 사용 없이 송금을 수행한다. 따라서, 로그인한 사용자는 추가 인증 정보 없이 해당 기능을 사용할 수 있다.
CSRF 동작
공격에 성공하기 위해서는 공격자의 악성 스크립트(HTTP 요청을 전송하는 코드)를 이용자가 실행해야하며, 이는 메일또는 게시판의 글을 통해 이용자가 이를 조회하도록 유도하는 방법이 있다.
HTML
* img 태그를 사용한 스크립트의 예로, 이미지의 크기를 줄일 수 있는 옵션을 제공하여 임의 페이지에 요청을 보내도록 한다
<img
src="http://bank.dreamhack.io/sendmoney?to=dreamhack&amount=1337"
width="0px"
height="0px"
/>
* img 또는 form 태그를 사용하면 HTTP 헤더인 Cookie에 이용자의 인증 정보가 포함됨
<img src="https://test.dreamhack.io/main-long.png" />
<form action="https://test.dreamhack.io/users/1" method="post">
<input name="user" />
<input name="pass" />
<input type="submit" />
</form>
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");
XSS vs CSRF
공통점
두 취약점 모두 클라이언트 사이드 공격이며, 이용자가 악성 스크립트가 포함된 페이지에 접속하도록 유도해야 한다.
차이점
- XSS : 세션 및 쿠키 탈취를 목적으로 하며, 공격할 사이트의 origin에서 스크립트를 실행시킨다. 클라이언트 PC에서 스크립트가 실행되며, 클라이언트가 웹사이트를 신뢰한다는 점을 이용한다.
- CSRF : 이용자가 임의의 페이지에 HTTP 요청을 보내는 것을 목적으로 하며, 공격자는 악성 스크립트가 포함된 페이지에 접근한 이용자의 권한으로 웹 서비스의 기능을 실행한다. 웹 서버가 클라이언트의 브라우저를 신뢰한다는 점을 이용하며, 인증된 세션이 필요하다.
- CSRF는 HTTP 요청은 보낼 수 있지만 응답을 볼 수는 없으며, XSS는 HTTP 요청을 보내고 응답을 수신할 수 있다
- CSRF는 악성 링크를 클릭하거나 악성 웹 페이지를 여는 등 사용자의 조치가 필요하지만, XSS는 취약점만 요구한다 (???)
- XSS는 악성 스크립트가 클라이언트에서 실행되지만, CSRF는 사용자가 악성 스크립트를 서버에 요청한다
XSS : 악성 스크립트가 포함된 글을 유저가 조회하면, 서버에 저장되어 있던 악성 스크립트가 전달되어 실행되고, 자동으로 해커에게 유저의 세션 쿠키 값이 전달된다. 해커는 이를 통해 유저 권한으로 서버에 접속한다.
CSRF : 사용자가 사이트에 정상적으로 접속했을 때 해커가 피싱 메일을 전송하고, 사용자가 메일을 누르면 해커가 정해둔 패스워드로 변경 요청이 나간다. 이 경우, 공격 링크를 클릭할 때 사이트에 로그인이 되어있어야만 공격이 성립된다.
실습
- XSS 발생 예시와 종류
Javascript 실행이 제한된 모듈에서, HTML 태그를 통해 계좌 잔고를 1000000원 이상으로 늘려보세요
<img src="/sendmoney?to=dreamhack&amount=1337" />
드림핵 웹 [함께실습] XSS
웹 서비스 분석
엔드포인트 : /vuln
이용자가 전달한 param 파라미터의 값을 출력한다. 이 때, 파라미터에 frame, script, on 세 가지의 키워드가 포함되어 있으면 이를 ‘*’ 문자로 치환하며, 이러한 키워드 필터링은 XSS 공격을 방지하기 위해 존재한다. 본 문제의 의도는 CSRF 공격을 통해 관리자의 기능을 수행하는 것이기에 일부 키워드가 필터링 되어있다
@app.route("/vuln") # vuln 페이지 라우팅 (이용자가 /vuln 페이지에 접근시 아래 코드 실행)
def vuln():
# 이용자가 입력한 param 파라미터를 소문자로 변경
param = request.args.get("param", "").lower()
# 세 가지 필터링 키워드
xss_filter = ["frame", "script", "on"]
for _ in xss_filter:
# 이용자가 입력한 값 중에 필터링 키워드가 있는 경우, '*'로 치환
param = param.replace(_, "*")
return param # 이용자의 입력 값을 화면 상에 표시
엔드포인트 : /memo
이용자가 전달한 memo 파라미터 값을 기록하고, render_template 함수를 통해 출력
@app.route('/memo') # memo 페이지 라우팅
def memo(): # memo 함수 선언
global memo_text # 메모를 전역변수로 참조
# 이용자가 전송한 memo 입력값을 가져옴
text = request.args.get('memo', '')
# 메모의 마지막에 새 줄 삽입 후 메모에 기록
memo_text += text + '\n'
# 사이트에 기록된 메모를 화면에 출력
return render_template('memo.html', memo=memo_text)
엔드포인트 : /admin/notice_flag
로컬호스트 (127.0.0.1)에서 접근하고, userid 파라미터가 admin일 경우 메모에 FLAG를 작성하며, 조건을 만족하지 못하면 접근 제한 메시지가 출력된다. 이 페이지는 모두가 접근 가능하며, userid 파라미터에 admin 값을 넣을 수 있지만, 일반 유저가 해당 페이지에 접근할 때의 ip주소는 조작이 불가능하다. 일반 유저의 ip가 자신의 컴퓨터를 의미하는 로컬호스트 ip가 되는 것은 불가능하기에, 단순 접근으로는 플래그를 획득할 수 없다.
@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 반환
엔드포인트 : /flag
메소드가 GET일 경우 이용자에게 URL을 입력받는 페이지를 제공하며, POST일 경우, param 파라미터의 값을 가져와 check_csrf 함수의 인자로 넣어 호출한다. check_csrf 함수는 인자를 CSRF 취약점이 발생하는 URL의 파라미터로 설정하며, 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:
options = webdriver.ChromeOptions() # 크롬 옵션을 사용하도록 설정
for _ in [
"headless",
"window-size=1920x1080",
"disable-gpu",
"no-sandbox",
"disable-dev-shm-usage",
]:
options.add_argument(_) # 크롬 브라우저 옵션 설정
driver = webdriver.Chrome("/chromedriver", 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 # 정상 종료 처리
취약점 분석 & 익스플로잇 (Exploit : 취약점 공격)
vuln에서 frame, script, on을 필터링하기에 xss 공격을 불가하지만, <,> 을 포함한 다른 키워드와 태그는 사용 가능하기에 CSRF 공격을 수행할 수 있다. notice_flag 페이지에서는 IP 주소가 로컬이고, userid가 admin일 때 memo_text에 플래그를 입력하도록 되어있으므로, flag 페이지에서 서버가 직접 notice_flag 페이지에 접근하도록 만들어야한다. 강의에서 드림핵의 툴을 사용하여 하는 방법이 나와있었는데 사실 이해가 잘 안 가서 그냥 img 태그에 src를 아래와 같이 적어주었다.
<img src="http://127.0.0.1:8000/admin/notice_flag?userid=admin" />
드림핵 웹 [혼자실습] XSS
코드를 살펴보았을 때 csrf-1 문제와 다른 점은 , admin 계정으로 로그인을 진행했을 때 화면에 플래그를 띄워준다는 것이다. 코드 최하단에 있는 change_password로 라우팅된 코드를 살펴보았을 때, 전달된 pw 파라미터를 통해 비밀번호를 변경한다는 것을 알 수 있었다. 또한, check_csrf가 flag에서 호출되는 형태를 보아, 로컬로 접속했을 때 이를 admin 계정으로 인식하는 것을 알 수 있었다. 이 두 정보를 조합하여 아래와 같은 코드를 flag 페이지에 입력하여 admin 계정의 비밀번호를 1234로 변경하도록 했다. 그 후, login 페이지에서 변경한 비밀번호로 로그인을 진행하면 플래그를 얻을 수 있다. 굉장히 접근이 쉬웠던 문제 ..
<img src="http://127.0.0.1:8000/change_password?pw=1234" />