상세 컨텐츠

본문 제목

[보안 공부] 3주 차 - 로그인 구현, ID & PW 없이 쿠키로 로그인해 보기

컴퓨터/보안 수업(Segfault)

by 디멘터 2024. 5. 5. 04:40

본문

※ 주의 : 보안 관련 학습은 자유이지만, 학습 내용을 사용한 경우 법적 책임은 행위자에게 있습니다.
(Note: Security-related learning is allowed, but the individual is solely responsible for any legal consequences arising from the use of the acquired knowledge.)

※ 공부 기록용 포스트이며, 검수를 하고 올리지만 간혹 잘 못된 정보가 있을 수도 있습니다.

※ 보안에 취약한 코드이므로 사용하시면 안 됩니다.

 
[보안 공부] 3주 차 - 로그인 구현, ID & PW 없이 쿠키로 로그인해 보기


1. 개념(로그인, 개인정보, 암호화)

● 식별, 인증, 인가

식별(identification) : 내가 누구인지를 알림
→ 인증(Authenticiation) : 신원을 확인하는 과정
인가(Authorization) : 권한 부여
 
일반적으로 로그인에서, 식별과 인증은 ID + PW로 동시에 이루어진다.
로그인이 되면(인증이 되면), 그에 맞는 권한을 인가한다.
 인증의 방식에는 세션 방식과, 토큰 방식이 있다.


● 로그인의 인증 방식 - 세션ID or 토큰을 ☞ 쿠키에 저장.

▶HTTP 통신에서, 원래는 로그인 상태가 지속되지 않는다.
→ 기본적으로 HTTP 통신은 무상태성(Stateless)과 비연결성(Connectionless)이라는 특징을 갖고 있기 때문에, 로그인 상태 지속이 불가능하다.
무상태성 : 클라이언트가 1을 요청하고, 다음 페이지에서 2를 요청해도 서버는 1, 2를 같은 클라이언트가 요청했는지 모른다.
비연결성 : 클라이언트가 서버에 자원을 요청하면, 서버는 HTML를 만들어서 제공하면 끝이다. 연결이 지속되지 않고 끊긴다.
그러므로 지속적으로 로그인되어 있는지 상태를 알 수가 없다.
 
▶로그인 상태를 지속하기 위한 방식 - 세션과 토큰
→ 세션(Session) : 로그인 과정에서 사용자가 성공적으로 인증이 되면, 서버는 각 세션을 구별하기 위해서 고유한 세션 ID를 생성한다. 이 세션 ID는 서버에 저장되고, 사용자에게도 쿠키 형태로 전달된다. 사용자는 이후 모든 HTTP 요청에서 이 세션 ID를 포함시켜 서버로 전송하고, 서버는 로그인 상태를 유지시켜 준다.
토큰(Token) : 인증을 위해 서버가 발행하는 인증 정보가 담긴 서명된 문자열, 서버가 생성하지만 서버에 저장하지 않고, 사용자에게 전달하여 사용자가 보관. 사용자는 토큰을 서버에 전송하므로 인증. 토큰은 인증과 권한 정보를 포함하고 있으며 주로 JWT(JSON Web Token) 형식을 사용.
 
▶JWT(JSON Web Token)
→ 일반 토큰과 다른 점 : 다른 점이라고 하기에는 비교 대상인 '일반 토큰'을 찾기가 어려웠다. ☞ 일상생활에서 찾아보자면 위조가 어렵고 제시만으로 인정될 정도의 여권 같은 신분증이라고 할 수 있을 것 같다. 다른 점은 규격화되어 있고, 전자 서명이 들어간다는 점.
JWT 특징 : 작은 크기, 전자 서명을 통한 인증, 범용성(표준화), 대칭키 or 비대칭키를 사용한 서명
→ 구조 : Header.Payload.Signature
라이브러리를 통해 간단하게 구현 및 사용이 가능하다.
 
JWT 생성 코드 (라이브러리 미사용)

더보기
<!--makejwt.php-->
<?php

require('jwt_key.php');//jwt 생성하는 비밀키를 $key 변수로 갖고 있음

// JWT 생성 함수
function generateJWT($payload, $key) {
    // 헤더 생성
    $header = ['alg' => 'HS256', 'typ' => 'JWT'];
    $header_json = json_encode($header);
    $header_base64 = base64UrlEncode($header_json);

    // 페이로드 생성
    $payload_json = json_encode($payload);
    $payload_base64 = base64UrlEncode($payload_json);

    // 서명 생성
    $signature = hash_hmac('sha256', "$header_base64.$payload_base64", $key, true);
    $signature_base64 = base64UrlEncode($signature);

    // JWT 생성
    return "$header_base64.$payload_base64.$signature_base64";
}

// base64 URL 인코딩 함수
function base64UrlEncode($data) {
    return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
}

// JWT 생성
$loginid = $_POST['loginid'];

//$key = 'your_secret_key'; // 서버가 갖고 있는 비밀 키인데 jwt_key.php에서 가져오는 것으로..
$payload = ['userId' => "$loginid", 'email' => 'user@example.com', 'exp' => strtotime('+1 hour')];
$jwt = generateJWT($payload, $key);

// JWT를 클라이언트에게 전달 (쿠키로)
setcookie('jwt', $jwt, time() + (3600), "/"); // 쿠키 만료 시간: 1시간 후
?>

 

→ 코드를 보면 헤더, 페이로드, 서명을 생성하는 것을 알 수 있다.
base64로 인코딩하는 것을 알 수 있다.(암호화가 아니다.)
base64는 0~25 : A~Z, 26~51 : a~z, 52~61 : 0~9, 62 : + 63 : / 인 단순한 규격이다.
php코드이기 때문에 서버 측에서 실행하는 코드로, your_secret_key는 서버 측에서 보관하고 있는 비밀키이다.
key는 서버 측에서 갖고 있는 비밀키가 된다.
 
JWT header 분석
→ 단순 배열로써 알고리즘, 타입이라는 속성과 그 속성에 해당하는 값으로 쌍이 이루어져 있다.
→ 특별한 것은 없다.
 
 JWT payload 분석
→ 유저의 정보가 들어가는데, 유효기간을 설정할 수 있다.(옵션)
유효기간 부분은 유닉스 시간이라서 매 초마다 생성되는 signature 값이 변한다.
 
 JWT signature 부분 분석
→ 핵심부이다. 서버가 갖고 있는 비밀키를 통해 생성한다.
만약 서버에서 이 비밀 key가 유출되면, 토큰을 발행해 모든 유저 ID로 로그인이 가능할 것으로 보인다.
 
JWT 검증 코드 (라이브러리 미사용)

더보기

<!--logined_check_jwt.php-->

<?php

require('jwt_key.php');//jwt 생성하는 비밀키를 $key 변수로 갖고 있음

// JWT가 쿠키에 있는 경우
if (isset($_COOKIE['jwt'])) {
    $jwt = $_COOKIE['jwt'];
   
    // JWT 분해
    $tokenParts = explode('.', $jwt);
    if (count($tokenParts) === 3) {
        $header = base64_decode($tokenParts[0]);
        $payload = base64_decode($tokenParts[1]);
        $signatureProvided = $tokenParts[2];
        $signatureProvideddecoded = base64_decode($signatureProvided);
               
        // 서명 검증
        $signature = hash_hmac('sha256', "$tokenParts[0].$tokenParts[1]", $key, true);  // key 중요
        $signatureEncoded = base64UrlEncode($signature); // UrlEncode를 사용하지 않으면 끝에 패딩문자 '=' 때문에 일치하지 않은 결과 생기므로 사용해야 함.
       
        echo '<script>alert("JWT 토큰의 서명 값을 비교 검증을 합니다.\nsignatureEncoded:'.$signatureEncoded.'  signatureProvided:'.$signatureProvided.'");</script>'; // 출력 한 번 해주고
             
        if ($signatureEncoded === $signatureProvided) {
            // JWT 페이로드 파싱
            $payload = json_decode($payload, true);
           
            // 유효 기간 확인
            if (isset($payload['exp']) && $payload['exp'] >= time()) {
                // 사용자 ID 추출 및 세션에 저장
                if (isset($payload['userId'])) {
                    $jwtid = $payload['userId'];
                    echo '<script>alert("jwt토큰 id는 : '. $jwtid.'입니다.");</script>';
                } else {
                    echo '<script>alert("JWT에 사용자 ID가 없음.");</script>';
                }
            } else {
                echo '<script>alert("JWT의 유효 기간이 만료됨.");</script>';
            }
        } else {
           
            echo '<script>alert("JWT의 서명이 올바르지 않음.");</script>';
        }
    } else {
        echo '<script>alert("잘못된 JWT 형식.");</script>';
    }
} else {
    // JWT가 없는 경우
    echo '<script>alert("JWT 토큰이 존재하지 않습니다.");</script>';
    header("refresh:0;url=login.php");
    //exit;
}

function base64UrlEncode($data) {
    return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
}

?>

 

생성된 서명과, 받은 JWT에서 헤더와 페이로드를 디코드 하고, 서버가 가진 비밀키로 SHA-256 알고리즘으로 해싱하여 서명 부분이 일치하는지 검증한다.
 
▶세션과 토큰에서 생기는 의문
→ 토큰은 서버에 저장되어있지 않은데 어떻게 인증이 가능한가? ☞ 전자서명(서버의 비밀키 or 비대칭키 사용)
→ 서버의 부하는 어느 것이 큰가? ☞ 세션 방식이 부하가 큼(세션 ID 저장)
→ JWT 방식은 서버에 부하가 없는가? ☞ NO, 토큰 유효성 검증을 위한 디코딩, 서명(SHA256) 계산이 필요.
 네이버, 카카오는 어떤 방식을 사용할까? ☞ (일반적으로 대규모 사이트는) 토큰방식이라고 한다.
 어느 부분이 취약한가?  ☞ 인증 정보가 담긴 쿠키를 탈취하면 로그인이 가능하다.


● 개인정보 (개인정보 보호법)

 ☞ 특정 인물을 식별할 수 있는 모든 정보(=신상정보) , 다른 정보와 결합하여 개인을 식별할 수 있는 정보 
 
개인정보(개인정보 보호법 제2조 정의)
①성명, 주민등록번호, 및 영상 등을 통하여 개인을 알아볼 수 있는 정보
②다른 정보와 쉽게 결합하여 알아볼 수 있는 정보
③가명정보
 
고유식별정보(개인정보 보호법 시행령 제19조 고유식별정보의 범위)
주민등록번호, 여권번호, 면허번호, 외국인등록번호


● 암호화가 필요한 정보(개인정보 보호법, 정보통신망법)

→ 비밀번호, 바이오 정보, 고유식별 정보, 민감정보


암호화

▶기본 원칙(더 많지만 핵심 몇 개만 가져왔습니다.)
→ 모든 암호는 언젠간 해독된다. ☞ 암호화를 맹신하면 안 된다.
암호화 비용과 해독이 되었을 때 파급력의 밸런스를 고려해야 한다. ☞ 경제성을 따져야 한다. 적당한 레벨의 암호화가 필요하다.
→ 비밀 알고리즘을 사용하지 말라. ☞ 세상에는 똑똑한 사람들이 많다. 공개되고 검증된 암호화 알고리즘을 사용해야 한다.
 
▶암호화 방식 분류
▶ 양방향 / 단방향 암호화
→ 양방향 : 복호화 가능 ☞ 암호화와 복호화를 위한 key가 있다.
→ 단방향 : 복호화 불가능 ☞ 해시 함수 사용
 
양방향 암호화 - 대칭키, 비대칭키
 대칭(Symmetric) 키 : 암호화와 복호화에 같은 Key 사용
대칭키 알고리즘 : DES(현재는 사용 금지 - 해독 쉬움), AES, RC5, ARIA, SEED, HIGHT 등
 
 비대칭(Asymmetric) 키 방식 : 암호화와 복호화에 사용하는 키가 서로 다르다.
비대칭키 알고리즘 : RSA, ECC, ElGamal, DSA 등
 
단방향 암호화 - 해시 함수 사용
→ 단방향 : 복호화 불가능 ☞ 해시 함수 사용
→ 복호화가 불가능하기 때문에 '같은지 확인하는' 무결성 검증에 사용된다.
단방향 알고리즘 : SHA-,MD5 등


2. 실행 - 세션방식 로그인 구현, 비밀번호 SHA 암호화

●PHP에서 세션 관련 함수 https://www.php.net/manual/en/reserved.variables.session.php

→ session_start(); // 세션 시작
 $_SESSION['userid'] = 'kim'; // 세션 변수 userid에 'kim'을 저장한다.

→ session_unset(); // 현재 세션에서 모든 세션 변수를 해제한다.
 session_destroy(); // 세션을 완전히 종료하고 서버에 저장된 세션 데이터를 삭제한다.


●PHP에서 해싱하는 함수 https://www.php.net/manual/en/function.hash.php

hash(string $algo, string $data, bool $binary = false, array $options = []): string
→ hash('sha256', 'The quick brown fox jumped over the lazy dog.'); // 사용 예시


순서도 5월 5일 버전
전투 결과물

 

보안공부 3주차.zip
0.02MB

 
→ 파일이 너무 많아서 압축하였다.
→ 2주 차에서 전체적으로 리모델링하고 새롭게 구현했다. 30시간은 넘게 걸린 것 같다. GPT의 도움을 많이 받았다.
→ GPT가 소스코드를 만들어 주면 배치와 조립만 열심히 했다...
→ 그 와중 JWT 토큰 검증에서 GPT가 큰 실수를, 맞는 것처럼 해서 찾느라 몇 시간이 걸렸다.

더보기
<?php
// JWT가 쿠키에 있는 경우
if (isset($_COOKIE['jwt'])) {
    $jwt = $_COOKIE['jwt'];
   
    // JWT 분해
    $tokenParts = explode('.', $jwt);
    if (count($tokenParts) === 3) {
        $header = base64_decode($tokenParts[0]);
        $payload = base64_decode($tokenParts[1]);
        $signatureProvided = $tokenParts[2];
       
        // 서명 검증
        $signature = hash_hmac('sha256', $tokenParts[0] . '.' . $tokenParts[1], 'your_secret_key', true);
        $signature = base64_encode($signature);
 
    // 이 부분에서 같은 변수에 다르게 넣어버리는 문제가 발생 했다.
    // 그래서, 같은 키로 발행한 JWT를 아무리 검증해도 검증 성공이 안되었다. 
       
        if ($signature === $signatureProvided) {
            // JWT 페이로드 파싱
            $payload = json_decode($payload, true);
           
            // 유효 기간 확인
 
--중략--

JWT에서 검증 될 두 값을 눈으로 볼 수 있게 팝업을 띄웠다.
JWT에 들어가 있는 id를 팝업으로 띄웠다.

 

세션로그인 시, 세션에 들어가 있는 유저 ID정보를 팝업으로 띄웠다.

 
 
 


● 스스로 리뷰

▶ ID와 PW 분리 인증은 DB에서 ID를 먼저 찾고 있으면 PW를 비교하는 방식을 썼다. ID가 존재하면 아래처럼 alert가 뜨고, DB에서 PW 확인 작업을 거친다.

ID, PW 분리 인증 확인용, ID가 존재한다고 너무 친절하게 알려준다.

 
▶ 로그인과 회원가입 시 패스워드를 HASH함수로 처리하긴 했는데, 패스워드를 서버가 넘겨받을 때까지는 POST로 구현해서 평문으로 날아오는 취약점이 존재한다. 사용자가 입력하고 서버에 넘기기 전에 HASH함수로 암호화하려면 자바스크립트를 써야 한다고 하는데 아직 잘 모른다. (더불어 HTTPS 사용이 필요할 것 같다.)

DB에는 해싱된 암호가 들어간다.

 
▶ 세션 ID가 실제로 서버 측에 저장되는 것인지 확인해 보았다.

서버측에 저장되는 세션ID가 확인 된다. (xampp사용) 로그아웃을 하면 파일의 내용이 파괴 된다. 그런데 6KB는 뭐지..?

 

로그아웃 전 세션ID에는 이런 변수가 들어가 있었다.

 
▶ JWT랑 세션을 동시에 사용하다 보니, 로그인 체크에서 뭔가 약간 꼬였었는데 억지로 풀었다. 세션은 세션만 사용하는 것으로, JWT 토큰은 토큰만 사용하는 방식으로 구현하는 것이 맞을 것 같은데, 토큰 방식인데도 GPT로 코드를 짤 때, 자꾸 session_start를 넣던데 이해가 잘 안 되었다. >> 나중에 다시 공부
 
▶ 자바스크립트랑, 로그아웃에 대한 이해가 아직 부족하다. 자바스크립트에 대해 더 공부해야 하고, 로그아웃을 누르면 세션도 파괴하고 JWT 쿠키도 무조건 파괴하도록 중복해서 짜놨다. 처음에는 폴더에 세션 ID가 남아있어서 제대로 파괴 안 되는 줄 알았는데 알고 보니 파일 안의 내용이 삭제되는 것이었다.
 
▶ 일단 JWT의 약점을 파헤쳐보면, 평문도 알고 있고, 해싱된 해시값도 알고 있으니 '알려진 평문공격'을 시도하는 것 정도는 가능할 것 같다.(하지만, 현재 컴퓨팅 파워로 SHA-256을 풀 수는 없을 것 같다. 전자서명에 유닉스 시간이라는 소금도 낭낭하게 쳐져 있으니 레인보우 테이블도 안 통할 것 같다.) 서버 측 비밀키에 의존하게 되는데 비밀키 관리를 잘해야 할 것 같다.
 
▶ 클라이언트는 쿠키가 탈취당하지 않도록 주의해야겠다. 몇 KB도 안되는 것에 매우 중요한 정보가 담겨 있다.
 


3. 실행 - ID & PW 없이 쿠키를 이용해서 로그인해 보기(네이버)

 
● 쿠키가 인증 정보라는 상당히 중요한 정보를 갖고 있는 것을 알았으니 사용해서 로그인을 시도해 보자.
 
→ 타인의 쿠키로 해 볼 수는 없으니 내 아이디로 해본다..^^
→ 구현 방법 : chrome에서 쿠키를 가져와서 Edge 브라우저에 id, pw 없이 로그인 성공하기!
크롬에서 쿠키를 찾는다. 변수명이 'NID'인 것 보니 네이버 ID인 것 같다.
'AUT'는 Authentication 또는 Authorization 인 것 같고. 'SES'는 Session인 것 같다.
 

로그인하면, 인증과 관련되어 보이는 값들이 보인다.
엣지를 켜서 쿠키를 지운 뒤 위에서 본, 두 값을 복사해서 붙여넣어 준다.(변수명, 값을 각각 붙여 넣어야 한다.)

 

로그인이 된다..!?

 
 

 
로그인이 되는 듯해 보였으나, 역시.. 보안이 철저했다.
메일 제목, 가입 카페 목록 정도는 볼 수 있었으나 클릭하면 로그인창이 떴다..ㅋㅋ (다행)
 

4. 평가

→ PHP기초 강의는 한 번 완료했는데.. 아직 갈 길이 먼 것 같다. 프론트앤드 공부를 더 하자.
→ 대부분 GPT가 구현한 것들이라서, 다시 짜라고 하면 못 짜는 상태이다. (다행히 절차는 어느 정도 이해가 돼서.. 조립하는 것은 가능하다.) 일단 만들어진 것들을 이해하고 흡수하는데 목표를 두자.
 표준과 로직을 만든 선구자들은 대단한 것 같다. 덕분에 개발, 창조하지 않아도 되고 배우기만 해서 사용할 수 있는 점은 정말 땡큐 하다.
 

● 더 생각해 보기

인증 방식에는 무엇이 있을까? 아마 수업에서 던진 질문은 해킹과 방어에 있어서 취약점 공부를 위한, 로그인 인증 절차의 '로직'에 관한 질문이었을 텐데 필자는 현재 아는 바가 없다.. 백지다.. 깨끗하다..
→ 하지만, '인증' 방법에 대해서는 몇 가지를 알고 있고, 추가로 쥐어짜 보았다.

더보기

인증 방법

1. ID&PW 인증
2. 세션, 토큰 방식 인증
3. 공인 인증서 사용 (비대칭 키, 서명)
4. OAuth 인증(타 플랫폼으로부터 인증)
5. 물리적 보안 토큰 인증 (스마트카드, 티켓, USB 등)
6. 미리 발행된 복구코드 입력
7. OTP 인증 (=비슷 일회용 QR코드 인증)
8. 바이오 인증 중에 갖고 있는 것 인증 (지문, 홍채, 안면인식, DNA, 지정맥)
9. 바이오 인증 중에 행위기반 인증 (서명, 음성, 움직임)
10. 위치기반 인증 (안전한 장소)
11. 이메일 인증 (=비슷 계좌번호 입금자명 확인)
12. 블록체인 인증
14. MAC주소 인증 (NAC)
15. 휴대폰 인증 (푸시알람, ARS, 문자메시지)
16. I-PIN 인증 (한국 한정)

 

현재도 많이 쓰이는 인증 방식이고,

최근 5년 이내라면, 위치기반, 블록체인 방식 빼고는 직접 사용해 보았다.

의문) SSO는 OAuth의 일종이라 봐야 하는가..? 개념이 살짝 뭔가 다른 것 같다.

관련글 더보기