가이드라인

SQL 주입

이제 SQL 인젝션에 대해 살펴볼 시간입니다. SQL 인젝션은 오랜 기간 동안 OWASP 상위 10위권에서 부동의 1위를 차지했습니다. 20년이 넘은 오래된 취약점이고 이 목록의 상위권에서 약간 떨어지긴 했지만, 여전히 매우 인기 있고 위험한 취약점입니다. 

웹 보안 취약점 중 하나인 SQL 인젝션(SQLi)은 공격자가 데이터베이스를 조작하고 중요한 정보를 추출할 수 있기 때문에 여전히 가장 흔하게 사용되는 '해킹' 기법 중 하나입니다. 더욱 놀라운 점은 공격자가 데이터베이스 서버의 관리자가 되어 데이터베이스를 파괴하고, 트랜잭션을 조작하고, 데이터를 공개하고, 더 많은 문제에 취약하게 만드는 등 정말 파괴적인 일을 할 수 있다는 것입니다.

어떻게 발생하는지 간단히 살펴보겠습니다.

SQL(또는 구조화된 쿼리 언어)은 관계형 데이터베이스와 통신하는 데 사용되는 언어로, 개발자, 데이터베이스 관리자 및 애플리케이션이 매일 생성되는 방대한 양의 데이터를 관리하기 위해 사용하는 쿼리 언어입니다.

애플리케이션 내에는 데이터 컨텍스트와 코드 컨텍스트 두 가지가 존재합니다. 코드 컨텍스트는 컴퓨터에게 실행할 내용을 알려주고 처리할 데이터와 분리합니다. SQL 인젝션은 공격자가 SQL 인터프리터가 코드로 잘못 취급하는 데이터를 입력하여 애플리케이션에서 중요한 정보를 수집할 수 있을 때 발생합니다. 

SQL 인젝션 공격의 영향

SQL 인젝션은 모든 웹 애플리케이션에 매우 해로울 수 있으며, 공격자가 중요한 데이터에 무단으로 액세스할 수 있기 때문에 많은 유명 침해 사고의 배후에서 선호되는 기법입니다. 공격자는 사용자 이름과 비밀번호부터 신용카드 정보, 개인 식별 번호에 이르기까지 수많은 정보를 볼 수 있습니다. 

공격자는 이 데이터에 액세스한 후 계정을 탈취하고, 비밀번호를 재설정하고, 온라인 쇼핑을 계속하거나 다른 (훨씬 더 심각한) 유형의 사기를 저지를 수 있습니다. 

그러나 SQLi의 가장 우려스러운 점은 공격자가 탐지되지 않는다면 시스템에 백도어를 오랫동안 유지할 수 있다는 점입니다. 백도어가 열려 있는 기간 동안 데이터 유출이 반복될 수 있다는 것은 상상할 수 있습니다. 무서운 일이죠. 

실제 작동 방식을 더 잘 이해하기 위해 몇 가지 예를 살펴보겠습니다.

SQLi 예제

SQLi에는 다양한 상황에 대처할 수 있는 다양한 취약점 기법이 포함되어 있습니다. 다음은 가장 일반적인 SQLi 예시 중 일부에 불과합니다:

기술 설명
숨겨진 데이터 검색 이 기법을 사용하면 공격자는 모든 SQL 쿼리를 수정하여 데이터베이스에서 더 많은 정보를 수집할 수 있습니다.
데이터 검사 공격자는 데이터베이스의 버전과 구조에 대한 정보를 추출하여 추가 정보를 악용할 수 있습니다. 이 기법은 데이터베이스마다 다를 수 있습니다.
연합 공격 공격자는 데이터베이스의 버전과 구조에 대한 정보를 추출하여 추가 정보를 악용할 수 있습니다. 이 기법은 데이터베이스마다 다를 수 있습니다.
블라인드 SQLi 블라인드 SQLi를 사용하면 공격자는 데이터베이스에서 쿼리를 구현할 수 있습니다. 문제는 공격자가 이 쿼리를 제어하고 애플리케이션의 응답에 어떠한 결과도 반환하지 않는다는 점입니다.
애플리케이션 로직 파괴 공격자는 애플리케이션의 논리를 방해하기 위해 쿼리를 방해하거나 조작합니다. 공격자는 쿼리를 조작하기 위해 SQL 주석 시퀀스 "--"과 WHERE 절을 결합할 수 있습니다.

SQLi 유형

이제 세 가지 SQLi 유형을 살펴보겠습니다. 

대역 내 SQLi

이는 가장 일반적이고 간단하며 효율적인 SQL 인젝션 유형 중 하나입니다. 이 유형의 공격에서는 동일한 통신 채널을 사용하여 공격하고 결과를 검색합니다.

다음은 대역 내 SQLi 공격의 두 가지 유형입니다:

  • 유니온 기반 SQLi - 유니온 기반 공격은 유니온 연산자를 사용하여 SELECT 문과 같은 두 개 이상의 SQL 쿼리를 결합하여 원하는 정보를 얻고 그 결과 HTTP GET 응답을 생성합니다.
  • 오류 기반SQLi - 공격자는 데이터베이스의 오류 메시지를 활용하여 데이터베이스의 구조를 파악합니다. 이 공격에서 공격자는 잘못된 요청을 보내거나 서버가 오류 메시지를 표시하도록 하는 작업을 수행하여 데이터베이스 정보를 수신할 수 있습니다. 따라서 개발자는 라이브 환경에서 오류 또는 로그 메시지를 보내지 말고 액세스가 제한된 상태로 저장하는 것이 중요합니다.

추론 SQLi

추론형 또는 블라인드 SQLi 공격은 더 복잡하고 악용하는 데 더 많은 시간이 걸릴 수 있습니다. 게다가 공격자는 실제로 공격 결과를 바로 알 수 없기 때문에 블라인드 공격이라고 할 수 있습니다. 

공격자는 HTTP 요청을 통해 페이로드를 데이터베이스 서버로 전송하여 사용자의 데이터베이스를 재구성한 다음 애플리케이션의 응답과 동작을 관찰하여 공격 성공 여부를 확인합니다. 

다음은 두 가지 유형의 추론형 SQLi 공격입니다:

  • 부울 기반 블라인드 SQLi - 이 공격에서는 데이터베이스에 쿼리를 보내 부울(참 또는 거짓) 결과를 얻고, 공격자는 HTTP 응답을 관찰하여 부울 결과를 예측합니다.
  • 시간 기반 블라인드 SQLi - 이 공격에서는 공격자가 데이터베이스에 쿼리를 보내 응답을 보내기 전에 몇 초 동안 기다리게 하고, 공격자는 HTTP 요청의 응답 시간으로 쿼리 결과를 판단합니다.

대역 외 SQLi

이 공격은 데이터베이스 서버의 활성화된 기능에 따라 달라지는 좀 더 드문 유형의 SQLi 공격입니다. 공격자가 다른 공격 유형을 실제로 사용할 수 없는 경우에 발생합니다.

예를 들어, 인밴드 공격에 동일한 통신 채널을 사용할 수 없거나 HTTP 응답이 쿼리 결과를 파악할 수 있을 만큼 명확하지 않은 경우입니다.
또한, 필요한 데이터를 공격자에게 전송하기 위해 데이터베이스 서버의 HTTP 또는 DNS 요청 기능에 크게 의존하기 때문에 흔한 공격은 아닙니다.

SQLi를 방어하는 방법

다행히도 SQL 인젝션은 워낙 오래되고 흔한 공격이기 때문에 이를 방지할 수 있는 방법이 있다는 점이 다행입니다. 이러한 종류의 예방 기술을 사용하는 것은 좋은 코딩 습관일 뿐만 아니라 SQLi에 대한 조직의 보안을 실제로 강화할 수 있습니다. 

이러한 종류의 공격으로부터 데이터베이스 서버를 보호하는 방법에는 입력 유효성 검사, 웹 애플리케이션 방화벽(WAF) 사용, 데이터베이스 보안, 타사 보안팀 또는 시스템 사용, 완벽한 SQL 쿼리 작성 등 여러 가지가 있습니다.

위에서 언급한 보안 조치 중 하나를 사용하여 Python에서 SQL 인젝션을 방지하는 예를 살펴보겠습니다.

파이썬 예제

이 예제에서 공격자는 시스템에서 중요한 정보를 얻기 위해 부울 기반 블라인드 SQL 인젝션을 사용합니다. 

Python: 취약성

데이터베이스에 "sample_data"라는 테이블이 있다고 가정합니다. 이 테이블에는 애플리케이션 사용자의 사용자 이름과 비밀번호가 저장됩니다. 

이제 사용자가 다음 명령을 사용하여 이 데이터베이스 테이블에서 값을 찾을 수 있습니다:

import mysql.connector
db = mysql.connector.connect
#Bad Practice. 피하세요! 이것은 단지 학습용입니다.
(host="localhost", user="newuser", passwd="pass", db="sample")
cur = db.cursor()
name = raw_input('Enter Name: ')
cur.execute("SELECT * FROM sample_data WHERE Name = '%s';" % name) for row in cur.fetchall(): print(row)
db.close()

SQL 주입

여기서 사용자가 검색에 이름(예: Alicia)을 입력해도 출력에는 아무런 문제가 없습니다. 

그러나 사용자가 다음과 같이 입력하면 데이터베이스에 큰 영향을 미칩니다.

Python: 수정

공격이 발생하지 않도록 SQL 문을 다음과 같이 변경해야 합니다:

cur.execute("SELECT * FROM sample_data WHERE Name = %s;", (name,))

이제 시스템이 사용자 입력을 문자열로 취급하며, 사용자가 SQL 쿼리를 삽입하려고 해도 사용자 입력을 이름 값으로만 취급합니다. 

이 간단한 변경으로 향후 쿼리에서 악의적인 활동을 방지하고 사용자 입력 공격으로부터 시스템을 보호할 수 있습니다.

Java 예제

이 예제에서는 애플리케이션의 사용자 데이터를 저장하는 "sample_data"라는 데이터베이스 테이블도 사용합니다. 

기본 로그인 페이지에는 사용자 이름과 비밀번호가 필요하며, 서블릿(LoginServlet)인 자바 파일은 데이터베이스에 대해 유효성을 검사하여 로그인 작업을 허용합니다. 

Java: 취약한 예제 

데이터베이스의 'sample_data' 테이블을 사용하여 사용자가 자신의 자격 증명을 입력으로 받아 로그인 작업을 수행할 수 있도록 합니다.

로그인 작업을 수용하기 위한 쿼리가 LoginServlet 파일에 있습니다:

//Bad Example. Do not use string concatenation.
String query = "select * from sample_data where username='" + username + "' and password = '" + password + "'";
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
            stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(query);
            if (rs.next()) {
                // Login Successful if match is found
                success = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                stmt.close();
                conn.close();
            } catch (Exception e) {}
        }
        if (success) {
            response.sendRedirect("home.html");
        } else {
            response.sendRedirect("login.html?error=1");
        }
    }

다음은 사용자 로그인을 위한 쿼리입니다:

sample_data에서 * select * where username='username', password='password'

SQL 주입

입력이 유효하면 시스템이 완벽하게 작동합니다. 예를 들어 사용자 아이디가 Alicia이고 비밀번호가 비밀이라고 가정해 보겠습니다. 

시스템은 이러한 자격 증명을 가진 사용자의 데이터를 반환합니다. 그러나 공격자는 SQL 인젝션을 위해 Postman과 cURL을 사용하여 사용자 요청을 조작할 수 있습니다. 

예를 들어 해커는 더미 사용자 아이디(Alicia)와 비밀번호 '또는 '1'='1'을 보낼 수 있습니다. 

이 경우 사용자 아이디와 비밀번호가 일치하지 않지만 '1'='1' 조건은 항상 참이므로 로그인 작업이 성공합니다.

Java: 예방

이를 방지하려면 LoginValidation 코드를 수정하고 쿼리 실행에 Statement 대신 PreparedStatement를 사용해야 합니다. 이렇게 변경하면 쿼리에서 사용자 이름과 비밀번호를 연결하지 않고 세터 데이터로 처리하여 SQL 인젝션을 방지할 수 있습니다. 

로그인 유효성 검사의 수정된 코드는 아래와 같습니다:

String query = "select * from sample_data where username=? and password = ?";
Connection conn = null;
PreparedStatement stmt = null;
try {
    conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
    stmt = conn.prepareStatement(query);
    stmt.setString(1, username);
    stmt.setString(2, password);
    ResultSet rs = stmt.executeQuery();
    if (rs.next()) {
       success = true;
       }
      rs.close();
      } catch (Exception e) {
         e.printStackTrace();
         } finally {
             try {
                 stmt.close();
                 conn.close();
            } catch (Exception e) {
            }
         }

이 경우 준비된 문, 설정자 및 기본 JDBC API가 사용자 입력을 처리하고 SQL 인젝션을 방지합니다.

예제

이제 다양한 언어로 된 몇 가지 예시를 더 살펴보고 실제로 어떻게 작동하는지 더 잘 이해해 보겠습니다.

C# - 안전하지 않음

이 예제는 `FromRawSql`을 사용하기 때문에 안전하지 않습니다. 이 메서드는 매개변수를 바인딩하거나 이스케이프를 시도하지 않습니다. 따라서 이 메서드는 어떤 대가를 치르더라도 피해야 합니다.

var blogs = context.Posts
    .FromRawSql("SELECT * FROM Posts WHERE state = {0} AND author = {1}", state, author)
    .ToList();

C# - 보안

이 예제는 보간된 값을 가져와 매개변수화하는 `FromSqlInterpolated`를 사용하기 때문에 안전합니다.

이것은 일반적으로 안전하지만, 안전하지 않은 'FromRawSql'과 매우 유사할 수 있는 위험이 있습니다. 

var blogs = context.Posts
    .FromSqlInterpolated($"SELECT * FROM Posts WHERE state = {state} AND author = {author}")
.ToList();

Java - 보안: 최대 절전 모드 - 명명된 쿼리 + 네이티브 쿼리

Hibernate는 '네이티브 쿼리'와 '네임드 쿼리'를 통해 안전한 방식으로 쿼리를 구성하는 두 가지 방법을 제공합니다. 두 가지 방법 모두 매개변수의 위치를 지정할 수 있습니다.

@NamedNativeQuery(
        name = "find_post_by_state_and_author",
        query =
        "SELECT * " +
                "FROM Post " +
                "WHERE state = :state" + 
         " AND author = :author",
        resultClass = Post.class)

java
List<Post> posts = session.createNativeQuery(
        "SELECT * " +
        "FROM Post " +
        "WHERE state = :state" +
        " AND author = :author" )
        .addEntity(Post.class)
        .setParameter("state", state)
        .setParameter("author", author)
        .list();

Java - 보안: jplq

jplq 리포지토리 인터페이스에서 '쿼리' 속성에 주석을 달면 여러 가지 형태를 취할 수 있으며 매개변수화할 수 있습니다.

@Query("SELECT p FROM Post p WHERE u.state = ?.1 and u.author = ?.2")
Post findPostByStateAndAuthor(String state, int author);
@Query("SELECT p FROM Post p WHERE u.state = :state and u.author = :author")
User findPostByStateAndAuthor(@Param("state") String state, @Param("author") int author);

자바스크립트 - 보안: pg

'pg` 라이브러리를 사용할 때 `query` 메서드를 사용하면 두 번째 매개변수를 통해 매개변수 값을 제공하여 매개변수화를 수행할 수 있습니다.

const { posts } = await db.query('SELECT * FROM Post WHERE state = $1 AND author = $2', [state, author])

자바스크립트 - 보안: 시퀄라이즈

sequelize` 라이브러리는 쿼리에 대한 설정을 취하는 두 번째 인수를 통해 쿼리를 매개변수화하는 방법을 제공합니다. 여기에는 쿼리에 매개변수로 바인딩할 값의 목록이 이름 또는 인덱스별로 포함됩니다.

await sequelize.query(
    'SELECT * FROM Post WHERE state = $state AND author = $author',
    {
        bind: { state: state, author: author},
        type: QueryTypes.SELECT
    }
);
await sequelize.query(
    'SELECT * FROM Post WHERE state = $1 AND author = $2',
    {
        bind: [state, author],
        type: QueryTypes.SELECT
    }
);