[주요정보통신기반시설 웹 애플리케이션 취약점 분석·평가 21일 완성 코스] 2강 — [Day 2] SQL 인젝션 (SQL Injection)
[Day 2] SQL 인젝션 (SQL Injection) 깊이 파헤치기
안녕하세요! Hackerstory 공부방의 IT/보안 튜터 정다은입니다. 👋
지난 시간 1강에서는 웹 애플리케이션의 명령 실행 흐름을 뒤흔드는 [Day 1] 코드 인젝션에 대해 배웠습니다. 웹 서버 단에서 코드 구문이 주입될 때의 위험성을 잘 이해하셨나요?
오늘 다룰 Day 2 주제는 웹 보안 진단 항목 중 **단연 1순위로 강조되며, 발생 시 피해 규모가 가장 치명적인 'SQL 인젝션(SQL Injection, SQLi)'**입니다.
데이터베이스(DB)는 서비스의 핵심 자산인 회원 정보, 결제 내역, 비밀 데이터 등이 모여있는 ‘보물창고’입니다. 이 보물창고의 문을 부수고 들어가는 SQL 인젝션의 원리부터 유형별 실습 예시, 그리고 확실한 방어 기법까지 하나씩 차근차근 배워보겠습니다!
1. SQL 인젝션이란? (개념 설명)
💡 개념 한 줄 요약
SQL 인젝션은 웹 애플리케이션이 사용자 입력값을 데이터베이스 쿼리문(Query) 생성 시 적절한 검증 없이 **문자열로 결합(Concatenation)**할 때 발생하며, 공격자가 악의적인 SQL 구문을 주입하여 미인가 데이터 조회, 수정, 삭제 및 인증 우회 등을 수행하는 취약점입니다.
동작 원리
웹 애플리케이션은 사용자가 입력한 값(예: 아이디, 검색어 등)을 받아 DB에 전달할 SQL 쿼리를 생성합니다.
정상적인 처리 과정에서는 입력값이 오직 **'데이터(Data)'**로만 취급되어야 합니다. 하지만 개발자가 입력값을 검증하지 않고 SQL 문장과 그대로 이어 붙이면, 공격자가 입력한 문자가 SQL의 **'제어 문자가 포함된 쿼리 구조(Code)'**로 해석되어 DB엔진이 공격자의 의도대로 작동하게 됩니다.
[사용자 입력값] ──> [웹 서버 (문자열 그대로 결합)] ──> [DB 엔진 (악의적 쿼리 실행)]
2. SQL 인젝션의 주요 유형 및 공격 메커니즘
SQL 인젝션은 공격자가 결과를 확인하는 방식에 따라 크게 In-Band(내부 채널), Blind(보이지 않는 채널), Out-of-Band로 나뉩니다. 실무에서 가장 흔히 접하는 4가지 핵심 유형을 예시와 함께 보겠습니다.
① 인증 우회 및 Error-based SQLi (에러 기반)
1) 인증 우회 (Auth Bypass)
로그인 폼 등에서 쿼리의 참/거짓 논리를 조작하여 인증을 통과하는 방식입니다.
- 정상 쿼리 예시:
SELECT * FROM users WHERE username = 'USER_INPUT' AND password = 'USER_INPUT'; - 공격자 입력 (username):
admin' --또는' OR '1'='1 - 완성된 악의적 쿼리:
SELECT * FROM users WHERE username = 'admin' --' AND password = '...';--이후의 비밀번호 검증 구문은 주석 처리되어 무시됩니다. 결국admin계정으로 비밀번호 없이 로그인에 성공하게 됩니다.
2) Error-based SQLi (에러 기반)
웹 페이지에 DB 에러 메시지가 노출될 때, 의도적으로 에러를 유발하여 에러 메시지 안에 원하는 데이터(예: DB 버전, 테이블명 등)를 실어 보낸 뒤 이를 확인하는 기법입니다.
- 공격 예시 (MySQL 기준):
' AND extractvalue(1, concat(0x3a, (SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()))) -- - 결과: 웹 화면에
XPATH syntax error: ':users,orders,products'와 같은 DB 에러 메시지가 출력되며 테이블 목록이 노출됩니다.
② Union-based SQLi (유니온 기반)
SQL의 UNION 연산자를 활용하여, 기존 쿼리의 결과 뒤에 공격자가 원하는 별도의 SELECT 구문을 추가하여 데이터를 뽑아내는 방식입니다.
조건
- 기존 쿼리의 컬럼 개수와 UNION 뒤 쿼리의 컬럼 개수가 일치해야 함
- 각 컬럼의 데이터 타입이 호환되어야 함
- 정상 쿼리 (상품 검색):
SELECT id, title, content FROM board WHERE title LIKE '%검색어%'; - 공격자 입력:
a' UNION SELECT 1, username, password FROM users -- - 완성된 쿼리:
SELECT id, title, content FROM board WHERE title LIKE '%a' UNION SELECT 1, username, password FROM users -- %'; - 결과: 기존 게시글 검색 결과와 함께
users테이블의 아이디와 비밀번호가 화면에 그대로 출력됩니다.
③ Blind SQLi (블라인드 기반)
웹 페이지에 DB 결과나 에러 메시지가 직접 출력되지 않을 때 사용하는 고급 기법입니다. 참(True)과 거짓(False)의 반응 차이 또는 응답 시간 차이를 이용해 데이터를 한 글자씩 알아내는 공격입니다.
1) Boolean-based Blind SQLi (논리 반응 기반)
쿼리 결과가 참일 때와 거짓일 때의 웹 페이지 반응(텍스트 존재 여부, HTTP 상태 코드 등)이 다른 점을 이용합니다.
- 공격 원리:
- "DB 이름의 첫 번째 글자가 'a'인가?" ➔ 거짓 (페이지 정상 출력 X)
- "DB 이름의 첫 번째 글자가 'b'인가?" ➔ 참 (페이지 정상 출력 O)
- 공격 페이로드 예시:
' AND SUBSTRING((SELECT database()), 1, 1) = 'a' --
2) Time-based Blind SQLi (시간 지연 기반)
참/거짓에 따른 페이지 화면 반응조차 동일할 때, 참인 경우 DB를 일의적으로 쉬게(Sleep) 만들어서 응답 시간의 차이로 참/거짓을 판별합니다.
- 공격 페이로드 예시 (MySQL / PostgreSQL):
' IF(SUBSTRING((SELECT database()), 1, 1)='a', SLEEP(5), 0) -- - 결과: 만약 DB 이름의 첫 글자가 'a'라면 서버 응답이 5초 뒤에 오고, 아니면 즉시 응답이 옵니다. 이를 자동화 도구(Python Script, sqlmap 등)로 반복하여 모든 데이터를 탈취합니다.
3. SQL 인젝션 진단 방법 (실무 관점)
실무에서 웹 애플리케이션의 SQLi 취약점을 진단할 때는 다음과 같은 순서로 진행합니다.
- 입력값 검증 테스트:
- 파라미터(GET/POST/Cookie/Header 등)에 작은따옴표(
'), 큰따옴표("), 역슬래시(\) 등을 입력하여 SQL Syntax Error가 발생하는지 확인합니다.
- 파라미터(GET/POST/Cookie/Header 등)에 작은따옴표(
- 논리 연산 테스트:
' AND '1'='1(참 반응 확인)' AND '1'='2(거짓 반응 확인)- 두 요청에 대해 응답 페이지의 길이, 내용, 상태 코드가 다르면 Blind SQLi 가능성이 매우 높습니다.
- 자동화 도구 활용:
- 취약점이 의심되는 파라미터를 확보한 후
sqlmap과 같은 수동/자동화 진단 도구를 통해 데이터베이스 구조 파악 및 취약점 성립 여부를 정밀 진단합니다.
- 취약점이 의심되는 파라미터를 확보한 후
4. 보안 대책 (어떻게 막을 것인가?)
SQL 인젝션을 막는 **가장 확실하고 근본적인 대책은 PreparedStatement(매개변수화된 쿼리)**를 사용하는 것입니다.
🛡️ 근본적 대책: PreparedStatement / Parameterized Query 사용
웹 서버가 DB에 쿼리를 보낼 때 쿼리 문장(컴파일)과 입력 데이터(파라미터)를 완벽히 분리하여 처리합니다. 이렇게 하면 입력값에 ' OR '1'='1 같은 악의적 구문이 들어가도 DB는 이를 쿼리 명령어가 아닌 **단순한 '문자열 데이터'**로 취급하므로 공격이 무력화됩니다.
❌ 취약한 코드 예시 (Java / String 결합)
// 입력값을 직접 쿼리 문자열에 더함 (위험!)
String query = "SELECT * FROM users WHERE id = '" + userId + "' AND pw = '" + userPw + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
⭕ 안전한 코드 예시 (Java / PreparedStatement)
// 쿼리 구조를 먼저 준비 (?)
String query = "SELECT * FROM users WHERE id = ? AND pw = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
// 데이터 바인딩 (입력값이 아무리 특수문자여도 오직 데이터로만 처리됨)
pstmt.setString(1, userId);
pstmt.setString(2, userPw);
ResultSet rs = pstmt.executeQuery();
🛡️ ORM 프레임워크(MyBatis, JPA 등) 사용 시 주의사항
요즘 개발 환경에서는 ORM/퍼시스턴스 프레임워크를 많이 사용합니다. 하지만 사용법을 잘못 익히면 여전히 SQLi에 노출됩니다.
MyBatis 예시
- ❌ 취약 (문자열 치환
${}):SELECT * FROM users WHERE id = ${userId} <!-- 입력값이 그대로 결합됨 --> - ⭕ 안전 (PreparedStatement 바인딩
#{}):SELECT * FROM users WHERE id = #{userId} <!-- 자동 매개변수화 처리됨 -->
💡 참고: 테이블명이나 컬럼명처럼
#{}을 쓸 수 없는 동적 쿼리 영역은 개발 단계에서 입력값을 **화이트리스트(Whitelist)**로 강력히 제한해야 합니다.
🛡️ 심층 방어 (Defense in Depth)
- 입력값 검증: 특수문자 및 SQL 키워드(
SELECT,UNION,DROP등) 필터링 (화이트리스트 방식 권장). - DB 계정 권한 최소화: 웹 애플리케이션이 사용하는 DB 계정에
db_owner나sa같은 과도한 권한을 부여하지 않고, 해당 서비스에 필요한 최소한의 권한(SELECT, INSERT, UPDATE 등)만 부여합니다. - 상세 에러 페이지 비활성화: DB 에러 발생 시 시스템 내부 에러 메시지를 사용자에게 노출하지 않고, 커스텀 에러 페이지로 대체합니다. (이는 Day 4 항목과도 연결됩니다!)
📝 [Day 2] 핵심 요약
| 구분 | 주요 내용 |
|---|---|
| 개념 | 사용자 입력값이 쿼리문 구조를 변조하여 DB를 불법적으로 조작하는 취약점 |
| 주요 유형 | Error-based, Union-based, Boolean-based Blind, Time-based Blind |
| 핵심 원인 | SQL 쿼리 생성 시 문자열을 그대로 이어서 결합(Concatenation)할 때 발생 |
| 최선 방어책 | PreparedStatement (Parameterized Query) 사용, ORM 바인딩(#{}) 활용 |
오늘 공부한 SQL 인젝션은 개념은 명확하지만, 실제 시스템 환경(DB 종류, 웹 프레임워크 구조 등)에 따라 매우 다양한 형태로 변형되어 나타납니다. 따라서 개발 시점부터 **'입력 데이터는 절대 신뢰하지 않고, 쿼리 구문과 분리한다'**는 원칙을 지키는 것이 가장 중요합니다!
🚀 다음 챕터 예고: [Day 3] 디렉터리 인덱싱 (Directory Indexing)
고생 많으셨습니다! 다음 3강에서는 웹 서버의 설정 미흡으로 인해 웹 서버 내부의 파일 및 디렉터리 구조가 리스트 형태로 외부인에게 고스란히 노출되는 [Day 3] 디렉터리 인덱싱 취약점에 대해 알아보겠습니다.
궁금한 점이 있다면 언제든 댓글이나 질문 게시판에 남겨주세요. 다음 시간에 만나요! 감사합니다. 😊
댓글 0
로그인하면 댓글을 작성할 수 있습니다.