보안

[보안] SQL Injection 실습

ssh9308 2024. 4. 22. 15:22
반응형

 

SQL Injection 이란?

 

SQL Injection은 보안 취약점을 이용한 공격 방식 중 하나로, 

악의적인 사용자가 웹 애플리케이션의 입력 필드에 SQL 구문을 삽입하여 

 

백엔드 데이터베이스에 영향을 주는 기술이다. 

이를 통해 공격자는 데이터베이스에서 데이터를 조회, 수정, 삭제할 수 있으며, 

 

경우에 따라서는 데이터베이스 서버의 제어권을 획득할 수도 있다.

 

 

 

 

작동 원리

 

웹 애플리케이션은 사용자로부터 입력받은 데이터를 SQL 쿼리에 활용하여 데이터베이스와 통신한다. 

 

SQL Injection은 이러한 과정에서 사용자 입력을 제대로 검증하거나 처리하지 않았을 때 발생한다. 

 

공격자는 특별히 조작된 입력값을 통해 의도치 않은 SQL 명령을 실행하도록 만들 수 있다.

 

 

 

 

예시

 

SQL Injection의 예시를 위해서,

 

React, RUST, MySQL 을 사용한다.

 

MySQL에서 관리자 및 유저의 정보는 USER_TBL_TEST에 있다.

 

SELECT * FROM USER_TBL_TEST;

 

보다시피 유저들의 비밀번호는 모두 해쉬화작업이 되어있다.

 

여기서 admin이라는 user_id 계정이 해당 시스템을 총괄하는 계정이라고 가정해 보자.

 

공격자는 이 admin 계정을 탈취하려고 시도할 것이다.

 

 

리액트로 간단히 로그인화면을 구성하였다.

 

Username에 유저의 아이디, Password에 유저의 비밀번호를 넣어서,

 

jwt를 가져와서 인증하는 방식이다.

 

만약 RUST 내부(백엔드)에 아래와 같은 코드로 유저의 로그인을 검증해 준다고 해보자.

 

[RUST]

async fn find_user_infos_injection(&self, user_id_str: &str, user_pw_str: &str) -> Result<Vec<UserPostData>, anyhow::Error> {

    let query = format!("SELECT user_seq, user_id, user_name FROM user_tbl_test WHERE user_id = '{}' AND user_pw = '{}'", user_id_str, user_pw_str);

    let res: Vec<UserPostData> = self.mysql_client.query_select_from(&query).await?;

    Ok(res)
}

 

위와 같은 검증방식은 대단히 잘못된 방식이다.

 

위와 같이 검증했을 때 어떠한 보안 취약점이 나올 수 있는지 확인해 보자.

 

 

대부분 로그인할 때의 쿼리로직은 아래와 같다.

 

SELECT * FROM USER_TBL_TEST WHERE user_id = 'user1' AND user_pw = '123456';

 

아이디와 패스워드에 해당하는 유저의 정보를 가져와서,

 

백엔드에서 처리해 주는 게 일반적이다.

 

하지만 아래와 같은 쿼리가 입력되면 어떻게 될까?

 

SELECT * FROM USER_TBL_TEST WHERE user_id = 'admin'; -- AND user_pw = '123456';

 

놀랍게도, AND 절 뒤는 무시되어, 바로 admin 계정의 데이터가 나오게 된다.

 

 

 

위와 같이 아이디에 admin을 넣고 의미 없는 값을 비밀번호로 기입하였을 때에는

 

당연히 로그인 실패가 될 것이다.

 

하지만 아래와 같이 기입을 해보자.

 

 

 

 

 

놀랍게도 어떤 비밀번호를 넣었던지 상관없이,

 

admin 계정을 탈취할 수 있다.

 

위의 예제는 극단적인 예제는 하지만, 이는 많은 점을 시사한다.

 

그렇다면, SQL Injection을 방어하는 가장 좋은 방법은 무엇일까?

 

 

 

 

SQL Injection 방어 방법

 

SQL Injection 을 방어하는 방법은 대표적으로 3가지가 존재한다.

 

1) 입력값 검증 및 이스케이프

 

사용자 입력값을 안전하게 처리하여 SQL 쿼리에 사용하기 전에 적절히 이스케이프 하는 방법이 존재한다.

 

예를 들어 입력되는 아이디에 ; , -- 이런 식으로  Sql injection에 사용될 수 있는 문자열 자체를 이스케이프 하는 것이다.

 

 

2) 준비된 문장(Prepared Statements) 사용

 

변수를 쿼리에 직접 삽입하는 것이 아니라, 파라미터로 전달하여 SQL 쿼리를 실행한다.

 

이 방식을 사용하면, 데이터베이스가 파라미터를 데이터로만 인식하기 때문에 SQL 코드로 인식되지 않는다.

 

현재 많은 서비스들이 이 방법을 사용하여 SQL Injection을 방어하고 있다.

 

[RUST]

async fn find_user_infos_injection(&self, user_id_str: &str, user_pw_str: &str) -> Result<Vec<UserPostData>, anyhow::Error> {

        let query = r"
            SELECT 
                user_seq, 
                user_id, 
                user_name 
            FROM user_tbl_test
            WHERE user_id = ?
            AND user_pw = ?
        ";

        let res: Vec<UserPostData> = self.mysql_client.query_select_from_param(query, (user_id_str, user_pw_str)).await?;
        
        Ok(res)
    }

 

예를 들어 위와 같이 Prepared Statement 형식으로 로직을 구성하게 되면,

 

SQL Injection 자체가 불가능해진다.

 

 

3) ORM(Object-Relational Mapping) 사용

 

SQL 쿼리를 직접 작성하는 대신 ORM을 사용하여 데이터베이스 작업을 수행한다.

 

ORM은 내부적으로 SQL Injection을 방지하는 메커니즘을 갖추고 있다.

async fn find_user_infos_injection(&self, user_id_str: &str, user_pw_str: &str) -> Result<Vec<UserPostData>, anyhow::Error> {


    let mut conn: diesel::r2d2::PooledConnection<ConnectionManager<MysqlConnection>> = self.diesel_client.get_conn_from_diesel_conn().await?;

    let user_infos = (user_id_str.to_string(), user_pw_str.to_string());

    tokio::task::spawn_blocking(move || {

        let user_id_moved = user_infos.0;
        let user_pw_moved = user_infos.1;

        user_tbl_test
                .filter(user_id.eq(user_id_moved))
                .filter(user_pw.eq(user_pw_moved))
                .select((user_seq, user_id, user_name))
                .load::<UserPostData>(&mut conn)
                .map_err(anyhow::Error::from)

    })
    .await
    .map_err(|e: task::JoinError| anyhow::Error::new(e))?

}

 

 

반응형