각진 세상에 둥근 춤을 추자

개발자 도구 페이로드 평문 패스워드 노출: 클라이언트 측 RSA 암호화를 활용한 로그인 (2) - CAPTCHA 적용에 대해 본문

보안

개발자 도구 페이로드 평문 패스워드 노출: 클라이언트 측 RSA 암호화를 활용한 로그인 (2) - CAPTCHA 적용에 대해

circle.j 2024. 8. 26. 18:00

[이전 게시글 요약]

2024.08.22 - [Java] - 개발자 도구 페이로드 평문 패스워드 노출: 클라이언트 측 RSA 암호화를 활용한 로그인 (1)

 

개발자 도구 페이로드 평문 패스워드 노출: 클라이언트 측 RSA 암호화를 활용한 로그인 (1)

스프링 시큐리티 로그인 시 패스워드 노출 문제와 보안 위험성 웹 애플리케이션에서 스프링 시큐리티를 사용해 로그인 기능을 구현할 때, 사용자가 입력한 패스워드가 서버로 전송되기 전에 페

this-circle-jeong.tistory.com

 

로그인 시 클라이언트 측에서 패스워드를 RSA로 암호화하여 서버에 전송하는 방식은 개발자 도구의 네트워크 탭에서 평문 패스워드가 노출되는 것을 방지하는 효과적인 방법이다. 이때 클라이언트가 서버에 공개키를 요청하는 시점은 "로그인 버튼 클릭" 순간이다. 이는 필요한 경우에만 키를 생성하여 서버의 불필요한 부하를 줄이기 위함이다. 

 

그러나 이 방식은 악의적인 공격자가 로그인 버튼을 반복적으로 클릭해 서버가 지속적으로 키를 생성하게 만들 경우,
서버 과부하가 발생할 위험이 있다. 

이를 방지 하기 위해 CAPTCHA를 도입한다.
일정 횟수 이상의 로그인 시도가 발생할 경우, CAPTCHA를 통해 사용자가 실제 사람인지 확인한 후에만 로그인을 시도할 수 있도록 설정한다. 


 


CAPTCHA

 

CAPTCHA(Completely Automated Public Turing test to tell Computers and Humans Apart)는 컴퓨터와 인간을 구분하기 위해 사용되는 자동화된 테스트이다. 
웹사이트에서 볼 수 있는 CAPTCHA는 사용자에게 이미지 내의 문자를 입력하거나, 특정 이미지를 선택하도록 하여 사용자가 인간인지 봇인지 식별하는 방식으로 동작한다. 
주로 자동화된 봇 공격으로부터 웹사이트를 보호하는 데 사용된다.

 

 

[주로 쓰이는 CAPTCHA의 형태]

 

(1) 텍스트 기반 CAPTCHA: 왜곡된 문자를 사용자가 입력 - 하지만 점점 더 정교해지는 OCR(광학 문자 인식) 기술에 의해 무력화되는 위험이 있다. OCR 기술은 이미지를 분석해 텍스트로 변환하는데,  AI 기반의 OCR 시스템이 이를 인식하고 탈취할 가능성이 점점 더 커지고 있다. 

(2) 이미지 선택이나 텍스트 입력 방식의 CAPTCHA - 특정 이미지에서 사물(예: 자동차, 횡단보도 등)을 선택하게 하거나, 주어진 텍스트를 입력하게 하는 방식이다. 그러나 사용자가 추가적인 작업을 해야 하기 때문에, 사용자 경험을 저해할 수 있다. 사용자 편의성을 고려했을 때, 해당 방식을 통해 보안성은 높일 수 있지만 사용자에게 번거로움을 줄 수 있다. 

(3) 마우스 클릭 방식의 CAPTCHA (체크박스 클릭 또는 올바른 문장 선택) - 사용자 경험을 최적화하면서 보안을 유지할 수 있는 방식 

 

 

[CAPTCHA의 적용 전략]

(1) 로그인 시도 횟수 기반 CAPTCHA 적용: 사용자가 로그인 시도를 일정 횟수 이상 실패하면, 이후 시도에는 CAPTCHA를 적용한다. 이 경우 정상적인 사용자에게는 불필요한 방해 없이 로그인이 가능하고, 의심스러운 활동이 감지될 때만 CAPTCHA가 적용된다.

(2) 비정상적인 활동 감지 후 CAPTCHA: 특정 IP에서 비정상적인 활동(예: 짧은 시간 내에 다수의 로그인 시도)이 감지될 경우, 해당 IP의 사용자는 CAPTCHA를 통과해야만 로그인을 시도할 수 있도록 설정한다.

 

이 경우, 서버에서 지속적으로 IP를 추적하고 차단하는 것보다 효율적이다. 

 

 

[CAPTCHA 도입의 장점]

(1) 서버 부하 감소: 자동화된 공격으로 인해 발생하는 서버의 부하를 줄일 수 있다. IP 기반 요청 제한과 달리 CAPTCHA는 사람만 통과할 수 있으므로 봇 공격을 효과적으로 차단한다.

(2) 보안성 강화: 악의적인 공격자가 무차별 대입 공격을 시도하는 것을 받지할 수 있다.

(3) 유연한 적용: 사용자가 정상적으로 로그인 시도를 하는 경우에는 CAPTHCA를 적용하지 않는다.

 

 


[CAPTCHA 구현 예시]

1. ALTCHA (라이브러리)
https://altcha.org/

 

ALTCHA - Free, open-source and GDPR-compliant CAPTCHA alternative, Spam Filter API and Forms

ALTCHA is a free CAPTCHA alternative that uses a proof-of-work mechanism to protect your website, and online services from spam.

altcha.org

- ALTCHA는 체크박스 클릭과 같은 단순한 사용자 경험을 제공하는 오픈 소스 CAPTCHA 솔루션이다.
- ALTCHA는 사용자가 체크박스를 클릭하면 PoW(Proof of Work) 기반 검증을 자동으로 수행한다.

 

2. Google reCAPTCHA (API)
https://www.google.com/recaptcha/about/

- 마우스 움직임 분석: 체크박스 클릭 전 사용자 마우스 움직임 패턴 분석 
- 사용자 행동 데이터: 클릭 외에도 사용자가 사이트에서 어떻게 행동하는지, 페이지에서 얼마나 머무르는지, 그리고 다른 상호작용들을 분석하여 사람과 봇을 구별
- 무료 제공 / 내부 작동 방식이나 알고리즘 미공개 

 

3. jCaptcha 
https://jcaptcha.sourceforge.net/

 

JCaptcha -

Last Published: 01 Sep 2012  | Version: 2.0-alpha-1-SNAPSHOT

jcaptcha.sourceforge.net

- JAVA 기반의 CAPTCHA 라이브러리

 

4. 별도의 라이브러리 없이 직접 구현 

더보기

(1) 첫 번째 방법 (보안코드입력/ JavaScript)

참고: https://www.redinfo.co.kr/post/view/208 (PHP로 캡차 기능 구현)

- config.js : CAPTCHA의 랜덤 문자열을 생성하고, 비밀키와 함께 해싱하여 보안 코드를 생성 

const RI_CAPTCHA_SECRET_KEY = '비밀키'; 
// CAPTCHA 생성을 위한 고정된 비밀키를 설정. 이 키는 보안코드 생성 시 사용.

function createRandTxt(clen = 6) {
  // 랜덤 문자열을 생성하는 함수. 기본적으로 6자리의 문자열을 생성.
  const txt = (Date.now().toString(36) + Math.random().toString(36).substr(2, 10)).toUpperCase();
  // 현재 시간과 난수를 결합해 문자열을 생성한 후, 대문자로 변환.
  
  const atxt = txt.split('');
  atxt.sort(() => 0.5 - Math.random());
  // 생성된 문자열의 각 문자를 배열로 변환한 후 무작위로 섞음.
  
  return atxt.join('').substr(0, clen);
  // 무작위로 섞은 배열을 다시 문자열로 변환하여, 필요한 길이만큼 잘라서 반환.
}


- index.html: CAPTCHA를 사용자에게 보여주고, 사용자가 보안코드를 입력할 수 있는 HTML 폼
페이지가 로드되면 CAPTCHA를 생성하는 함수가 호출되고, 사용자가 보안코드를 입력하여 제출하면 CAPTCHA 검증 함수가 실행

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>CAPTCHA 예제</title>
  <script src="config.js"></script>
  <script src="captcha.js"></script>
  <!-- CAPTCHA 생성과 검증을 위한 자바스크립트 파일을 로드. -->
</head>
<body>
  <form id="form">
    <!-- 사용자가 질문을 입력하고, 보안코드를 입력할 수 있는 폼. -->
    <input type="text" name="txt" placeholder="질문을 입력해주세요."><br>
    <div>
      <span id="captcha-txt"></span><br>
      <!-- 랜덤하게 생성된 CAPTCHA 문자열이 표시. -->
      <input type="text" name="code" placeholder="보안코드입력">
      <!-- 사용자가 CAPTCHA 문자열을 보고 입력할 수 있는 입력 필드. -->
    </div>
    <input type="submit" value="제출">
  </form>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      createCaptcha();
      // 페이지가 로드되면, CAPTCHA를 생성하는 함수가 호출.
    });

    document.getElementById('form').addEventListener('submit', function(event) {
      event.preventDefault();
      // 폼이 제출되면 기본 제출 동작을 막고, CAPTCHA 검증을 진행.
      verifyCaptcha();
    });
  </script>
</body>
</html>

 

- captcha.js: CAPTCHA 생성 및 검증 로직 담당

let captchaTxt = '';
let secure = '';

function createCaptcha() {
  captchaTxt = createRandTxt();
  // 랜덤하게 생성된 CAPTCHA 텍스트를 저장.
  
  secure = md5(RI_CAPTCHA_SECRET_KEY + captchaTxt);
  // 비밀키와 CAPTCHA 텍스트를 조합하여 보안 해시 값을 생성.
  
  document.getElementById('captcha-txt').textContent = captchaTxt;
  // 생성된 CAPTCHA 텍스트를 HTML 페이지에 표시.
}

function verifyCaptcha() {
  const userCode = document.querySelector('[name="code"]').value;
  // 사용자가 입력한 보안코드를 가져온다.
  
  const checkSecure = md5(RI_CAPTCHA_SECRET_KEY + userCode);
  // 사용자가 입력한 보안코드와 비밀키를 조합해 해시를 생성하여 원래 보안 해시와 비교.
  
  if (checkSecure === secure) {
    alert("CAPTCHA 통과!");
    // 해시 값이 일치하면 CAPTCHA를 통과한 것으로 처리.
  } else {
    alert("CAPTCHA 불일치. 다시 시도하세요.");
    // 해시 값이 일치하지 않으면 실패 메시지를 표시.
  }
}

 


(2) 두 번째 방법 (체크박스 선택/ JavaScript/ 첫 번째 방식을 체크박스 선택으로 수정)

(동작 요약)
-- 체크 박스 생성: 사용자가 "로봇이 아닙니다" 체크박스를 클릭하면 'verifyCaptcha()' 함수가 호출되어 검증이 수행
-- 폼 제출 시점 확인: 사용자가 체크박스를 클릭하지 않은 경우 제출이 차단 

- config.js : CAPTCHA의 랜덤 문자열을 생성하고, 비밀키와 함께 해싱하여 보안 코드를 생성 

const RI_CAPTCHA_SECRET_KEY = '비밀키'; 
// CAPTCHA 생성을 위한 고정된 비밀키를 설정. 이 키는 보안코드 생성 시 사용.

function createRandTxt(clen = 6) {
  // 랜덤 문자열을 생성하는 함수. 기본적으로 6자리의 문자열을 생성.
  const txt = (Date.now().toString(36) + Math.random().toString(36).substr(2, 10)).toUpperCase();
  // 현재 시간과 난수를 결합해 문자열을 생성한 후, 대문자로 변환.
  
  const atxt = txt.split('');
  atxt.sort(() => 0.5 - Math.random());
  // 생성된 문자열의 각 문자를 배열로 변환한 후 무작위로 섞음.
  
  return atxt.join('').substr(0, clen);
  // 무작위로 섞은 배열을 다시 문자열로 변환하여, 필요한 길이만큼 잘라서 반환.
}

 

- index.html: 체크박스를 포함한 HTML 폼, 사용자가 체크박스를 선택했을 때 CAPTCHA 검증이 이루어진다. 

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>CAPTCHA 예제</title>
  <script src="config.js"></script>
  <script src="captcha.js"></script>
</head>
<body>
  <form id="form">
    <!-- 보안 검증용 체크박스 -->
    <label>
      <input type="checkbox" id="captcha-checkbox">
      로봇이 아닙니다.
    </label><br>
    <input type="submit" value="제출">
  </form>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      createCaptcha();
    });

    document.getElementById('captcha-checkbox').addEventListener('change', function() {
      if (this.checked) {
        verifyCaptcha();
      }
    });

    document.getElementById('form').addEventListener('submit', function(event) {
      event.preventDefault();
      if (document.getElementById('captcha-checkbox').checked) {
        alert("CAPTCHA 통과!");
      } else {
        alert("CAPTCHA를 확인하세요.");
      }
    });
  </script>
</body>
</html>


- captcha.js: 체크박스 클릭 시 CAPTCHA 검증을 수행한다.

let secure = '';

function createCaptcha() {
  const captchaTxt = createRandTxt();
  secure = md5(RI_CAPTCHA_SECRET_KEY + captchaTxt);
}

function verifyCaptcha() {
  const checkSecure = secure; // 체크박스 클릭 시 미리 생성된 보안 값을 검증에 사용.
  if (checkSecure) {
    console.log("CAPTCHA 통과!");
  } else {
    console.log("CAPTCHA 실패.");
  }
}

 

별도의 라이브러리나 API 없이 직접 기능을 구현할 경우, 다음과 같은 과정으로 CAPTCHA를 구현할 수 있다. 

(1) 폼에 포함된 체크박스가 실제 사용자에 의해 클릭되었는지 확인 (체크박스에 체크가 되지 않은 상태로 폼의 전송 여부 확인)
(2) 체크박스의 이름과 값을 서버 측에서 검증 (추가적인 보안)

 

그러나 악의적인 사용자가 매크로나 스크립트를 사용해 체크박스 클릭 메서드를 호출하게 되면, 자동으로 폼을 통과할 수 있다. 
즉, 매크로 봇이나 스크립트가 쉽게 CAPTCHA를 우회할 수 있다.

단순한 체크방식은 기본적인 방어 기능만 제공하므로, 매크로 공격에는 취약하다.
따라서 높은 보안 수준이 필요하다면 행동 분석이나 마우스 패턴 분석과 같은 기능을 포함한 솔루션(구글 reCAPTCHA)를 사용하는 것이 좋다. 


만약 별도의 솔루션없이 체크박스 CAPTCHA를 구현하고자 한다면, CAPTCHA를 강화하기 위해 여러 방어 대안이 필요하다. 

 

[매크로 공격 방어]

1. 마우스 움직임 감지 
단순히 체크박스를 클릭하는 것만으로는 보안이 취약하므로, 마우스 움직임을 감지하여 사용자가 실제로 화면에서 체크박스를 클릭했는지 확인한다. 

let mouseMoved = false;

document.addEventListener('mousemove', function() {
  mouseMoved = true;
});

document.getElementById('captcha-checkbox').addEventListener('click', function() {
  if (!mouseMoved) {
    alert("Please move your mouse before clicking.");
    this.checked = false;
  }
});

 

2. 랜덤화된 클릭 요소
고정된 체크박스의 위치를 랜덤화한다 .

// 체크박스 위치를 무작위로 변경
const captchaCheckbox = document.getElementById('captcha-checkbox');
captchaCheckbox.style.position = "absolute";
captchaCheckbox.style.top = Math.random() * 500 + "px";
captchaCheckbox.style.left = Math.random() * 500 + "px";

 

3. 타임스탬프 검증
매크로의 경우 체크박스 클릭 후 즉시 폼을 제출한다. 
이를 기반으로 폼 제출 전 일정 시간 (예: 1초, 2초)이 경과해야만 제출이 가능하도록 설정한다.

let clickTime = 0;

document.getElementById('captcha-checkbox').addEventListener('click', function() {
  clickTime = new Date().getTime();
});

document.getElementById('form').addEventListener('submit', function(event) {
  const currentTime = new Date().getTime();
  if (currentTime - clickTime < 2000) { // 2초 미만
    event.preventDefault();
    alert("Please wait a moment before submitting.");
  }
});

 

4. 허니팟 필드 
허니팟 필드는 실제 사용자가 볼 수 없는 숨겨진 폼의 필드이다.
매크로 봇이 자동으로 모든 필드를 채워 폼을 전송할 경우 해당 필드의 값이 입력되어 있으면 요청을 거부한다. 

<!-- 사용자에게는 보이지 않는 허니팟 필드 -->
<input type="text" name="honeypot" style="display:none;">

 

5. 동적 토큰 활용
폼 제출 시 서버 측에서 동적으로 생성된 CSRF 토큰과 같은 보안 토큰을 추가하여, 매크로 공격을 방지한다.
CSRF 토큰은 각 세션마다 새로 생성되며, 클라이언트에서 해당 토큰을 함께 제출하지 않으면 요청을 거부한다. 

6. 비정상적인 클릭 패턴 감지 
일정 시간 내에 동일한 위치에서 여러 번 클릭되면 요청을 차단한다. 

let lastClickTime = 0;
let lastClickPosition = { x: 0, y: 0 };

document.getElementById('captcha-checkbox').addEventListener('click', function(event) {
    const currentTime = new Date().getTime();
    const clickPosition = { x: event.clientX, y: event.clientY };

    if (currentTime - lastClickTime < 500 && clickPosition.x === lastClickPosition.x && clickPosition.y === lastClickPosition.y) {
        alert("Suspicious activity detected.");
        this.checked = false; // 체크박스 해제
    }

    lastClickTime = currentTime;
    lastClickPosition = clickPosition;
});

 

7. 동적 폼 생성
매크로 봇은 사전에 정해진 폼 구조를 타겟팅하는 경향이 있으므로, 동적으로 폼을 생성하면 폼을 자동으로 채우는 것을 방지할 수 있다. 

document.addEventListener('DOMContentLoaded', function() {
    const form = document.getElementById('form');
    const captchaCheckbox = document.createElement('input');
    captchaCheckbox.type = 'checkbox';
    captchaCheckbox.id = 'captcha-checkbox';
    form.appendChild(captchaCheckbox);
});