클라이언트측 보안은 절대로 깨진다

김병렬·2025년 3월 19일
post-thumbnail

0. 들어가며

복호화 스크립트 실행.gif

위 영상은 제가 보안 메일을 분석해 직접 작성한 복호화 스크립트를 실행하는 모습입니다.
일부 과정은 4배속으로 편집했고 실제로는 비밀번호 없이 약 30초 만에 원본 내용을 확인할 수 있었습니다.

이러한 시도가 가능했던 이유는 두 가지입니다.

  1. 한정된 비밀번호 범위: 비밀번호가 6자리 생년월일(YYMMDD)로 제한되어 경우의 수가 매우 적음
  2. 클라이언트 측 처리: 모든 복호화 과정이 브라우저의 JavaScript로 이루어짐

이는 '보안 메일'이라는 이름에도 불구하고 클라이언트 측에서 처리되는 보안은 본질적으로 안전하지 않다는 것을 보여줍니다.

1. 취약점 발견

최근 국내 모 금융 서비스의 거래 내역서를 발급받을 일이 있었습니다. 메일로 발급하기 옵션을 선택하니 잠시 후 비밀번호가 걸린 HTML 파일이 도착했습니다.
갑자기 개발자로서의 호기심이 발동한 저는 코드를 살짝 들여다봤고... 클라이언트측에서 복호화가 이루어지고 있음을 발견했습니다.

코드를 자세히 분석한 결과, 개발자 도구만으로 쉽게 우회할 수 있음을 확인했습니다.
곧바로 취약점을 고객센터와 버그 바운티 프로그램을 통해 제보했지만 "과도한 사용자 개입이 필요하다"는 이유로 조치 대상에서 제외되었습니다.

KISA 답신

이 글에서는 해당 사례를 들어 클라이언트측 보안이 왜 믿을 수 없는지 설명하겠습니다.

이 글은 교육적 목적으로 작성되었으며, 특정 서비스의 취약점을 악용하도록 장려하지 않습니다
다루고 있는 보안 취약점은 책임감 있는 방식으로 해당 기업에 보고되었습니다
핵심적인 내용 중 일부는 악용의 여지를 우려하여 의도적으로 생략했습니다

2. 보안 처리 방식

해당 파일은 사용자가 올바른 비밀번호를 입력하면 암호화된 진짜 메일 내용을 보여주는 방식입니다. 이 보안 처리가 위험한 이유는 단순합니다.

완전히 클라이언트측에서 이루어지기 때문입니다.

실제로 다음과 같은 방법으로 해당 파일의 복호화 로직을 확인했습니다. 그리고 비밀번호를 구하는 스크립트를 작성할 수 있었습니다.

복호화 로직 확인 과정

  1. 보안 HTML 파일을 브라우저를 이용해 엽니다.
  2. F12를 눌러 개발자 도구를 엽니다.
  3. Elements 탭에서 form 제출 이벤트에 할당된 함수를 확인할 수 있습니다.
    form
  4. 해당 함수를 console에 호출하는 것으로 해당 함수의 선언부로 이동할 수 있습니다.
    calljustify
  5. 코드를 분석해 단계적으로 수행될 로직들을 유추합니다
  6. 중단점을 이용해 유추한 call stack이 일치하는지 확인합니다

위 과정을 통해 확인한 핵심 로직은 다음과 같았습니다.

  1. 유저가 입력한 비밀번호(생년월일)를 복호화 함수의 키로 사용합니다
  2. 올바른 비밀번호일 경우 암호화된 데이터가 성공적으로 복호화되어 PDF 파일 객체가 생성됩니다
  3. 잘못된 비밀번호일 경우 복호화된 데이터가 유효하지 않아 null을 반환합니다

이러한 로직은 브루트포스 공격에 취약할 수밖에 없습니다. 생년월일 형식(YYMMDD)으로 한정된 비밀번호는 경우의 수가 매우 제한적이기 때문입니다.

제가 비밀번호를 찾기 위해 선택한 전략은 다음과 같습니다.

  1. 오늘 날짜부터 하루씩 과거로 거슬러 올라가는 방식으로 비밀번호 생성
  2. 생성된 비밀번호를 인자로 복호화 함수 실행
  3. null이 아닌 결과가 나올 때까지 반복

이 과정을 단순화한 JavaScript 코드를 첨부합니다.

function run() {
    let endDate = new Date(1900, 0, 1);  // 1900년 1월 1일부터
    let startDate = new Date();              // 오늘까지
    let currentDate = startDate;

    while (currentDate > endDate) {
        // 날짜를 YYMMDD 형식으로 변환
        let yy = String(currentDate.getFullYear()).slice(2);
        let mm = String(currentDate.getMonth() + 1).padStart(2, '0');
        let dd = String(currentDate.getDate()).padStart(2, '0');

        const input = yy + mm + dd
        
        // 추출한 HTML에서 가져온 복호화 함수 실행
        const res = decode(input)
        if(res) {
            return input;  // 복호화에 성공한 비밀번호 반환
        }
        
        // 하루 이전 날짜로 이동
        currentDate.setDate(currentDate.getDate() - 1);
    }
}

const pw = run();  // 찾아낸 비밀번호

100년치 생년월일을 시도한다 가정하면 약 36,500개의 경우의 수만 확인하면 됩니다. 현대 컴퓨터의 처리 속도라면 몇 초에서 몇 분 내로 모든 조합을 시도할 수 있습니다.

3. 클라이언트 보안의 한계

클라이언트측 보안 처리는 다음과 같은 이유로 안전할 수 없습니다.

  1. 코드 접근성: 모든 클라이언트 측 코드(JavaScript 등)는 사용자가 열람하고 수정할 수 있습니다.
  2. 복호화 로직 노출: 복호화가 클라이언트에서 이루어지기 때문에 이를 분석하여 원본 데이터를 복구할 수 있습니다.
  3. 변수 조작 가능성: 복호화 성공 여부로 비밀번호 값을 평가하기 때문에, 복호화된 정보를 적절한 변수에 대입하는 것으로 보안 처리를 우회할 수 있었습니다.

앞서 보여드린 영상이 그 증거입니다. 저처럼 보안 전문가가 아닌, 평범한 프론트엔드 개발자도 쉽게 우회할 수 있습니다.

제가 웹 개발을 시작했을 때 가장 먼저 배운 원칙 중 하나는 "클라이언트에서 오는 모든 정보는 신뢰할 수 없다"였습니다.
같은 맥락에서 우리는 "클라이언트에서 이루어지는 모든 보안 처리는 깨질 수 있다"고 생각해야 합니다.

4. 왜 이 문제가 중요한가

이 문제가 특히 걱정되는 이유는 몇몇 사용자들이 '보안 메일'이라는 이름을 보고 실제로 안전하다고 생각할 수 있다는 점입니다. 파일의 보안을 신뢰한 나머지 클라우드 스토리지나 공유 PC 등 안전하지 않은 환경에 보관할 수 있습니다. 이 경우 파일에 접근하는 누구나 내용을 확인할 수 있는 상태가 됩니다.

제가 이 글을 쓰게 된 이유도 여기에 있습니다. 단순히 취약점을 지적하기 위해서가 아닙니다. 중요한 정보가 담긴 '보안 메일'을 맹신하지 않도록 알려야 한다는 생각이 들었기 때문입니다.

5. 맺으며

개발자로서 우리는 '보안'이라는 이름을 사용할 때 당연히 그에 걸맞은 실질적인 보호 조치를 제공하려고 노력하고 있을 것입니다.
하지만 동시에 다른 서비스를 이용하는 사용자로서의 우리는, 서비스에 제공되는 '보안' 장치가 정말로 안전한지 항상 의문을 가질 필요가 있습니다.

이번 사례를 통해 여러분께 클라이언트 측 보안의 한계를 구체적으로 보여줄 수 있었기를 바랍니다.

다음 사항을 꼭!! 염두에 두시기 바랍니다:

  1. 서비스 제공자의 보안 처리를 무조건 신뢰하지 마세요. 특히 클라이언트 측 보안은 완전하지 않습니다!
  2. 중요한 개인정보가 담긴 문서는 안전한 환경에 보관하세요.
  3. 가능하다면 중요 문서는 열람 후 즉시 삭제하거나 별도 암호화하여 보관하세요.

직장 동료분이 소개해주신 흥미로운 자료를 공유하며 글을 마무리하겠습니다.
(Thanks to Log 🥹)

Adblock Plus 개발자로 유명한 블라디미르 팔란트(Wladimir Palant)는 한국 금융권의 다양한 "보안 소프트웨어"에 대한 취약점 분석을 공개했습니다.

원문은 영어로 작성되었지만 링크에서 한국어로 번역본을 읽을 수 있습니다. 이 글에서 다룬 것보다 훨씬 심각한 문제들을 다루고 있어 보안에 관심이 있다면 한번 살펴보시면 좋을 것 같습니다.

긴 글 읽어주셔서 감사합니다. 여러분의 소중한 개인정보가 항상 안전하게 보호되길 바랍니다!

profile
3년차 프론트엔드 중에서 제일 잘 칩니다

7개의 댓글

comment-user-thumbnail
2025년 3월 19일

와… 버그 바운티라니 대단하십니다...
저는 아직 개발만으로 벅찬데요 ㅋㅋㅋㅋ

1개의 답글
comment-user-thumbnail
2025년 3월 21일

짱 신기하네요..

1개의 답글
comment-user-thumbnail
2025년 3월 23일

취약점이 저렇게 명백한데 그것도 금융회사가 참 안일하네요.

오래전 보안 트랜드는 클라였고 그 후 클라가 아닌 서버로 그리고 지금은 아무것도 믿지 말고 검증하라는 Zero Trust 원칙으로 발전하고 있습니다.

아무리 제로트러스트라 하더라도 결국 사회공학공격에 깨지더라구요. 이제 보안은 영역 구분이 없습니다. 클라도 보안에 경각심을 가지고 관리해야 합니다.

하물며 저도 은행 업무는 무조건 lte 사용합니다.
WPA3도 뚫리는 사례가 발생하기에 wifi도 불안합니다. 참고로 국내는 WPA2가 가장 일반적입니다..
작정하고 스니핑하면 카페 같은 곳은 다 관제되어버립니다.

그나마 LTE는 RW라 스니핑에 좀 더 안전하죠.

1개의 답글