
주입 SQL
이제 SQL 인젝션에 대해 살펴볼 때입니다. 오랫동안 이 취약점은 OWASP 상위 10위 목록에서 단연코 1위를 차지해 왔으며, 이는 수년간 지속되었습니다. 비록 약 20년이라는 오랜 역사를 지녔고, 이 목록에서 1위 자리를 약간 내주긴 했지만, 여전히 매우 흔하고 위험한 취약점입니다.
웹 보안 취약점으로서 SQL 인젝션(SQLi)은 공격자들이 데이터베이스를 조작하고 중요한 정보를 추출할 수 있게 해주기 때문에 여전히 가장 흔히 사용되는 '해킹' 기법 중 하나입니다. 더욱 우려되는 점은 공격자가 데이터베이스 서버 관리자로 위장하여 데이터베이스 파괴, 거래 조작, 데이터 유출 및 기타 취약점 노출과 같은 치명적인 작업을 수행할 수 있다는 것입니다.
이것이 어떻게 진행되는지 살펴보자
SQL(구조적 쿼리 언어)은 관계형 데이터베이스와 통신하는 데 사용되는 언어입니다. 개발자, 데이터베이스 관리자 및 애플리케이션이 매일 생성되는 방대한 양의 데이터를 관리하기 위해 사용하는 쿼리 언어입니다.
애플리케이션 내부에는 두 가지 컨텍스트가 존재합니다: 하나는 데이터를 위한 것이고, 다른 하나는 코드를 위한 것입니다. 코드 컨텍스트는 컴퓨터가 무엇을 실행해야 하는지 지시하며, 처리할 데이터와 분리됩니다. SQL 인젝션은 공격자가 SQL 인터프리터가 실수로 코드로 처리하는 데이터를 입력할 때 발생하며, 이를 통해 애플리케이션으로부터 가치 있는 정보를 수집할 수 있게 됩니다.
SQL 인젝션 공격의 영향
SQL 인젝션은 모든 웹 애플리케이션에 극도로 위험할 수 있으며, 공격자가 권한 없이 중요한 데이터에 접근할 수 있게 해주기 때문에 수많은 유명 보안 침해 사건의 주요 수단으로 활용되어 왔습니다. 공격자는 사용자 이름과 비밀번호, 신용카드 정보, 개인 식별 번호 등 다양한 정보를 열람할 수 있습니다.
이러한 데이터에 접근한 후 공격자는 계정을 장악하고, 비밀번호를 재설정하며, 장기간 온라인 구매를 수행하거나 (훨씬 더 심각한) 다른 유형의 사기를 저지를 수 있습니다.
그러나 SQLi의 가장 우려되는 점은 공격자가 탐지되지 않을 경우 시스템에 장기간 백도어를 유지할 수 있다는 것입니다. 상상할 수 있듯이, 백도어가 열려 있는 동안 반복적인 데이터 유출이 발생할 수 있습니다. 정말 무서운 일이죠.
실제 적용 사례를 살펴보며 그 작동 방식을 더 잘 이해해 보겠습니다.
SQLi 예시
SQLi에는 다양한 상황에 대응할 수 있는 여러 취약점 기술이 포함됩니다. 아래는 가장 흔한 SQLi 예시 몇 가지입니다:
SQLi 유형
자, 이제 세 가지 유형의 SQLi를 살펴보겠습니다.
SQLi 인트라밴드
이는 가장 흔하고, 가장 단순하며, 가장 효과적인 SQL 인젝션 유형 중 하나입니다. 이 유형의 공격에서는 동일한 통신 경로를 사용하여 공격을 수행하고 결과를 회수합니다.
두 가지 유형의 인트라밴드 SQLi 공격은 다음과 같습니다:
- 유니온 기반 SQLi - 유니온 기반 공격은 유니온 연산자를 사용하여 두 개 이상의 SQL 쿼리(예: SELECT 문)를 결합함으로써 원하는 정보를 획득하고 HTTP GET 응답을 얻습니다.
- 오류 기반 SQLi - 공격자는 데이터베이스의 오류 메시지를 활용하여 데이터베이스 구조를 파악합니다. 이 공격 과정에서 공격자는 서버가 오류 메시지를 표시하도록 허위 요청을 전송하거나 특정 작업을 수행하여 데이터베이스 정보를 획득할 수 있습니다. 따라서 개발자는 실제 운영 환경에서 오류 메시지를 전송하거나 기록하지 않도록 주의해야 하며, 대신 접근이 제한된 방식으로 저장해야 합니다.
추론적 SQL
추론형 또는 맹목적 SQLi 공격은 더 복잡하며, 이를 악용하는 데 더 많은 시간이 소요될 수 있습니다. 게다가 공격자는 공격 결과를 즉시 얻지 못하므로, 이는 맹목적 공격이 됩니다.
공격자는 HTTP 요청을 통해 데이터베이스 서버에 페이로드를 전송하여 사용자 데이터베이스를 재구성한 후, 응답과 애플리케이션의 동작을 관찰하여 공격이 성공했는지 여부를 확인합니다.
추론형 SQLi 공격에는 두 가지 유형이 존재합니다:
- 부울 기반 블라인드 SQLi - 이 공격에서는 부울 값(참 또는 거짓)을 얻기 위해 데이터베이스에 쿼리를 전송하고, 공격자는 HTTP 응답을 관찰하여 부울 결과를 예측합니다.
- 시간 기반 블라인드 SQLi - 이 공격에서 공격자는 데이터베이스에 쿼리를 전송하여 응답을 보내기 전에 몇 초 동안 대기하도록 하고, 공격자는 HTTP 쿼리의 응답 시간에 따라 쿼리 결과를 평가합니다.
SQLi 외부 처리
이는 데이터베이스 서버의 활성화된 기능에 의존하는 보다 드문 유형의 SQLi 공격입니다. 공격자가 다른 유형의 공격을 실제로 사용할 수 없는 경우에 발생합니다.
예를 들어, 동일한 통신 채널을 이용한 인밴드 공격이 불가능하거나, HTTP 응답이 쿼리 결과를 계산하기에 충분히 명확하지 않은 경우입니다.
또한, 공격자가 요구하는 데이터를 전송하기 위해 데이터베이스 서버가 HTTP 또는 DNS 쿼리를 수행할 수 있는 능력에 크게 의존하기 때문에 흔히 발생하지 않습니다.
SQLi로부터 어떻게 방어할 것인가
다행히도 SQL 인젝션의 장점은 너무 오래되고 흔하기 때문에 이를 방지할 수 있는 방법이 존재한다는 점입니다. 이러한 유형의 예방 기술을 사용하는 것은 단순히 좋은 코딩 습관일 뿐만 아니라, 조직의 SQLi에 대한 보안도 강화할 것입니다.
데이터베이스 서버를 이러한 유형의 공격으로부터 보호하는 방법은 여러 가지가 있습니다. 예를 들어, 입력 검증, 웹 애플리케이션 방화벽(WAF) 사용, 데이터베이스 보안 강화, 보안 전문 인력 또는 제3자 보안 시스템 활용, 그리고 오류 없는 SQL 쿼리 작성 등이 있습니다.
위에서 언급한 보안 조치 중 하나를 사용하여 Python에서 SQL 인젝션 방지를 위한 예시를 살펴보겠습니다.
Python 예제
이 예시에서 공격자는 부울형 블라인드 SQL 인젝션을 사용하여 시스템의 중요한 정보를 수집할 것입니다.
Python : 취약한
데이터베이스에 "sample_data"라는 테이블이 존재한다고 가정합시다. 이 테이블에는 애플리케이션 사용자들의 사용자 이름과 비밀번호가 포함되어 있습니다.
이제 사용자가 다음 명령을 사용하여 이 데이터베이스 테이블에서 값을 찾을 수 있도록 허용하십시오:
importer mysql.connector
base de données = mysql.connector.connect
Pratique #나쁜 코드. 피하세요! 학습용입니다.
(host="localhost », user="newuser », passwd="pass », db="sample »)
cur = db.cursor ()
name = raw_input ('이름을 입력하세요 : ')
cur.execute (« SELECT * FROM sample_data WHERE Name = '%s' ; » % name) pour la ligne dans cur.fetchall () : print (row)
db.fermer ()
주입 SQL
여기서 사용자가 검색창에 이름(예: Alicia)을 입력하면 출력에 아무런 문제가 없습니다.
그러나 사용자가 "Alicia"; DROP TABLE sample_data;와 같은 내용을 입력하면 데이터베이스에 심각한 영향을 미칩니다.
파이썬 : 문제 해결
SQL 명령어는 공격이 발생하지 않도록 다음과 같이 수정해야 합니다:
cur.execute(« SELECT * FROM SAMPLE_DATA WHERE Name = %s ; », (name,))
이제부터 시스템은 사용자가 SQL 쿼리를 주입하려고 시도하더라도 사용자 입력을 문자열로 처리하며, 사용자 입력을 이름의 값으로만 처리합니다.
이 간단한 수정으로 향후 요청 시 악의적인 활동을 차단하고 사용자의 입력 공격으로부터 시스템을 보호할 수 있습니다.
예제 Java
이 예제에서는 애플리케이션의 사용자 데이터를 저장하는 "sample_data"라는 데이터베이스 테이블도 사용할 것입니다.
기본 로그인 페이지는 사용자 이름과 비밀번호를 사용하며, Java 파일(서블릿인 LoginServlet)은 이를 데이터베이스와 대조하여 로그인 작업을 허용합니다.
Java : 취약점 예시
데이터베이스의 "sample_data" 테이블을 사용하여, 시스템은 사용자가 자신의 인증 정보를 입력으로 사용하여 연결 작업을 수행할 수 있도록 합니다.
LoginServlet 파일에는 로그인 작업에 적합한 요청이 포함되어 있습니다. 즉:
//Mauvais exemple. N'utilisez pas la concaténation de chaînes.
String query = « select * from sample_data where username=' » + username + « 'and password =' » + password + « '» ;
Connexion conn = null ;
Déclaration stmt = null ;
essayez {
conn = DriverManager.getConnection (« jdbc:mysql : //127.0.0. 1:3306 /user », « root ») ;
stmt = conn.createStatement () ;
ResultSet rs = STMT.ExecuteQuery (requête) ;
si (rs.next ()) {
//Connexion réussie si une correspondance est trouvée
succès = vrai ;
}
} catch (Exception e) {
e. printStackTrace () ;
} enfin {
essayez {
stmt.fermer () ;
conn.close () ;
} catch (Exception e) {}
}
si (succès) {
response.sendRedirect (» home.html «) ;
} autre {
Response.sendRedirect (» login.html ? erreur = 1") ;
}
}
사용자 연결 요청입니다:
* from sample_data where username = username and password = password
주입 SQL
입력이 유효하면 시스템은 완벽하게 작동할 것입니다. 예를 들어, 사용자 이름이 다시 Alicia이고 비밀번호가 secret이라고 가정해 보겠습니다.
시스템은 해당 인증 정보와 함께 사용자 데이터를 반환합니다. 그러나 공격자는 Postman 및 cURL을 사용하여 사용자 요청을 조작하여 SQL 인젝션을 수행할 수 있습니다.
예를 들어, 해커는 가상의 사용자 이름(Alicia)과 "or '1'='1'"이라는 비밀번호를 보낼 수 있습니다.
이 경우 사용자 이름과 비밀번호는 일치하지 않지만, "1'='1" 조건은 여전히 참이므로 로그인 작업이 성공합니다.
자바 : 예방
예방 차원에서 LoginValidation 코드를 수정하여 쿼리 실행 시 Statement 대신 PreparedStatement를 사용해야 합니다. 이 변경 사항은 쿼리에서 사용자 이름과 비밀번호를 연결하는 것을 방지하고, 이를 설정 데이터로 처리하여 SQL 인젝션을 방지합니다.
아래는 수정된 LoginValidation 코드입니다:
String query = « select * from sample_data where username= ? et mot de passe = ? » ;
Connexion conn = null ;
PreparedStatement stmt = null ;
essayez {
conn = DriverManager.getConnection (« jdbc:mysql : //127.0.0. 1:3306 /user », « root ») ;
stmt = Conn.PrepareStatement (requête) ;
STMT.setString (1, nom d'utilisateur) ;
STMT.setString (2, mot de passe) ;
ResultSet rs = STMT.executeQuery () ;
si (rs.next ()) {
succès = vrai ;
}
rs.fermer () ;
} catch (Exception e) {
e. printStackTrace () ;
} enfin {
essayez {
stmt.fermer () ;
conn.close () ;
} catch (Exception e) {
}
}
이 경우 PreparedStatement, 세터 메서드 및 기본 JDBC API가 사용자 입력을 처리하여 SQL 인젝션을 방지합니다.

예시
이제 우리는 실제 적용 사례를 더 잘 이해하기 위해 다양한 언어의 추가 예시를 살펴보겠습니다.
C# - 비보안
이 예제는 FromRawSQL을 사용하기 때문에 안전하지 않습니다. 이 메서드는 매개변수를 바인딩하지 않으며 이스케이프 처리도 시도하지 않습니다. 따라서 이 메서드는 반드시 피해야 합니다.
var blogs = context.POSTS
.fromRawSQL (« SÉLECTIONNEZ * PARMI LES ARTICLES OÙ state = {0} ET author = {1} », state, author)
.toList () ;
C# - 보안
이 예제는 FromSQLInterpolated를 통해 안전하게 처리됩니다. FromSQLInterpolated는 보간된 값을 가져와 매개변수로 변환합니다.
비록 일반적으로 안전하지만, FromRawSQL과 매우 유사할 수 있으며 이는 안전하지 않습니다.
var blogs = context.POSTS
.fromSQLInterpolated ($"SÉLECTIONNEZ * PARMI LES ARTICLES OÙ state = {state} ET author = {author} »)
.toList () ;
Java - 보안: Hibernate - 명명된 쿼리 + 네이티브 쿼리
Hibernate는 '네이티브 쿼리'와 '네임드 쿼리'를 통해 안전하게 쿼리를 구성하는 두 가지 방법을 제공합니다. 두 방법 모두 매개변수의 위치를 지정할 수 있습니다.
@NamedNativeQuery (
name = « find_post_by_state_and_author »,
requête =
« SÉLECTIONNEZ *" +
« DE LA POSTE » +
« WHERE state =:state » +
« ET auteur = : auteur »,
Classe de résultats = Post.class)
java
Liste des <Post>messages = Session.createNativeQuery (
« SÉLECTIONNEZ *" +
« DE LA POSTE » +
« WHERE state =:state » +
« ET auteur = : auteur »)
.Ajouter une entité (Post.class)
.setParameter (« état », état)
.setParameter (« auteur », auteur)
.liste () ;
Java - 보안: jplq
jplq 저장소 인터페이스에 `Query` 속성을 주석 처리함으로써, 여러 형태를 취할 수 있으며 매개변수화됩니다.
@Query("SELECT p FROM MESSAGE p WHERE u.state = ?1 AND u.author = ?2 INDEX:
") 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 MESSAGE WHERE STATUS = 1$ AND AUTHOR = 2$ », [status, author])
자바스크립트 - 보안: 시퀀리즈
Sequelize 라이브러리는 두 번째 인자를 통해 쿼리를 매개변수화하는 방법을 제공합니다. 이 인자는 쿼리의 매개변수를 받아들입니다. 여기에는 쿼리에 매개변수로 바인딩할 값들의 목록이 포함되며, 이름 또는 인덱스로 지정할 수 있습니다.